Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Documentable Laravel Package

madebyclowd/laravel-documentable

A Laravel package that adds document management to Eloquent models through a reusable documentable trait, letting you attach, store, and organize files with polymorphic relationships so any model can handle documents cleanly.

View on GitHub
Deep Wiki
Context7

Getting Started

madebyclowd/laravel-documentable attaches files to any Eloquent model through a polymorphic documents relationship, with first-class support for S3-style presigned (including multipart) uploads so large files never pass through your PHP workers.

Minimal path to a working feature:

  1. composer require madebyclowd/laravel-documentable
  2. Publish and run the migration, then publish the config: php artisan vendor:publish --tag="documentable-config" (also publish documentable-migrations if the tag differs in your version).
  3. Set the disk and temporary-URL behaviour in config/documentable.php — this is the first file to read, because disk, temporary_url and the multipart part-size/expiry settings drive almost every behaviour you'll debug later.
  4. Add the trait to the model that owns files:
    class Invoice extends Model
    {
        use \Madebyclowd\Documentable\Traits\HasDocuments;
    }
    
  5. Attach and read back:
    $invoice->documents()->create([
        'path' => $request->file('pdf')->store('invoices', config('documentable.disk')),
        // plus whatever metadata columns your published migration defines
    ]);
    
    $url = $invoice->documents()->first()->temporaryUrl(now()->addMinutes(10));
    

First use case to build: a plain server-side upload (file posted to your controller, stored on the configured disk) rendered through a temporary URL. Get that end-to-end before touching presigned or multipart uploads — it isolates config problems from browser/CORS problems.

Implementation Patterns

Two upload topologies — pick one per feature, not per app. Server-side upload (Laravel receives the bytes, store()s them, then persists a Document) is the right default for small files and admin tooling. Presigned/multipart upload is for large files or direct-to-bucket flows: your endpoint mints URLs, the client PUTs parts, and you persist the document only after the upload completes. Mixing both in one controller is a common source of "file exists but no document row" bugs.

Persist after completion, not before. For presigned flows, treat the document row as the completion record. Create it in the callback/finalise step so a failed or abandoned browser upload never leaves a dangling row pointing at a half-written object.

Use the polymorphic relation, don't denormalise. $model->documents() gives you ordering, deletion cascade hooks and consistent URL signing. If you find yourself storing document_url on the parent, you're caching a signature that will expire and silently break the page.

Collections/categories. Where the package exposes a category or collection column, use it for grouping ("contracts", "receipts") rather than separate tables — you keep one migration, one trait, one cleanup path.

Always sign at render time. Resolve temporaryUrl() in the controller or a resource class, close to the response. Signing in a model accessor means URLs get baked into serialized payloads, cache entries and queued jobs, and expire before they're used.

Eager load. Invoice::with('documents')->paginate() — a document list view without eager loading is an N+1 on every row.

Deletion discipline. Wire a model observer or deleting hook that removes storage objects alongside rows, or run a scheduled reconciliation command. The package manages rows; it can't know about your retention policy.

Testing. Use Storage::fake(config('documentable.disk')) and assert on assertExists. For presigned flows, assert on the URL generation response shape rather than trying to perform real multipart uploads against a fake disk — that path needs an integration test against a real bucket or a LocalStack container.

Gotchas and Tips

Upgrade to 2.1.2 if you use multipart with temporary_url. This release fixes a bug where part-upload presigned URLs ignored the temporary_url configuration, meaning part URLs were generated with the wrong signing mode/expiry relative to the rest of the package. Symptom on ≤2.1.1: single-part uploads work but multi-part presigned uploads fail with signature or expiry errors on the individual parts, or parts expire mid-upload on long transfers. If you set temporary_url to anything non-default, treat this as a required patch — there's no config workaround.

temporary_url must match your actual bucket access model. Private bucket → temporary_url => true. Public bucket or CDN in front → false. Getting this wrong produces either 403s on every download or URLs that never expire (a quiet security problem rather than a loud one).

Part size vs. URL expiry. Presigned multipart parts have their own expiry independent of the final object URL. A 5 GB upload on a slow connection can outlive a 15-minute part window. Check the part-size/expiry keys in the config and size them to your worst-case upload duration, not your average.

CORS is outside the package. Direct-to-bucket uploads need PUT (and POST for multipart initiation) allowed on the bucket with your app origin. When multipart fails in the browser but works in Postman, it's CORS 100% of the time.

Disk name vs. disk driver. The config takes a filesystem disk name, resolved through config/filesystems.php. Changing the disk in documentable.php without the corresponding entry in filesystems.php fails at runtime, not at boot.

Don't cache signed URLs. Any response cache, ETag or CDN layer in front of a download endpoint will serve expired signatures. If you need cacheable downloads, route through a controller that re-signs or streams.

Extension points to reach for before forking: the published config for disk/expiry/part sizing, the published migration for adding your own metadata columns (checksum, mime, uploader), and a custom model binding if you publish and extend the Document class. Most "the package doesn't do X" issues are a column or a config key away.

Debugging order for a failed upload: (1) does Storage::disk(config('documentable.disk'))->put() work from tinker? (2) is temporary_url correct for the bucket's ACL? (3) does a single-part presigned PUT succeed? (4) only then chase multipart. That sequence separates credentials, signing config and CORS problems cleanly.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console