laravel-enso/documents
Laravel Enso Documents provides document management for Enso applications, letting you upload, store, organize, and attach files to models, with template support, permissions, and ready-made UI integration.
laravel-enso/documents is a module in the Laravel Enso ecosystem, so it assumes Enso's core scaffolding (laravel-enso/enso, laravel-enso/helpers, laravel-enso/forms, laravel-enso/tables) is already in place. It is not a standalone upload package — its UI, permissions and routes are wired through Enso's Vue/Vuetify admin shell. If you don't have an Enso app, this package will fight you; use spatie/laravel-medialibrary instead.
Assuming an Enso app:
composer require laravel-enso/documentsdocuments config for storage disk and allowed types): php artisan vendor:publish --tag=documents then php artisan migrateuse LaravelEnso\Documents\Traits\HasDocuments;
class User extends Model { use HasDocuments; }
$user->documents()->create(
Document::attributesFrom($request->file('document'))
);
Or route the file through the package's own upload endpoint so it handles validation, disk naming and metadata in one place.The first place to look in the repo is src/Models/Document.php (the shape of the record: name, original_name, mime, size, type, documentable_type/id), then src/Traits/HasDocuments.php, then the Vue components under resources/js if you're customising the front end.
Polymorphic attachment. The documentable morph means one documents table serves every entity in your app. $company->documents, $user->documents, $invoice->documents all work without extra tables. Reach for the trait rather than hand-rolling join tables.
Typed documents. Enso uses a DocumentType enum on the record so uploads can be grouped (e.g. contract vs. identity vs. attachment). Pass the type on creation so listing/filtering in the front end works — the built-in list component filters by it.
FormBuilder integration. The package's Vue uploader slots directly into Enso's FormBuilder:
->file('document', 'contract')
This is the idiomatic day-to-day path — you rarely touch the upload controller yourself.
Download through a policy, not a raw URL. Documents live on the private disk. Serve downloads via the package's download endpoint (or your own StreamedResponse) so Enso's ACL runs. Never expose storage/app/documents as a public URL — that bypasses authorisation.
Cleanup on deletion. When a parent model is deleted, cascade its documents. Either add ->onDelete('cascade') to the morph column in your migration or hook a model observer to $model->documents()->each(fn($d) => $d->delete()) so the physical file is removed alongside the row.
Custom subtypes. Extend Document and override protected $fillable / add a scopeForType when you need domain logic (e.g. "latest signed contract"). Keep the base package untouched — Enso expects the parent class to remain canonical.
Event-driven post-processing. Listen for the package's DocumentCreated / DocumentDeleted events to hook OCR, virus scan, thumbnail generation, or Elastic sync without touching the upload flow.
local/private disk under storage/app/documents. If you're on S3 in production, set the disk in config/documents.php and make sure the Enso policy lets the download controller generate a signed URL for the right disk — mismatches produce silent 404s.upload_max_filesize and post_max_size in php.ini (and Nginx client_max_body_size) will kill large uploads before Validator runs. Symptom is a 413 or an empty $_FILES, not a validation error..heic) requires updating config/documents.php and the front-end accept attribute if you use the Vue uploader.roles / permissions tables, not just Gate definitions. Missing a documents.* permission silently hides the UI.original_name is what users see in the list. If you build your own listing, always surface original_name, and be aware two users can upload files with identical original names.Storage::fake(). The package writes real files via the configured disk; without faking storage, test runs litter storage/app. Faking also catches disk-config mistakes early.documentable_type strings, Enso v4 moved to enums and Vue 3. Mixing package versions with an older Enso core will break routes and Vue components — pin in composer.json and test upgrades.try/catch that deletes the file on exception, or schedule a periodic orphan sweep comparing storage/app/documents against the documents table.storage/logs/enso.log if present). If the Vue uploader appears but silently fails, check the browser network tab for the /api/documents response — Enso returns 422 with a message key rather than throwing.Document::observe(...) and the package events are the safest hooks. Avoid overriding Document::create() unless you truly need to — you'll fight future upgrades.How can I help you explore Laravel packages today?