dashed/dashed-files
Dashed Files is a Laravel package for managing files with a clean, expressive API. It provides helpers for storing, retrieving, and organizing files, integrating smoothly with Laravel's filesystem and storage drivers for simple, reliable file handling in your applications.
Because the package is early-stage with no dependents, treat the repository itself as the documentation. Read these three things before installing:
composer.json — confirm the Laravel/PHP version constraints and whether it pulls in intervention/image, league/flysystem-aws-s3-v3, or a queue driver.src/DashedFilesServiceProvider.php — lists every publishable tag, config key, migration, and binding. This is the real API surface.database/migrations/ — tells you the schema (single files table vs. per-model tables, polymorphic columns, ordering columns).Minimal install:
composer require dashed/dashed-files
php artisan vendor:publish --tag=dashed-files-config
php artisan vendor:publish --tag=dashed-files-migrations
php artisan migrate
php artisan storage:link # only if using the public disk
Then set your disk and base path in config/dashed-files.php, and make sure the matching disk exists in config/filesystems.php:
// config/dashed-files.php
'disk' => env('DASHED_FILES_DISK', 'public'),
'path' => 'uploads',
First use case: attach an upload to an existing Eloquent model. Add the package trait to the model, then accept the file from a controller or Livewire component and hand it to the package's upload entry point rather than calling Storage::putFile() yourself — going through the package is what creates the DB record, generates the path, and wires up conversions.
Verify the exact trait name and upload method signature from the source before writing code; the README is likely thinner than the implementation.
Trait-based attachment. The dominant pattern in Laravel file packages is a HasFiles-style trait on the owning model, giving you a morph relation plus a helper for the "primary" file. Expect to write:
class Post extends Model
{
use HasFiles;
}
$post->attachFile($request->file('cover'), collection: 'cover');
$post->file('cover')?->url();
Substitute the package's real method names. The important habit is to always go through the relation/helper so path generation and cleanup stay consistent.
Collections / groups. Most implementations support a named group (cover, gallery, attachments) so a model can hold several independent file sets. Use collections liberally instead of adding nullable columns like cover_path — the latter defeats the point of the package and will drift out of sync with deletions.
Single vs. multiple. For a "one image" field, look for a singleFile/replace variant. If it doesn't exist, enforce it yourself in a form request or an observer, because replacing means deleting the old record and the old blob — two steps that are easy to get half-right.
Disk strategy. Keep private documents on a private disk and public imagery on public/s3-public. If the package exposes a per-collection disk override in config, use it rather than branching in your controllers. Generate URLs through the package (->url(), ->temporaryUrl()), never Storage::url($file->path) — the package may add a prefix or signed-URL logic.
Upload pipeline. Typical flow: validated UploadedFile → package stores blob → package writes DB row → optional queued conversion → model relation. Keep the controller thin:
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
$post->attachFile($request->file('cover'), 'cover');
return redirect()->route('posts.edit', $post);
}
Conversions / thumbnails. If conversions exist, they should be queue-driven (ShouldQueue) and idempotent. Render a placeholder in the UI while the conversion is pending rather than blocking the request on image processing.
Cleanup and lifecycle. Wire the package's pruning command into the scheduler and register model event hooks (or rely on the package's own deleted listener) so deleting a Post removes its files and blobs. Test that explicitly — orphaned blobs are the classic failure mode of these packages.
API responses. Wrap file output in a small API resource so the shape is stable if the package's toArray() changes:
return [
'url' => $file->url(),
'thumb' => $file->url('thumb'),
'name' => $file->original_name,
'size' => $file->size,
];
Testing. Use Storage::fake('public') in the test suite and assert both the DB row and Storage::disk('public')->assertExists($file->path). If the package resolves its disk at container-boot time, a fake registered in setUp() may not take effect — check the service provider for a deferred binding or config() read.
Zero dependents means zero battle-testing. Nothing in the ecosystem has stress-tested this yet. Before committing, read the migration and model source end to end, run the package's own tests if any exist, and pin the version in composer.json. Budget time for reading source when something misbehaves — there will be no Stack Overflow answers.
The "last release 2026-04-08" date is a red flag worth checking. Confirm on Packagist that the tag exists and matches the branch you're reading. A future-dated or unreleased tag often means the repo's default branch has diverged from what Composer will actually install.
Config caching. php artisan config:cache will freeze your disk and path settings. If uploads suddenly land in the wrong place after a deploy, clear the cache first — this burns nearly everyone at least once.
Disk visibility mismatch. A file written with 'visibility' => 'private' on S3 cannot be served by Storage::url(). If images 403 in production but work locally, it's almost always visibility or a missing storage:link, not the package.
Path collisions and overwrites. If the package derives filenames from the original name, two photo.jpg uploads may collide or silently replace each other. Check whether it appends a UUID/hash. If not, supply a custom path generator (usually a config callable or an interface binding) and use Str::uuid().
Validation is your job. Packages typically store whatever they're given. Enforce mimes: and max: in a FormRequest, and prefer MIME sniffing over extension checks. Don't trust getClientOriginalExtension().
Deletion is not automatic unless proven otherwise. Verify with a test: create a model with a file, delete the model, assert the row and the blob are gone. If the package doesn't cascade, add an observer and a scheduled files:prune for the leftovers.
Queue workers need the same config. If conversions are queued, the worker must see identical filesystems.php and dashed-files.php values, plus credentials for the S3 disk. A conversion that works in sync but fails on the worker is a config-parity problem, not a code problem.
Extension points to look for (in rough order of usefulness):
model_type as a FQCN, register a morph map early so refactors don't orphan records.Debugging checklist when an upload "works" but produces nothing:
files row actually written? (->files()->count())Storage::disk(config('dashed-files.disk'))->exists($file->path) return true?config:cache from an earlier deploy win?Escape hatch. Because the package is small and unproven, keep your own calls behind a thin service or repository (FileService::store()) rather than scattering package method calls across controllers. If you later need to swap to Spatie Media Library or plain Flysystem, you change one class instead of forty.
How can I help you explore Laravel packages today?