waad/filament-media
A Filament media manager for browsing, uploading, and organizing images and files in your admin panel. Includes reusable media library UI and fields for attaching media to Eloquent models, simplifying file selection and management across Filament resources.
waadmawlood/filament-media is a Filament plugin that adds a media library (upload, browse, pick, and attach files) to a panel. The day-to-day value is that you stop hand-rolling upload fields and a media table for every project.
Minimal path to first value:
composer require waadmawlood/filament-mediaphp artisan vendor:publish --tag="filament-media-config"
php artisan vendor:publish --tag="filament-media-migrations"
php artisan migrate
php artisan storage:link
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
\Waadmawlood\FilamentMedia\FilamentMediaPlugin::make(),
]);
}
Where to look first: the package README for the exact namespace/config key names (they vary between versions), then the published config/filament-media.php. That file is the fastest map of what is configurable — disk, directory, accepted mime types, image conversions, and whether the media resource appears in navigation.
Panel plugin vs. per-resource usage. Registering the plugin gives you the browse/manage UI and navigation entry; the field component is what you actually use inside your own resources. Most projects want both, but you can register the field without exposing the full resource by disabling its navigation in config — useful for client-facing panels where editors shouldn't see a raw media library.
Single vs. multiple attachments. Use the picker field for a single relation (e.g. a cover_image_id) and the multi-select/multiple mode for galleries. Store the foreign key or pivot rows yourself via a normal Eloquent relationship — the package handles selection and upload, not your domain schema. Keeping your own relationship means policies, cascades, and API serialization stay under your control.
Tables and infolists. Point an ImageColumn at the media URL accessor rather than duplicating path logic. For lists with many rows, prefer a thumbnail conversion over the original file so you aren't shipping 4MB JPEGs into an admin table.
Disk and directory strategy. Set the disk in config, not in each field. A common split: public locally, S3 in production, with the directory namespaced (media/{tenant_id}) when you have multi-tenancy. Do this through a config override in a service provider so it can read from the current tenant.
Uploads from the front end. Use the package's upload entry point behind your own controller/FormRequest when uploads come from outside the panel, then run the same Storage::fake() tests you'd write for a hand-rolled upload. Reuse the package's validation rules for mime/size rather than re-declaring them.
Seeding and tests. Storage::fake('public') plus UploadedFile::fake()->image(...) and programmatic creation of media records keeps feature tests deterministic. Don't assert on generated file names; assert on the record and the disk path prefix.
Upgrade hygiene. Pin the version in composer.json and read the changelog between minors — this package is pre-2.0 and refactors the service provider internals as it matures.
resources/views/vendor/..., verify after upgrading that your overrides still resolve; if not, re-publish or point the field/resource at your own view explicitly. This is a low-risk internal refactor with no reported breaking API changes, but it is the kind of change that silently reverts a custom UI.storage:link is not optional. A perfectly working upload with a broken image is almost always a missing symlink or a disk whose url config doesn't match APP_URL.php artisan config:clear and php artisan filament:optimize-clear (or a full optimize:clear) before you start debugging why your new disk setting "isn't applied" — Filament caches components and the panel config aggressively in production.livewire.temporary_file_upload rules — the panel's max upload size and PHP's upload_max_filesize/post_max_size are separate limits, and hitting either produces a vague validation error.canAccess()/shouldRegisterNavigation() rather than removing the plugin — removing the plugin also removes the field component.How can I help you explore Laravel packages today?