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

Filament Media Manager Laravel Package

slimani/filament-media-manager

A media manager plugin for Filament v4 and v5 with hierarchical folders, tagging, search and filtering, previews, and configurable multi-disk storage. Built natively for Filament forms, tables, and actions with a smooth, responsive browser UI and per-field or global disk settings.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish both migration sets — the plugin's own tables (media_files, folders, tags, media_attachments) plus Spatie MediaLibrary's, since the package builds on it:

composer require slimani/filament-media-manager

php artisan vendor:publish --tag="media-manager-migrations"
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"

php artisan migrate

php artisan vendor:publish --tag="media-manager-config"

Register the plugin in your panel provider — this alone gives you the media browser page (folders, search, filters, tags, bulk actions) in the sidebar:

use Slimani\MediaManager\MediaManagerPlugin;

public function panel(Panel $panel): Panel
{
    return $panel->plugin(MediaManagerPlugin::make());
}

Then make a model "media-aware" with the InteractsWithMediaFiles trait and add the FK column for single-file relations:

Schema::table('users', function (Blueprint $table) {
    $table->foreignId('avatar_id')->nullable()->constrained('media_files')->nullOnDelete();
});
class User extends Model
{
    use InteractsWithMediaFiles;

    protected $fillable = ['name', 'email', 'avatar_id'];

    public function avatar(): BelongsTo
    {
        return $this->mediaFile('avatar_id');
    }
}

Where to look first: the published config file (global disk, conversions, table prefix), then docs/url-generation.md — disk visibility drives almost every "my image doesn't load" bug you'll hit. Skim the README's Selection behavior note too, because the multi-select gesture is non-obvious and will generate user questions.

First use case: drop a MediaPicker::make('avatar_id')->relationship('avatar') into an existing resource form. That's the shortest path from install to value — the media browser page is already useful on its own for ad-hoc uploads, but the picker is where it earns its keep day-to-day.

Implementation Patterns

Single-file vs. polymorphic collections. There are two distinct integration styles, and picking the wrong one is the most common setup mistake:

// Single file — needs an FK column on YOUR model's table + $fillable entry
public function avatar(): BelongsTo { return $this->mediaFile('avatar_id'); }

// Collections — no migration needed, uses the package's media_attachments table
public function documents(): MorphToMany { return $this->mediaFiles('documents'); }

Form field pattern. relationship() must point at the method name on the model, not the FK column:

MediaPicker::make('avatar_id')->relationship('avatar')->label('User Avatar')->required(),

MediaPicker::make('documents')->relationship('documents')->multiple()->label('Files'),

Table + infolist presentation. MediaColumn is for list views; it proxies Filament's image column API so ->circular() and ->stacked() work as expected. For detail views, split by intent — MediaImageEntry when you want thumbnail/conversion behaviour, MediaFileEntry when you want the built-in slide-over preview for PDFs, videos, and mixed types:

MediaImageEntry::make('avatar')->conversion('thumb')->circular()->width(100)

MediaFileEntry::make('cv')->label('User CV')

RichEditor integration is a three-part contract. All three pieces are required or you get silent failures:

  1. Model implements HasRichContent + uses InteractsWithRichContent, with setUpRichContent() registering the plugin and its collection/directory.
  2. The RichEditor component gets MediaManagerRichContentPlugin::make() in its ->plugins([...]).
  3. Rendering goes through renderRichContent($field), not raw HTML:
TextEntry::make('resume')->html()->state(fn ($record) => $record->renderRichContent('resume'))

That third step is what makes private-disk images work at all, since it re-signs URLs at render time. Treat it as mandatory, not optional.

Disk strategy. Configure globally in the config file for the app default, and override per field where a model needs isolation (e.g. avatars on public, contracts on s3-private). Pair conversions_disk with the primary disk deliberately — File::getUrl() honours it separately from the storage disk.

Tenancy. Opt-in via the separate migration + ->tenantAware(). The default resolver reads Filament::getTenant(); override with tenantResolver: and tenantColumn: when you're not on Filament's tenant model. Note the resolver and column can't be chosen after the fact without editing the migration first — decide before you run it.

Panel-level customisation as a workflow tool. ->navigationLabel(), ->navigationGroup(), ->shouldRegisterNavigation(fn () => auth()->user()->isAdmin()), plus headerWidgets/footerWidgets and Blade header/footer views let you turn this into a branded "Assets" section of an internal admin without forking the package. That's the extension point worth exploiting before considering a fork.

Gotchas and Tips

  • Two migration publishes, not one. Skipping the Spatie migration publish produces confusing failures at upload time rather than install time. Put both in your setup docs.
  • Custom Filament themes break silently. If you use a custom theme, you must add both lines to theme.css — the @import for component styles and the @source glob so Tailwind scans the plugin's Blade files for utility classes. Missing @source yields an unstyled-but-not-broken grid, which is easy to misdiagnose as a CSS bug.
  • $fillable omissions are the classic runtime error. The FK columns (avatar_id, cv_id, …) must be in $fillable, or the picker saves nothing and the form appears to "revert". Symptom: no exception, just a null after save.
  • Multi-select is a gesture, not a modifier key. A normal click selects exactly one item; multi-select requires the check icon (desktop) or a long-press (touch). Users will report "I can't select multiple files" — pre-empt this in onboarding or tooltips. Behaviour is under review upstream (issue #18), so don't build a workflow that depends on it staying identical across versions.
  • Private disks + expiring URLs. Clean URLs only come from disks with visibility => 'public'. Everything else gets temporary signed URLs, so any place you cache or persist a resolved URL (emails, PDF exports, API payloads) will eventually serve expired links. Always call getUrl() at render/send time, and use renderRichContent() for stored HTML.
  • Tenancy fails closed. With tenantAware() enabled and no resolvable tenant, reads return empty sets and writes throw. And critically: tenant DB scoping does not make a public disk private — files stay accessible to anyone with the URL. Use a private disk for tenant assets.
  • withVideoThumbnails() requires FFMPEG on the server. Easy to enable locally and forget in the deploy image; uploads will succeed and thumbnails will silently be absent.
  • Adoption risk is real. ~21 stars, zero dependents, and only a handful of screenshots as documentation outside the README. Pin an exact version in composer.json, and smoke-test upgrades against your panel — Filament v4/v5 dual support means the internals carry compatibility branches. Budget for the possibility of adopting the source if you lean on it heavily.
  • Debugging checklist, in order: php artisan storage:link exists → disk configured in config → disk visibility is public if you expect clean URLs → conversions_disk set if you use conversions → queue worker running if conversions are queued (Spatie default). That sequence resolves the majority of "file uploaded but nothing shows" reports.
  • Extension points before forking: header/footer views and widgets cover most branding asks; the published config covers disks and table prefix. Only the picker's internal UI behaviour (selection gestures, grid layout) genuinely requires touching source.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core