leovie/php-filesystem
A lightweight PHP filesystem library offering an object-oriented API for common file and directory operations such as reading, writing, listing, copying, and deleting. A simple drop-in helper for projects that need clean, dependency-free filesystem handling.
Install it directly (pin the major you've verified — see below, this is now a 2.x line):
composer require leovie/php-filesystem
Where to look first: the v2.0.0 diff, then src/. The last assessment said the newest release was May 2022; that's now out of date — there is a v2.0.0 followed by v2.0.1, so the package has been revived at least once. The major-version bump is the thing you actually need to read: compare v1.x...v2.0.0 on the repo and confirm the namespace (LeoVie\PhpFilesystem\…), the class name, the method signatures, and the PHP constraint in composer.json. Do that before you write any code against the snippet below, because a 2.0 that nobody has written about is exactly where naming drifts.
After the major-version check, the rest is quick: this is still a single-purpose wrapper ("some small filesystem operations") with no service provider, no facade, no published docs. Open src/ and read every file — you'll be done in a few minutes — and confirm whether a test suite appeared in the 2.x line, because that changes how much you trust edge-case behaviour.
The shape you're likely working with is a plain instantiable class with stateless methods:
use LeoVie\PhpFilesystem\Filesystem;
$fs = new Filesystem();
$fs->createDir('/var/www/app/storage/exports');
$fs->writeFile('/var/www/app/storage/exports/report.txt', $contents);
$content = $fs->readFile('/var/www/app/storage/exports/report.txt');
$fs->deleteFile('/var/www/app/storage/exports/report.txt');
Verify against 2.x before committing. If the signatures moved, adjust; the wrapper's value is unchanged — it collapses the file_exists → mkdir → file_put_contents → chmod dance into one call.
First use case: an Artisan command or scheduled job that manipulates absolute local paths — creating a working directory, dropping a generated file next to a binary, cleaning up temp artifacts. That's the sweet spot: places where reaching for Storage::disk() would force you to define a disk for something that isn't really "storage," and where file_put_contents() with manual mkdir/is_dir checks is getting noisy.
Do a two-minute sanity check before adopting: write one file, read it back, delete it, and confirm the exceptions thrown on failure are ones you can reasonably catch in a catch (Throwable) around a command. Also check it installs alongside your existing composer.lock without conflict — see the roave/security-advisories note below, which is the one genuinely consumer-visible change in this release.
Wrap it, don't scatter it. Zero dependents, one star, and an uneven release history (a 2022 tag, then silence, then a 2.0.x revival). Maintenance is non-zero again, which is better than before — but treat it as vendored utility code anyway. Put it behind your own interface so you can swap the implementation later without touching call sites.
namespace App\Support;
interface LocalFiles
{
public function put(string $path, string $contents): void;
public function get(string $path): string;
public function ensureDir(string $path): void;
}
final class WrappedLocalFiles implements LocalFiles
{
public function __construct(private \LeoVie\PhpFilesystem\Filesystem $fs) {}
public function put(string $path, string $contents): void
{
$this->fs->writeFile($path, $contents);
}
// ...
}
Bind the interface as a singleton in a service provider — the underlying class is stateless, so new per call is harmless, but a singleton keeps your container clean and makes mocking in tests trivial.
Pin the major in composer.json. Given the version history (1.x → long gap → 2.0.0 → 2.0.1), ^2.0 is the constraint you want, not * or an unpinned require. If a 2.1 or 3.0 lands while you're not looking, you don't want a composer update to silently change filesystem semantics in a deploy script.
Use it for the "outside the disk" cases. The natural division of labour in a Laravel app:
| Task | Use |
|---|---|
| User uploads, S3, public assets | Storage::disk() / File facade |
Reading composer.json, .env, package manifests |
File::get() |
| Arbitrary absolute paths, temp work dirs, subprocess artifacts | This wrapper (or symfony/filesystem) |
| Copying a directory tree with progress | Neither — use symfony/filesystem or shell |
Wrapping this package instead of using Illuminate\Support\Facades\File only pays off if you want the class decoupled from the framework (e.g., shared with a CLI tool or a non-Laravel package). If everything is Laravel, File:: already does all of this and has better error messages.
In queued jobs, make it idempotent. Filesystem ops in jobs get retried. Before writing, make sure the directory exists (or that your wrapper's ensureDir is a no-op when present). Before deleting, tolerate "not found" as success — otherwise a retried job dies on the second attempt.
In tests, avoid touching the real disk. There's no virtual-filesystem support here; mock your LocalFiles interface or use Storage::fake() for the Laravel-flavoured parts. Never let a unit test hit /var/....
Compose with Artisan output. For CLI tasks, wrap each call and log the path:
$this->info("Writing {$path}");
$fs->writeFile($path, $json);
When something fails, the exception alone usually won't tell you which of your five writes broke.
File::-style usage is wrong.roave/security-advisories moved to dev — this is good news, and mildly install-affecting. In v2.0.1 it was relocated from require to require-dev. That package is a meta-package of conflict rules blocking known-vulnerable dependency versions; when it sits in a library's runtime require, every consumer inherits those conflicts, which is a classic cause of "your package blocks my composer update" tickets. If you previously had to work around this package in a resolution conflict, retry on 2.0.1. Note the flip side: it no longer runs in your CI unless you require it in your own require-dev — which you should, if you were relying on it transitively (you shouldn't have been, but it happens).\ErrorException, \RuntimeException, or native warnings bubble up inconsistently. A major version is precisely when an author introduces a FilesystemException hierarchy or tightens return types. Re-audit every method's failure mode once on the version you install, then normalise them in your adapter. Verify by triggering a permission-denied write in a scratch container.~, and resolve relative paths against the process CWD (which in a queue worker is not your project root). Always pass absolute paths built from base_path() or storage_path().file_put_contents()-style write means a reader can observe a partial file, and a crash mid-write leaves a truncated artifact. For anything another process consumes, write to $path . '.tmp' and rename() — check whether the wrapper exposes a move/rename method; if not, use rename() directly.chmod() afterwards if you need predictable permissions.app/Support/ rather than patching vendor/.league/flysystem is the correct tool; this package is deliberately not that. Worth a quick grep of the 2.0.0 diff in case a stream abstraction was added alongside the bump.strace -f -e trace=file php artisan your:command) or add a dd(realpath($path), is_writable(dirname($path))) before the call. Nine times out of ten it's the parent directory, not the file.symfony/filesystem's Finder/Filesystem for those rather than chaining five calls here.Bottom line for day-to-day use: this is a convenience shim for short, imperative file work on the local disk. The 2.0.1 release is a dependency-hygiene fix with no API implications in itself — the real news is that a 2.0 line exists at all, so re-baseline your assumptions (PHP constraint, class/method signatures, exception behaviour) against the 2.0.0 diff rather than against the 2022 tag. Then adopt it behind an interface, in one place, for tasks where File:: feels heavy or where you want framework independence — and keep symfony/filesystem and Storage available for everything else.
How can I help you explore Laravel packages today?