michaeldrennen/local-file
A lightweight Laravel package for working with files on the local filesystem. Read, write, and manage local files through a simple, expressive API that keeps file handling clean and consistent in your PHP applications.
Know what you're adopting first. michaeldrennen/local-file is a plain-PHP utility library (MIT, ~3 stars, last tagged release March 2021, zero known dependents). It is not a Laravel package in the usual sense: expect no service provider, no auto-discovery, no facade, no config file to publish. It is a thin wrapper over fopen/file_put_contents/unlink-style operations. Before writing a single line against it, read the single source class end-to-end — at this size the entire API surface fits on one screen, and that read is faster than guessing method names.
Install:
composer require michaeldrennen/local-file
Then confirm what you actually got:
# What namespace, what methods, what PHP constraint?
composer show michaeldrennen/local-file
cat vendor/michaeldrennen/local-file/composer.json
ls vendor/michaeldrennen/local-file/src/
Where to look first: the constructor signature (does it take a path, or is the path per-call?) and the exception behaviour (does it throw, return false, or silently warn?). Everything else in your integration follows from those two answers.
First use case — the one that justifies the dependency: a one-off CLI/artisan command or importer that needs to read a CSV, write a temp file, and clean up. Something small, local, and not worth a full service class:
use MichaelDrennen\LocalFile\...; // verify exact FQCN from src/
$file = new LocalFile('/path/to/import.csv'); // verify ctor shape
if ($file->exists()) {
$contents = $file->read(); // verify method name/return
}
The pragmatic first decision: if your use case is inside a Laravel app, ask whether Illuminate\Support\Facades\File or Storage::disk('local') already covers it. They almost certainly do, and they are maintained. Adopt this package only where its specific convenience methods save you real code, and keep it behind your own boundary so that trade-off stays reversible.
newBecause there's no service provider, register the binding yourself in a FilesystemServiceProvider or AppServiceProvider:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->bind(LocalFileFactory::class, function () {
return new LocalFileFactory(); // or whatever the entry point is
});
}
Then inject your own wrapper, never the vendor class, into application code. This is the single highest-value pattern here: a 3-star, 2021-era dependency should be one use statement away from removal.
namespace App\Support;
use MichaelDrennen\LocalFile\...; // the vendor class
class LocalFileStore
{
public function put(string $absolutePath, string $contents): void
{
// normalise, then delegate
$this->guardPath($absolutePath);
// ... vendor call, verified against src/ ...
}
public function get(string $absolutePath): string
{
$this->guardPath($absolutePath);
// ... vendor call ...
}
private function guardPath(string $path): void
{
$real = realpath($path) ?: $path;
if (! str_starts_with($real, base_path())) {
throw new \RuntimeException("Path escapes app root: {$real}");
}
}
}
This gives you the package's ergonomics today and a five-minute swap to File:: tomorrow.
Storage paths to real local pathsThe package works on real filesystem paths. Laravel's Storage works on disk-relative paths. If your files live on the local disk, convert explicitly — this is the most common integration seam:
$absolute = Storage::disk('local')->path('imports/2024-06.csv');
// now pass $absolute to the vendor class
Do not do this for s3 or any remote disk — ->path() will throw or lie. If the disk could be remote, branch on the driver:
if (Storage::disk($disk)->getDriver()->getAdapter() instanceof LocalFilesystemAdapter) {
// safe to use local-file
} else {
// use Storage:: API only
}
The classic day-to-day shape — a queued job that ingests a dropped file, writes an artefact, and deletes the source on success:
public function handle(LocalFileStore $files): void
{
$source = Storage::disk('local')->path($this->incomingPath);
$rows = $files->get($source); // read
$out = $files->put($this->outputPath, $this->transform($rows)); // write
$files->delete($source); // cleanup — only after the write succeeds
}
Order matters: delete last, after the write is durable. A 2021-era helper will not give you transactional semantics for you.
Since the wrapper is yours, fake it in tests instead of touching the real filesystem:
$this->mock(LocalFileStore::class, function ($mock) {
$mock->shouldReceive('get')->once()->andReturn("a,b\n1,2");
});
For the few tests that must hit disk, use Storage::fake('local') for the Laravel side and tmpfile()/sys_get_temp_dir() for the vendor side — never write into the repo.
It is unmaintained — treat that as a design constraint, not a warning label. Last release 2021-03-23. It may predate PHP 8.1+ features, may use create_function-era idioms, and will not receive security patches (path traversal, symlink handling, TOCTOU). Pin the exact version in composer.json ("michaeldrennen/local-file": "1.0.*" — verify the actual constraint) so a composer update on a transitive dep can't shift behaviour under you.
Zero dependents means zero field-testing. You are the field test. Budget an hour to write characterisation tests against the real filesystem — existence checks, empty files, unicode filenames, filenames with spaces, nested dir creation, and what happens when the parent directory doesn't exist. You will find at least one surprise around return values or missing-directory handling.
Return-value ambiguity is the top bug source. Small helper libs commonly mix conventions: false on failure, 0 bytes written, true on unlink, and PHP warnings on top. Never write if ($file->write($data)) until you've read the source and confirmed. Prefer checking the observable result (file_exists, filesize) over trusting a boolean.
Path traversal if any path segment is user-supplied. The library has no reason to validate paths — it's a file helper, not a security boundary. Any filename arriving from a request must go through basename() plus an allowlist before it reaches the package. The guardPath() helper above is the cheap version of this.
Symlinks and realpath. If you validate paths by prefix, resolve with realpath() first, or a symlink inside your app root will happily point at /etc/passwd. Note realpath() returns false for paths that don't exist yet, so validate before creation carefully.
Atomic writes. If a reader (web request, queue worker) can touch the file while you write it, you get partial reads. The fix is universal and library-agnostic: write to $path . '.tmp', then rename() — rename is atomic within a filesystem. If the package offers a write that opens the destination directly, wrap it or skip it.
Permissions and umask. Files created by a queue worker may not be readable by your web user. Set the group/umask deliberately, or chmod post-write. This bites hardest in Docker where CLI and FPM run as different users.
Large files — check whether it's file_get_contents under the hood. If so, a 200 MB import will hit memory_limit. For anything above a few MB, stream with native fopen/fgets and use the package only for the small-file conveniences.
Windows path separators. If dev machines are Windows and prod is Linux, hardcoded / in constructed paths will work on both for the most part, but DIRECTORY_SEPARATOR-sensitive comparisons in your own validation code will not. Test the guard logic on both.
Extension points are effectively "none" — so create them yourself. No interfaces, no events, no config. Your LocalFileStore wrapper is the extension point. Add a Filesystem interface with your three or four methods (get, put, delete, exists) and bind it to the vendor-backed implementation. When you're ready to drop the dependency — and given the maintenance signal, plan for that — you swap the binding to an Illuminate\Filesystem implementation and delete one class.
Escalation threshold. If you find yourself needing more than ~3 calls into this package, or wanting to wrap most of its methods, that's the signal to stop and use Illuminate\Support\Facades\File instead. The convenience-to-risk ratio flips quickly with a 3-star, four-year-stale library.
How can I help you explore Laravel packages today?