sfcod/filesystem
Sfcod Filesystem is a Laravel package that simplifies working with files and directories through convenient helpers for uploads, storage, copying, moving, and deletion, while integrating cleanly with Laravel's Storage and filesystem abstractions.
The published description is the only prose documentation that ships with this package, so treat the source as the spec. Budget 30–45 minutes to read it before writing any code.
Install and pin
composer require sfcod/filesystem:^1.1
Pin to ^1.1 rather than dev-main. With 9 stars, 0 dependents, and a single release in 2022, you want a lockfile entry you can diff and roll back, not a moving target.
Where to look first, in this order
composer.json of the package — the require block is the real documentation. Check the illuminate/* / laravel/framework constraint and the league/flysystem constraint. The 1.1.0 changelog says only "Updated to SD 5.4"; if SD is an upstream storage dependency, this constraint tells you whether the package targets Flysystem 1/2 (Laravel ≤8 era) or Flysystem 3 (Laravel 9+). If it caps you below Flysystem 3, it will not install cleanly on a modern Laravel app — stop here.src/ — find the service provider first (auto-discovery or manual registration), then the class the provider binds. That class is the API surface; read every public method signature. Most thin wrappers expose a handful of read/write/copy/delete/exists/files-style methods plus path helpers.getFacadeAccessor() and any alias registered in extra.laravel.aliases.Your first use case
The package's stated value is removing Storage::disk('s3')->put(...) boilerplate for everyday read/write/copy/delete. So the smallest useful trial is:
config/filesystems.php (the package claims to drop into existing Storage config, so no new config file should be required — verify by checking the provider for publishes() and mergeConfigFrom()).local, then repeat against an S3-compatible disk.Storage call. That single exercise tells you whether the wrapper is a faithful pass-through or has its own conventions.Do not integrate it into application code until you have confirmed three things: it resolves on your Laravel/Flysystem version, it delegates to the Storage facade (not its own Flysystem instantiation), and its return/exception contract matches what your error handling expects.
Treat it as sugar, not architecture. The package is infrastructure and explicitly not a differentiator. Put it behind your own small interface — a FileStore contract with the four or five operations you actually use — and let one adapter call sfcod/filesystem while another calls Storage directly. If the package stalls (last release 2022, single maintainer), swapping the adapter is a one-file change instead of a grep across the codebase. This is the single highest-leverage pattern for a 9-star dependency.
Disk selection. Decide up front whether you pass a disk name per call or rely on the default. Per-call is more verbose but survives multi-tenant or multi-bucket setups where FILESYSTEM_DISK varies by environment. If the wrapper only reads the default disk, you lose a capability you had with Storage::disk() — check before committing.
Keep business logic out of the wrapper. Use it at the edges: controllers, jobs, import/export services. Do not let helper calls leak into domain models. The abstraction is thinner than your requirements will be, and it will not survive a change of storage backend on its own.
Testing. Laravel's Storage::fake() only intercepts the Storage facade. If the package resolves its own Filesystem instance from the container, fakes will silently no-op and your tests will write to the real disk. Write one test asserting a fake disk received a file before you trust the suite. If fakes don't work, fall back to a real temp directory with storage_path/sys_get_temp_dir() in tearDown.
Jobs and queues. For anything user-uploaded or larger than a few MB, check whether the helper streams or buffers. A convenience read that returns a string will OOM on a 500 MB file. If the wrapper lacks streaming, drop to Storage::disk()->readStream() for those paths and use the package only for small metadata-style files.
Directory helpers. Path joining and directory listing are where wrappers most often diverge from native behaviour (trailing slashes, recursive flags, hidden files). Normalize paths at your boundary — ltrim($path, '/') — and write a test for folder/ vs folder before you rely on either.
Migration pattern. If you adopt it mid-project, replace Storage:: call sites file-by-file rather than in one sweep, and run the suite after each file. The changelog gives you no signal about behavioural changes between versions, so the only reliable regression check is your own tests.
composer why sfcod/filesystem and composer update --dry-run that the transitive dependency resolves, and note in the PR whether you actually exercise the affected path. Unverified is unverified.League\Flysystem\FileNotFoundException became UnableToReadFile. If the package type-hints or catches the old classes, your try/catch blocks will not fire. Grep the source for League\Flysystem imports before assuming compatibility.false. If yours does, code that checks if (!$result) will work but code that relies on exceptions will not — and a failed S3 write will look like a successful no-op. Wrap every call in a check and log the disk name plus path on failure.put/copy/delete return bool or void. Laravel's own API changed here historically, and wrappers copy whatever they were written against.local resolves relative to storage/app; S3 treats the key as a root-level object key. A leading slash that is harmless locally creates an empty-named folder on S3. Strip it centrally.public visibility is a filesystem permission; S3 visibility is an object ACL or a bucket policy. If the wrapper's helpers do not accept a visibility argument, you cannot serve public assets from S3 without a separate call.extra.laravel block, you will get a container binding error with no helpful message. Confirm the provider appears in php artisan about or bootstrap/cache/packages.php; if not, register it manually in config/app.php.publish tag exists, publish and commit the config file so environment-specific settings are reviewable in diffs. If none exists, do not assume the package reads your filesystems.disks array — check where it gets its disk config.Storage::disk('name')->getAdapter() and Storage::disk('name')->path('') are the fastest way to confirm which root a wrapper call actually hit. Add a temporary Log::debug() of the resolved disk and path inside your adapter the first time something lands in the wrong place.$this->app->bind(). Forking is a last resort but is legitimate here — at 9 stars and one maintainer, vendoring is the documented fallback if the package becomes load-bearing.Storage is already the right tool. The package earns its dependency only when you have multiple disks and genuinely repetitive boilerplate — and even then, only if the source review above comes back clean.How can I help you explore Laravel packages today?