codedor/filament-media-library
Filament Media Library gives your admin panels a full media manager: upload, browse, search, and organize files, then attach them to your models via Spatie Media Library. Includes a reusable picker, image conversions, and a clean, native Filament UI.
Minimal setup
composer require wotzebra/filament-media-library
php artisan vendor:publish --tag="filament-media-library-config"
php artisan vendor:publish --tag="filament-media-library-migrations"
php artisan migrate
Register the plugin on the panel you want the media browser available in:
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(MediaLibraryPlugin::make());
}
Attach the media contract to the model you want to store files against (the package builds on Spatie's media library conventions):
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class Post extends Model implements HasMedia
{
use InteractsWithMedia;
}
Where to look first
config/filament-media-library.php — this is where the disk, accepted mime types, and (as of v4.7.0) the attachment model live. Read it before touching anything else.resources/views — the grid markup from PR #89 lives here, so any visual override happens at this level rather than in CSS hacks.First use case
The fastest win is a media picker on a Resource form: drop the field into your schema, upload through the modal, and the selection is persisted to the model's media collection. Once that loop works end-to-end, replace the field in one table column and confirm the grid renders the thumbnail — that validates the whole chain (storage, conversions, grid) in about ten minutes.
Form field inside a Resource
The package's value is that the media picker behaves like any other Filament field. Keep it in the schema, not in a custom Blade view, so validation, reactive state, and ->required() keep working:
use Wotzebra\FilamentMediaLibrary\Forms\Components\MediaPicker;
MediaPicker::make('hero')
->label('Hero image')
->collection('hero')
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp'])
->maxFiles(1)
->required(),
Table column / read-only rendering
Use the same component family in table columns or infolists so the admin sees the thumbnail fallback from #89 rather than a broken image icon when no conversion exists yet.
Custom attachment model (v4.7.0, PR #88)
This is the headline change for anyone with non-default requirements. Before v4.7.0 the attachment model was fixed, which meant the only route to custom behaviour (extra columns, global scopes, tenant relations) was a fork or a container binding that could break on patch releases. Now:
attachment_model) at your class.// config/filament-media-library.php
'attachment_model' => App\Models\MediaAttachment::class,
class MediaAttachment extends \Wotzebra\FilamentMediaLibrary\Models\Attachment
{
protected static function booted(): void
{
static::addGlobalScope(new TenantAttachmentScope);
}
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('thumb')->width(400)->height(400)->nonQueued();
}
}
Multi-tenant / per-panel wiring
Because the model is now swappable, the natural pattern is resolving the model per panel rather than globally: a TenantAttachment for the tenant panel, the default for the central admin. Register the plugin with the resolved class in each panel provider, or drive the config from the current panel in a service provider's boot().
Conversions and the thumbnail fallback
PR #89 added a fallback when a thumbnail isn't available. Treat that as a safety net, not a strategy — register an explicit thumb conversion so the grid never loads originals. A multi-MB original rendered into a 200px card is the single most common cause of a "slow media page" report.
Integration tips
->with('media')) — the grid touches media per row and will otherwise N+1 silently.Storage::fake() in tests and assert against the collection name, not the file path.Config caching will bite you on the model swap. After changing attachment_model, run php artisan config:clear (and config:cache in deploy). A stale cached config is the most likely reason the new model "isn't being used" — the class resolves fine but you're still reading the old value.
Swapping the model doesn't migrate anything. PR #88 changes which class is instantiated; it does not alter the schema. If your custom model expects extra columns, you need your own migration. Conversely, if you keep the default table but introduce a morph alias, set Relation::enforceMorphMap() before any media exists, otherwise existing rows point at the FQCN and new ones at the alias — a subtle, data-dependent bug.
Don't assume the fallback hides missing conversions. The #89 fallback exists so a freshly uploaded file renders before conversions finish. If you queue conversions, expect a brief window of full-size images in the grid. For large collections, use nonQueued() on the thumb conversion or pre-generate on upload.
Breakpoint changes are layout-only. The "real breakpoints" work in #89 means the grid reflows at genuine Tailwind breakpoints rather than fixed column counts. If you had CSS targeting the old selectors or a hardcoded ->columns(4), check it in a panel with the sidebar expanded and collapsed — the content width differs and is where regressions show up.
Card titles come from the media name, not your model. Truncation and escaping are handled, but if you were relying on the filename being hidden, you now get a title line. Override the column/view rather than fighting it with CSS.
Deletion semantics. Filament's delete action on an attachment removes the DB row; the underlying Spatie media record and file may survive depending on how you wired the relation. Decide explicitly between soft deletes, force deletes, and a cleanup job — orphaned files on S3 accumulate quietly and cost real money.
Version the plugin against Filament, not just itself. This package is on 4.x while Filament's own major line has moved; a 4.7.0 plugin release is not a statement about Filament compatibility. Pin both in composer.json and read the package's composer.json constraints before a composer update on a Friday.
Extension points, in order of preference: config (model, disk, mime types) → subclassing the attachment model → publishing and overriding views → forking. Reaching for a fork should be a last resort now that #88 exists; most customisation requests that previously needed one are satisfied by the model swap.
Upgrade risk from 4.6.1 → 4.7.0: low. Both changes are additive (a new configurable seam and layout/CSS improvements). No deprecations or breaking changes are signalled in the release notes. The one thing to verify post-upgrade is any prior workaround you used to customise the attachment model — if you had a container binding or a class_alias shim in place, remove it and use the config key instead, or you'll get two competing resolutions and confusing behaviour.
How can I help you explore Laravel packages today?