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

Media Laravel Package

brackets/media

Brackets Media adds flexible file and image management to Laravel models. Attach uploads to any disk, generate resized conversions and thumbnails, and fetch URLs through a fluent API. Configurable storage, image manipulation and Blade helpers keep media handling simple across your app.

View on GitHub
Deep Wiki
Context7

Getting Started

brackets/media gives you a polymorphic media table plus a trait that lets any Eloquent model own uploaded files. The mental model is: a Media record is a pointer to a file on a disk, attached to a model via a mediable morph relation.

  1. Install and publish:
composer require brackets/media
php artisan vendor:publish --provider="Brackets\Media\MediaServiceProvider"
php artisan migrate

You should now have a media table (columns typically include model_type, model_id, disk, file_name, file_path, mime_type, size, and collection/tag fields). Read the published migration first — column names are the ground truth for everything below. As of v7.1.0 the published migration is an anonymous-class migration (return new class extends Migration), which is what modern Laravel expects; if you're upgrading inside an app that published the old class-based migration, do not blindly re-publish over it.

  1. Add the trait to a model:
use Brackets\Media\HasMedia\HasMediaTrait;

class Article extends Model
{
    use HasMediaTrait;
}
  1. First use case — attach an uploaded cover image and render it:
$article = Article::find(1);

$media = $article->addMedia($request->file('cover'))
    ->toMediaCollection('cover'); // or ->toDisk('public')

echo $media->getUrl();

If those exact method names don't exist in your installed version, open src/HasMedia/HasMediaTrait.php and src/Models/Media.php — the trait is short and is the real documentation for a package this size.

  1. Check the new max-file-size setting. v7.1.0 added a basic max file size setting. Open config/media.php after publishing and look for it — it is a package-level ceiling, not a substitute for per-endpoint validation (see Gotchas).

  2. Where to look first (in order): the published migration → the Media model → the HasMedia trait → the config file (config/media.php, now including the max file size value).

Implementation Patterns

Uploading from a controller — keep it thin. Do validation and storage in the controller, keep the model out of HTTP concerns:

$request->validate(['cover' => 'required|image|max:4096']);

$article->addMedia($request->file('cover'))->toMediaCollection('cover');

return back();

Retrieving and rendering. Always eager-load; the relation is polymorphic and will N+1 otherwise:

$articles = Article::with('media')->paginate(20);
@if ($cover = $article->media->firstWhere('collection_name', 'cover'))
    <img src="{{ $cover->getUrl() }}" alt="">
@endif

API responses. Don't serialize the whole model with ->media blindly — the morph columns and absolute paths leak. Map explicitly:

'cover' => $article->media
    ->firstWhere('collection_name', 'cover')
    ?->getUrl(),

Replacing a file. The safe pattern is delete-then-attach, wrapped so a failed upload doesn't orphan the old file:

DB::transaction(function () use ($article, $request) {
    $article->media()->where('collection_name', 'cover')->get()->each->delete();
    $article->addMedia($request->file('cover'))->toMediaCollection('cover');
});

Attaching files you already have on disk (imports, seeds, jobs) is the pattern most teams miss — you're not limited to UploadedFile:

$article->addMedia(storage_path('imports/hero.jpg'))
    ->preservingOriginal()
    ->toMediaCollection('cover');

Attaching in a queued job. Pass IDs and re-fetch; never serialize UploadedFile or models with media state:

ProcessCover::dispatch($article->id, $tempPath);

Integration tip — treat collections as your bucket key. Pick a small, fixed vocabulary ('cover', 'gallery', 'attachment') and guard it with an enum or constant, so queries stay sane and nothing does firstWhere against a typo string.

Form/Blade integration. For multi-file galleries, iterate the relation rather than a fixed key:

@foreach ($article->media->where('collection_name', 'gallery') as $image)
    <img src="{{ $image->getUrl() }}" alt="">
@endforeach

Sizing your upload rules against the config ceiling. Define one app-level constant sourced from config('media.max_file_size') (verify the key in your published config) and reuse it in both FormRequest rules and any JS-side hint, so the package ceiling and your validation never disagree.

Gotchas and Tips

The package is thin and low-adoption — still audit before committing. It remains a small dependency with few dependents, but v7.1.0 shows renewed maintenance: three merged PRs, two new contributors, and a real feature. That is a good sign, not a guarantee. Practical implications: (a) Laravel version drift — this release explicitly adds Laravel 10 support, so 10 is now the tested ceiling; if you're on Laravel 11/12, check the composer.json constraints before installing, because nothing in these notes claims 11+ coverage; (b) no community answers — there are still no Stack Overflow threads or blog posts to fall back on. Budget time to read src/ rather than searching.

The new max-file-size setting is a floor, not your validation. It appears to be a single "basic" setting added in PR #18. Keep your per-endpoint max: rules — different collections legitimately need different limits (a 200 KB avatar vs. a 20 MB attachment), and you want a proper 422 with a field-specific message rather than whatever the package does on rejection. Treat the config value as a hard safety net against oversized uploads, and the FormRequest as the user-facing rule.

Weigh it against spatie/laravel-medialibrary. That is the de facto standard and covers conversions, responsive images, and queue-based regeneration that this package does not. Choose brackets/media only if you specifically want a minimal polymorphic file table with no conversions pipeline — or if it's already in the codebase (e.g. pulled in by Craftable). Note the naming collision risk: the migration PR references "medialibrary" terminology, so grepping your vendor/ and config/ for "media" will turn up both packages if you ever install the other one.

File deletion may not be automatic. Many small media packages delete the DB row on model delete but leave the file on disk. Check whether the trait hooks deleting; if unsure, add your own:

protected static function booted()
{
    static::deleting(fn ($model) => $model->media->each->delete());
}

Verify Media::delete() actually unlinks the file — if it only removes the row, you'll accumulate orphans in storage/app.

Disk and URL mismatch is the #1 "why is my image 404" bug. The disk saved on the media row must be the same disk getUrl() resolves against. If you store on public but your config defaults to local, URLs point outside storage/app/public. Check config/filesystems.php and that php artisan storage:link has been run.

Morph map hygiene. If you use Relation::enforceMorphMap(), the package will store morph aliases in model_type. Seeders and manual inserts that write raw class names will produce records the relation can't resolve. Stick to aliases everywhere, or drop the morph map.

Polymorphic N+1 sneaks past with(). Eager-load media on every collection that renders files, including nested resources (Article::with('media', 'author.media')). Use DB::listen or Telescope once during development to confirm the query count.

Debugging checklist when an attach silently no-ops: (1) does the model actually use HasMediaTrait? (2) is the file a real UploadedFile/path that exists? (3) does the target disk exist in filesystems.php? (4) does the media table have a non-nullable column your attach isn't filling — check the migration for defaults? (5) is the file over the new max-file-size config value, and does the package fail silently when it is? The package likely doesn't throw descriptive exceptions, so dd() the Media row before save.

Extension points are your safety valve. Avoid hardcoding Brackets\Media\Models\Media in your code — type-hint your own subclass or the relation instead. Same for the trait: if you need behaviour changes, wrap it in your own trait that uses theirs, so upgrades don't clobber you.

Pin the version and vendor-lock your expectations. Pin to a known-good tag ("brackets/media": "7.1.0" or ^7.1) rather than floating, and be aware v7.1.0 changed the migration shape — that's the kind of diff that silently diverges between environments that published at different times. Compare your published migration against vendor/brackets/media/database/migrations after every upgrade, and if you need a fix, a committed fork is still safer than dev-main.

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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource