plank/laravel-mediable
Laravel-Mediable lets you easily upload files and attach them to Eloquent models via many-to-many polymorphic relationships, with tags for organizing media, configurable filesystem destinations, MIME/extension restrictions, and image variant support.
Architecture fit. plank/laravel-mediable is a conventional Laravel package (service provider, facade, publishable config, package-loaded migrations) and slots into the framework the same way first-party components do. Its data model is the main architectural decision it forces on you: a media table plus a mediables polymorphic many-to-many pivot carrying a tag column. That means no schema changes to your domain models, at the cost of introducing a generic attachment table that becomes a shared dependency across the app. For a product with heterogeneous attachable entities (posts, users, products, CMS pages) this is a good trade. For a system with one or two media-bearing models and strict per-domain storage rules, the abstraction may be more generality than the product needs.
The package is filesystem-driven on Laravel's Flysystem disks, with pluggable source adapters (uploaded file, remote URL, stream, raw content, data URL as of 6.0) and optional image manipulation via intervention/image. That is a clean separation: storage concerns live in config/filesystems.php, upload semantics in MediaUploader, and relationships in the model trait. Variant generation is expressed as queued jobs — correct for the problem, but it pulls your queue infrastructure into the feature's critical path.
Integration feasibility. Low-friction. composer require, auto-discovery on Laravel 5.5+, one vendor:publish, one migrate. The 6.0 line requires PHP 8.1+ (dropped 7.4/8.0) and Laravel 10/11 (dropped 8/9), and adds intervention/image 3.x support via intervention/image-laravel. A code spike should be a day's work, not a sprint.
Technical risk.
SourceAdapter, removed getStreamResource(), getContents(), path(), valid(), and renamed ImageManipulation::usingHashForFilename()). Budget upgrade work per Laravel major — this is not a fire-and-forget dependency.Media row leaves an orphan file; a deleted model does not delete files unless you explicitly do so. Orphans are the primary long-term cost driver.UPGRADING.md before jumping, since migrations are now loaded from the package (a 5.0 change) rather than copied into database/migrations.Key questions.
media table serve tenant-isolated buckets, or does isolation require a custom Media subclass and table (a path the package supports but that has historically been bug-prone — see 4.4.2, 5.0.6)?validateHash() and useHashForFilename() are in-box; the rest is ours to build.alt text — is it authored per-locale, and does that belong in media or in our domain tables?Stack fit. Strong fit for a standard Laravel app on PHP 8.1+ / Laravel 10–11, Flysystem 3 disks, and a queue worker pool. It composes with intervention/image 3.x and spatie/image-optimizer for derivative and optimization work. It does not fit apps that need per-domain attachment tables, hard real-time variant generation, or non-Laravel services that must read media metadata directly.
Migration path. For a greenfield feature, the path is: install and pin the 6.x minor, publish config/mediable.php, define upload disks with explicit visibility, run the package migrations, and wrap the facade behind an internal MediaService. For brownfield, the harder question is backfill: existing files need Media rows plus mediables pivot entries, and existing URLs need to be rewritten or redirected. Scope that as its own workstream — it is typically larger than the package integration itself.
Compatibility. Verify before committing: PHP ≥8.1, Laravel 10/11, intervention/image v3 pairing, and Flysystem minor constraints (historically a source of breakage — see 4.3.2 and 5.4.1). Because migrations ship inside the package, confirm no conflicts with a squashed migration history (5.0.0's stated reason for the change) and check that the app's doctrine/inflector version is compatible. Confirm S3 temp-URL generation respects disk root configuration on Flysystem 3 — that was patched in 5.4.1 but is exactly the kind of thing to assert in a smoke test.
Sequencing.
thumbnail, featured, gallery, download) before any model uses it. Tags are a de-facto schema; ad-hoc tags are the most common way this package becomes unmaintainable.MediaUploader and getMedia() calls spread through controllers. One service class owns upload policy (allowed MIME/extension/aggregate types, destination, visibility, alt text, hash policy), one owns retrieval and URL generation.CreateImageVariants job leaves a media record with no variants and no user-visible error unless we surface it.Maintenance. Expect periodic major-version work tied to Laravel upgrades rather than steady-state patching. The changelog is disciplined but the cadence is reactive to framework releases, so a Laravel upgrade plan must include a mediables line item. Because migrations live in the package now, upgrades can move the schema underneath us — every upgrade needs a migrate --pretend review. Owning a thin internal wrapper is the cheapest insurance against needing a fork.
Support. Community-only: GitHub issues, readthedocs, an UPGRADING.md. No SLA, no vendor escalation. For a media pipeline that is user-facing and revenue-adjacent, that argues for keeping the integration surface small enough that we can patch it ourselves, and for treating "fork and pin" as a legitimate, budgeted option rather than a failure mode.
Scaling. Storage scales trivially (S3 + CDN); the database does not. Watch for:
getMedia() inside loops — eager-load the polymorphic relation.(mediable_type, mediable_id, tag); 4.3.2 removed a redundant index, so verify our query plans against real cardinality.Failure modes.
Media row persists; URLs 404 silently.Ramp-up. Low for anyone fluent in Laravel: half a day to productive, two to three days to confident with variants and S3. The real ramp-up cost is organizational, not technical — establishing the tag taxonomy, the upload policy, and the "no direct facade calls in controllers" convention. Without those, the package's flexibility becomes a source of inconsistency within a quarter. Budget roughly a sprint for integration plus a sprint for backfill and hardening on a mid-sized app.
How can I help you explore Laravel packages today?