laravel-mediable attaches tagged media to Eloquent models without touching their schema
Laravel-Mediable is a package for easily uploading and attaching media files to models with Laravel
At a glance
- What is it?
- laravel-mediable is a Laravel package that uploads files to a named disk, stores them as Media records and attaches them to any Eloquent model through tagged, many-to-many polymorphic relationships. The benefit is that no model needs a media column, the cost is a join table and a migration you do not write yourself.
- Who is it for?
- laravel-mediable fits a Laravel application that needs to attach images and files to several unrelated models, with tags so the same attachment can serve as a thumbnail, a featured image, a gallery entry or a download, and it fits teams that would rather not add media columns to every table.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Media records live apart from your models
laravel-mediable is a package for easily uploading and attaching media files to models with Laravel, and the design decision that shapes everything is that the media does not live in the model's table.
The flow in the README is three steps. You upload a file, which creates a Media record you can refer to.
$media = MediaUploader::fromSource($request->file('thumb'))
->toDestination('uploads', 'blog/thumbnails')
->upload();You attach that Media to another Eloquent model with one or more tags that define the relationship.
$post = Post::create($this->request->input());
$post->attachMedia($media, ['thumbnail']);And you retrieve it later by tag.
$post->getMedia('thumbnail')->first()->getUrl();The feature list states the mechanism directly: many-to-many polymorphic relationships allow any number of media to be assigned to any number of other models without any need to modify their schema.
That is the whole reason to use this package instead of adding a media_id column. A blog post, a product, a user profile and a comment can all hold attachments, and none of those tables gets a column, a morph type pair or a nullable foreign key added to it. The package ships its own migrations, which you run with php artisan migrate, and those migrations create the tables the package needs.
The repository layout backs this up. There is a src directory, a migrations directory, a config directory, a tests directory, docs, and configuration for static analysis with phpstan.neon and PHPUnit via phpunit.xml. There is also infection.json.dist, which is the mutation testing tool, so the test suite is being held to a stricter standard than just coverage.
Tags are how one model plays several roles with the same file
The tagging mechanism is what stops the polymorphic relation from becoming a dumping ground.
The README describes attaching media with tags so you can set and retrieve media for specific purposes, such as 'thumbnail', 'featured image', 'gallery' or 'download'. The attachMedia call takes an array of tags, and getMedia takes a tag and returns the matching collection.
That gives a model several independent slots for media without several tables or several columns. A post can have one image tagged as thumbnail, another tagged as featured image, and a set tagged as gallery, and the code reads them by name.
It also means a single Media record can be attached to several models at once, each with its own tag. If the same uploaded file is used as a site logo in the settings table and as an avatar in the users table, the file exists once on disk and the Media row exists once, with two attachment rows pointing at it.
That reuse is where the package earns its efficiency, and it is also where the awkwardness lives. Because the physical file and the Media row are shared, deleting an attachment cannot mean deleting the file, and the README does not document a cleanup strategy for orphaned Media records or for files that no attachment references. If you add delete hooks to your models, you need to decide whether a delete removes the file, removes the Media row, or only removes the attachment row and leaves the file in place for the other model that shares it.
That question is not answered in the documentation, and it is the first thing to settle before you attach one Media record to several models.
The filesystem, not the database, decides where files live
The upload destination is expressed as a disk name and a path, and the first feature in the list describes a filesystem-driven approach that is easily configurable to allow any number of upload directories with different accessibility.
The call in the example names the disk uploads and the path blog/thumbnails, so each logical group of media can live in its own directory on a disk you configure in Laravel's filesystem settings. A disk can be local, or it can be a remote object store, and the package treats the disk abstraction as the boundary between your application and wherever the bytes land.
The second half of that first feature is restriction. The package can easily restrict uploads by MIME type, by extension, and by aggregate type, where the README gives the example of image for JPEG, PNG or GIF. The aggregate type is the useful one for a user facing upload form, since it lets you accept a family of formats without writing a list of extensions and then trusting the browser's content type.
These restrictions live in the config file. The package asks you to publish config/mediable.php.
php artisan vendor:publish --provider="Plank\Mediable\MediableServiceProvider"and then the uploaded files, the allowed types and the directories are configured there rather than in code. The README points to the documentation site at laravel-mediable.readthedocs.io for the details of those configuration keys.
The upload directory structure follows the path you pass to toDestination, so blog/thumbnails in the example produces a directory tree under the uploads disk. That is a useful default because it keeps one kind of asset out of another kind's folder, but it means the directory layout is decided at the call site, and a codebase that uploads from many places will produce many directories unless you agree on a convention.
Image variants through integration/image, not through the media layer
The fourth feature is Integrated support for integration/image for manipulating image files to create variants for different use cases.
Read carefully, this says the package integrates with the intervention/image library to produce resized or otherwise manipulated variants. It does not say that laravel-mediable itself resizes images. The distinction matters, because it means your composer.json needs intervention/image alongside plank/laravel-mediable, and because the variant configuration lives in intervention/image's own configuration rather than in config/mediable.php.
This is a common pattern in Laravel packages: the package handles the attachment model and the file placement, and it delegates the image processing to a library that already solves that problem. The benefit is that you keep intervention/image's API and its driver set instead of learning a second image pipeline.
The practical consequence for your application design is that a thumbnail variant is a separate piece of work from a thumbnail tag. The tag tells you which Media record is the thumbnail; intervention/image tells you how the bytes for that record are produced. If you attach the same uploaded file under two different tags and want different dimensions for each, you need to think about where the variants are stored and how they are addressed, and the README does not describe that in detail.
The facade is registered as MediaUploader. In Laravel 5.5 and beyond you can skip the manual registration if package auto-discovery is enabled.
'aliases' => [
...
'MediaUploader' => Plank\Mediable\MediaUploaderFacade::class,
...
]Installation on Laravel 5.5 and beyond is three commands
The install path is short, and the README spells out the version nuance: register the service provider in config/app.php, but in Laravel versions 5.5 and beyond this step can be skipped if package auto-discovery is enabled. The same applies to the facade alias.
The provider registration for older setups looks like this.
'providers' => [
...
Plank\Mediable\MediableServiceProvider::class,
...
];The whole installation then reduces to the composer require, the vendor:publish to get the config file, and the migrate to create the tables.
composer require plank/laravel-mediablephp artisan migrateThat is worth pausing on, because it is where the package's schema commitment lands. Running php artisan migrate adds the required tables to your database, and those tables then exist for the life of your application. The migration files live in the package's migrations directory, so you can read them before running, and the UPGRADING.md file at the repository root is where the upgrade notes for existing installs will be.
The repository also carries a CHANGELOG.md and a CONTRIBUTING.md, and the CI configuration is visible in the .github directory. The releases are frequent: 7.1.1 on 2026-10-04, 7.1.2 on 2026-10-09 and 7.1.3 on 2026-10-12, with the last push to the repository on 2026-10-12. The major version has moved past 7, which tells you the package has been through a lot of schema iterations and that you should read UPGRADING.md before bumping versions in an existing application.
The licence is MIT, and the project comes from Plank, a web development agency based in Montreal.
Where laravel-mediable is the wrong tool
The first limitation is versioning. Nothing in the documented API suggests that a Media record can hold multiple versions of a file with a history. The relationship model is attach and retrieve by tag. If your domain needs a document that was revised, where v3 is the current one and v2 is still downloadable, this package does not model that and you would be building a parallel structure beside it.
The second is the shared file question raised earlier. A Media record and its file can be referenced by several models, which is efficient, but it means attachment deletion, file deletion and reference counting all become your problem. The README documents none of that lifecycle, and an application that lets users delete attachments will need to answer those questions itself.
The third is that the package is Laravel and Eloquent specific, and the topics confirm the scope: eloquent, file-upload, laravel, hacktoberfest. There is nothing framework agnostic here, and nothing for a Symfony or Slim application.
The fourth is a plain engineering one. The package stores media in a database table and the bytes on a filesystem disk. That is a fine design for a monolithic Laravel application. In a setup where the application is deployed to many instances behind a load balancer with a shared object store, you have to ensure the disk is genuinely shared and that the Media rows are in a shared database, otherwise an attachment created on one instance is a dangling reference on another. That is a deployment concern rather than a package defect, but it is the thing most likely to surprise you in production.
How it compares with storing a media id on the model
The alternative is the arrangement Laravel gives you for free: add a nullable media_id or morphs columns to each table that needs attachments, and resolve them yourself.
That approach is simpler in one dimension and more complex in another. It is simpler because there is one attachment per row, no tag, no join table and no separate Media model, and the deletion question never arises because the row owns the file. It is more complex because every table that needs media needs its own migration, and the second, third and fourth such table is where a package starts to look attractive.
laravel-mediable trades that for a shared vocabulary. One Media class, one attachment mechanism, one set of tags that mean the same thing everywhere in the application, and any model can hold any number of attachments. The cost is the join table and the shared-file semantics.
There is also the pragmatic middle ground of using the package for the many-to-many cases and a plain column for the simple ones, though mixing the two conventions in one application tends to produce exactly the confusion the package is meant to remove.
The honest summary is that laravel-mediable is aimed at applications where media is a first-class, plural, tagged concept attached to many kinds of content. If your application has one image per blog post and nothing else, the column approach is less code and fewer tables.
Editorial conclusion
laravel-mediable fits a Laravel application that needs to attach images and files to several unrelated models, with tags so the same attachment can serve as a thumbnail, a featured image, a gallery entry or a download, and it fits teams that would rather not add media columns to every table. It does not fit an application that needs media versioned immutably, since the package attaches and reattaches rather than versioning, and it does not fit a project where a single asset must be linked to many models with per-model metadata. Before adopting, run php artisan migrate and check the tables the package creates, publish config/mediable.php and confirm your disk names and upload directories match your infrastructure, and decide how you will handle the fact that the same physical file can be referenced by many records, which is exactly what makes both the reuse and the deletion story awkward.
Frequently asked questions
How do I install laravel-mediable in a Laravel application?
Run composer require plank/laravel-mediable, publish the config with php artisan vendor:publish --provider="Plank\Mediable\MediableServiceProvider", and run php artisan migrate to create the required tables. On Laravel 5.5 and later the service provider and the MediaUploader facade can be skipped if package auto-discovery is enabled.
How do I attach an uploaded file to a model with laravel-mediable?
Upload the file with MediaUploader::fromSource($request->file('thumb'))->toDestination('uploads', 'blog/thumbnails')->upload(), then call attachMedia on the model with an array of tags, for example $post->attachMedia($media, ['thumbnail']). You retrieve it afterwards with $post->getMedia('thumbnail').
Do I need to add a media column to my Eloquent models?
No. The package uses many-to-many polymorphic relationships so any number of media can be assigned to any number of other models without modifying their schema. The package's own migrations create the tables it needs when you run php artisan migrate.
What can laravel-mediable restrict uploads by?
Uploads can be restricted by MIME type, by extension, or by aggregate type such as image for JPEG, PNG or GIF. The settings live in the published config/mediable.php file, and the upload destination is expressed as a disk name plus a path.
Does laravel-mediable resize images itself?
It integrates with the integration/image library for manipulating image files to create variants for different use cases, so you need that library alongside the package. The variant configuration belongs to that library rather than to config/mediable.php.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/plank-laravel-mediable)Was this useful?