ralphjsmit/filesystem
A Laravel package that wraps filesystem operations in a clean, expressive API. Create, copy, move, delete, and inspect files and directories with readable methods, keeping common disk tasks simple and consistent across your app's storage disks.
Architecture fit. The package is a thin fluent wrapper over Laravel's Storage/Flysystem layer — not a subsystem. There is no service provider, config file, migration, driver registration, or plugin model to reason about, so it does not add an architectural layer; it adds syntax on top of one that already exists. That is the correct shape for what it is, and it caps both the upside and the downside. From a product-architecture standpoint the defensible move is to treat the package as an implementation detail behind an internal FilesystemService interface: domain code, controllers, jobs, and commands never reference RalphJSmit\*. With that seam in place, the decision becomes reversible in hours rather than a codebase-wide sweep. Without that seam, this is a leaf dependency that quietly spreads, and it should not be adopted.
Integration feasibility. Straightforward on a supported Laravel version. composer require ralphjsmit/filesystem:^1.4, pin the lock entry, read src/File.php and src/Directory.php (the entire API surface, roughly ten minutes), then convert a single call site. There is no service provider to publish and no scaffolding step to misconfigure. The realistic adoption cost is measured in hours, not sprints — which is precisely why the governance around it (pinning, seam, lint rule) matters more than the technical work. Feasibility is bounded less by engineering effort than by whether the team will commit to the containment rule, the Storage::fake() verification, and the version-pin discipline.
Technical risk. Contained but real, and concentrated in four places:
1.4.0 (Feb 2023) added Laravel 10 support and is the only published compatibility data point. Laravel 11+ is unproven. On a modern framework, native Storage should be the default answer.UnableToWriteFile), package-specific wrappers, or swallowed booleans. A swallowed failure inside an archival move is a silent data-loss path. Equally, string-vs-stream signatures and path-traversal handling are unverified and must be treated as such. Storage::fake() compatibility is the explicit adoption gate, not an afterthought.Key questions to resolve before any commitment:
Storage::fake() still work through our adapter, and do existing file-workflow tests keep passing unchanged?Storage:: call sites do we have? (Fewer than ~a dozen is the documented stop signal: keep native Storage.)RalphJSmit\ outside app/Services/Filesystem/?Stack fit. Good, conditionally. It assumes Laravel and an existing Storage/Flysystem dependency, so the marginal risk is contained to what we already run. The value is ergonomics, not capability — every operation it exposes is one Storage already performs. If the requirement is multipart uploads, signed-URL policy, image transforms, virus scanning, retry logic, or resumable transfers, this is the wrong tool and must not be stretched into one.
Migration path. Deliberately narrow and reversible:
grep for Storage:: establishes the addressable surface. Small surface → stop and keep native Storage.app/Services/FilesystemService with only the operations the product needs, named in domain terms (archiveInvoice(), storeAvatar(), exportReport()), taking an explicit disk parameter on every entry point. The package goes behind this interface.Storage::fake('local') through the adapter before writing call sites. If fakes bypass to Flysystem and break, that is a stop-the-line finding — native Storage wins on the argument we're actually having.Storage::put() gains nothing and adds an import.RalphJSmit\ outside the service directory. Ten minutes of work; the difference between a reversible dependency and a scattered one.Compatibility. Pin 1.4.* and record the reviewed version and date in composer.json or the PR description. Do not float to ^1.0 and let a future tag surprise us. Re-check the repo at every framework bump — including and especially the Laravel 11+ upgrade, which no release claims. In jobs and queues: wrap in the adapter, pass primitives in the constructor, resolve in handle(). Never serialize a File or Directory object into a queue payload — that turns a version swap into a queue-drain problem.
Sequencing. Seam → test gate → one internal call site → lint rule → optional expansion. The seam and the test gate are prerequisites, not parallel work. If either fails, the correct outcome is to abandon the package and keep native Storage, and that should be an explicitly pre-agreed decision rather than a debate held under deadline.
Maintenance. Low-volume but non-zero, and it falls entirely on us. No vendor, no LTS, no visible maintainer team. The realistic budget is a couple of hours a year to review the release stream and confirm compatibility, or half a day to remove it. That number is small enough to accept and specific enough to plan against — the failure mode is not the cost, it is forgetting the dependency exists until a framework upgrade forces the discovery. Mitigate with a short note in the adapter class recording the pinned version, the native Storage equivalent of each method, and who reviewed it. That note is the artefact that makes the next swap an afternoon rather than an archaeology exercise.
Support. No support channel beyond the repository. No SLA, no security-patching commitment, no downstream ecosystem surfacing bugs on our behalf. The README is the specification; the two source files are the contract. This is acceptable for internal tooling and unacceptable for anything in a regulated, audited, or compliance-bound data flow (residency, retention, encryption at rest), where first-party Storage with its explicit upgrade and security coverage is the right default.
Scaling. Effective ceiling is the underlying Storage layer, so there is no scaling cliff introduced — but there is a cost profile that changes with distance. On local disk, chains are effectively free. On S3 or SFTP, every exists(), get(), and put() in a chain is a network round trip, and the fluent syntax obscures the count. Before adopting on a remote disk, count calls in the typical chain and decide whether readability justifies the trips; where it doesn't, batch or restructure rather than chain. Separately, if read/write methods are string-only, they load contents into memory — fine for config and small exports, wrong for anything in the hundreds of megabytes. Keep heavy lifting on Storage/Flysystem directly and let the package own the small-file convenience paths.
Failure modes to plan for:
Storage::fake() doesn't intercept, we've traded test coverage for syntax. Adoption gate, verified before commitment.Ramp-up. Very shallow. No service provider, no config, no migration; the entire API is two small classes. An engineer can read both files and be productive in under an hour. This is a genuine developer-experience lever for engineers who touch storage infrequently — frontend-leaning devs, contractors, new hires — and the measurable benefit shows up as shorter PR review time on internal tooling. The containment rule ("domain code never imports this") is the one thing that must be taught alongside it, and it's a one-line convention rather than a training programme.
Decision framing for stakeholders. Position this as a tactical, leaf-level dependency on the architecture map — not a platform standard and explicitly not an org-wide storage abstraction. It is not cover for skipping a storage abstraction; it is a thinner doorway to the same Flysystem layer. Standardising on it is defensible only if two or more teams have already written overlapping in-house wrappers, in which case consolidating is a net reduction in surface area. The real product decision is the internal FilesystemService interface; the package is an implementation detail behind it, and the honest comparison is package-versus-in-house-helper, not package-versus-nothing.
How can I help you explore Laravel packages today?