czim/file-handling
Laravel package for structured file handling: store uploads, associate files with Eloquent models, manage storage locations and file variants, and simplify common file operations in PHP applications.
Install and publish.
composer require czim/file-handling
php artisan vendor:publish --provider="Czim\FileHandling\FileHandlingServiceProvider"
(If the provider tag differs in your version, php artisan vendor:publish and pick the file-handling tag interactively.) The service provider registers the storage manager and merges a config/file-handling.php file.
Where to look first. Open config/file-handling.php. The key section is strategies — named adapters (usually local, and optionally ftp/s3/custom) that map to a storage driver and a base path. Everything else in the package (creating, reading, deleting, storing uploads) resolves through these named strategies, so getting this config right is 80% of the work.
Also read the interfaces under Czim\FileHandling\Storage\Contracts (FileStorageInterface, StoredFileInterface) — that's the real API surface, and it's what you should type-hint and mock against in your own code.
First use case — read and expose a file. Resolve the storage manager, pick a strategy, and get a StoredFile value object:
use Czim\FileHandling\Storage\FileStorage;
$storage = app(FileStorage::class);
$stored = $storage->file('uploads/avatars/42.jpg'); // strategy is 'local' by default
if ($stored->exists()) {
$url = $stored->url();
$size = $stored->size();
}
The StoredFile object wraps path resolution, URL generation, MIME detection, and deletion — treat it as the primitive you hand around instead of raw paths.
Strategy-as-tenant. Treat each strategy entry as a named bucket/dimension (e.g. public-uploads, private-documents, tmp). Pass the strategy name explicitly at call sites rather than relying on the default; it makes later relocation (local → S3) a config change with zero code churn.
$stored = $storage->strategy('private-documents')->file($path);
Persist paths, not files. Store the strategy name plus relative path on your Eloquent model (storage_disk, storage_path), not the StoredFile object. Rehydrate on read:
public function documentUrl(): ?string
{
return $this->storage_path
? app(FileStorage::class)->strategy($this->storage_disk)->file($this->storage_path)->url()
: null;
}
Wrap this in a small HasStoredFile trait so every model gets consistent accessors and a deleteFile() hook on deleting.
Upload handling. Use the uploaded-file handler for request files — it centralizes moving the temp file into a strategy and (optionally) generating derivative names:
$stored = $handler->handle($request->file('avatar'), 'public-uploads/avatars');
$model->update(['storage_path' => $stored->path()]);
Keep the handler as a constructor-injected service, not a static call, so you can swap it in tests.
Thin controller, service for orchestration. Controllers should do validation only; put store/delete/replace logic in a domain-level FileService that owns strategy names and naming conventions (hashed names, date subfolders). That's the layer that will survive a storage backend change.
Testing pattern. Because the public surface is interface-based, bind FileStorageInterface to a fake or to a local strategy pointed at a storage/framework/testing path in your test case. Assert on the returned StoredFile (path/existence) rather than filesystem details.
src/Storage/ and src/UploadedFile/ is your real reference. Read the interfaces before writing against any concrete class — method names between strategies have historically drifted.path in the strategy silently writes to an unexpected directory. Dump config('file-handling') early and confirm the resolved absolute path.StoredFile objects are lightweight wrappers, not handles. Calling url() or size() hits the underlying driver; don't chain them in hot loops. Cache derived values (size, mime) on your model if you display them often.local, url() typically assumes a public symlink (storage:link). Private strategies should go through Storage::temporaryUrl() (or your own controller), not StoredFile::url() — otherwise you'll leak private paths.deleting hook; otherwise orphaned files accumulate and are painful to clean up later.composer.lock compatibility — old czim packages sometimes pin to specific Laravel/Lumen ranges. If you're on L11+, verify the installed tag actually supports it before building on it.$storage->strategy('x') when a "file not found" is actually a misconfigured strategy — the exception message doesn't always name the strategy.How can I help you explore Laravel packages today?