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

Spatie Laravel Media Library Plugin Laravel Package

filament/spatie-laravel-media-library-plugin

Filament Spatie Media Library Plugin integrates Spatie's media library with Filament forms, letting you upload and attach files to Eloquent models using collections, custom disks, and file upload options.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a first-party Filament plugin (MIT, ships in the filamentphp/filament monorepo, versioned in lockstep with Filament majors — ^5.0 here means Filament v5). The README is essentially the whole documentation surface, so treat it as a thin adapter over spatie/laravel-medialibrary: nearly every non-obvious question is answered in Spatie's docs, not Filament's.

First three places to look

  1. This README — it is the complete component API (SpatieMediaLibraryFileUpload, SpatieMediaLibraryImageColumn, SpatieMediaLibraryImageEntry, SpatieMediaLibraryFileAttachmentProvider).
  2. Spatie's "Preparing your model" + "Working with media collections" pages — you cannot use the components without this.
  3. Filament's own FileUpload / ImageColumn / ImageEntry docs — the media components extend them, so ->image(), ->maxSize(), ->acceptedFileTypes(), ->imageEditor(), ->circular(), ->stacked() all still apply.

Minimal setup

composer require filament/spatie-laravel-media-library-plugin:"^5.0" -W

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class User extends Authenticatable implements HasMedia
{
    use InteractsWithMedia;

    public function registerMediaCollections(): void
    {
        $this->addMediaCollection('avatars')
            ->singleFile()
            ->useDisk('public');

        $this->addMediaCollection('attachments'); // multiple files
    }

    public function registerMediaConversions(?Media $media = null): void
    {
        $this->addMediaConversion('thumb')->width(200)->height(200)->nonQueued();
    }
}

First use case — avatar on a User resource

use Filament\Forms\Components\SpatieMediaLibraryFileUpload;

SpatieMediaLibraryFileUpload::make('avatar')
    ->collection('avatars')
    ->image()
    ->imageEditor()
    ->conversion('thumb');
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;

SpatieMediaLibraryImageColumn::make('avatar')
    ->collection('avatars')
    ->conversion('thumb')
    ->circular();

That pair — one collection, one form field, one column — covers ~70% of real-world usage (avatars, logos, cover images). Everything else is attachments/galleries.

What to build second: attachments with ->multiple()->reorderable(), then a rich-editor attachment provider if you need inline images in a RichEditor.


Implementation Patterns

1. The field name is virtual — bind it to a collection, not a column

The component persists through the media table, so the field name never needs to exist in $fillable or in your schema. Name it after the collection for readability, but always pass ->collection() explicitly so the intent survives refactors:

SpatieMediaLibraryFileUpload::make('cover')
    ->collection('covers')
    ->image()
    ->required();

Do not pair it with a cover DB column; if you already have one (legacy path-based upload), use a different field name or ->dehydrated(false)-style separation, otherwise you'll confuse the two storage mechanisms.

2. Single-file vs multiple-file is decided on the model, not the form

$this->addMediaCollection('avatars')->singleFile();   // replaces on each upload
$this->addMediaCollection('attachments');              // accumulates

The form mirrors it: ->multiple() for the latter, nothing for the former. A singleFile() collection rendered with ->multiple() produces silent replacement behaviour that's hard to debug.

3. Galleries: tag uploads with custom properties and filter on them

This is the pattern for "one page, many image slots" or multi-tenant galleries. The upload side writes a custom property from another field; the display/edit side filters on the same property:

SpatieMediaLibraryFileUpload::make('images')
    ->multiple()
    ->reorderable()
    ->collection('images')
    ->customProperties(fn (Get $get): array => [
        'gallery_id' => $get('gallery_id'),
    ])
    ->filterMediaUsing(
        fn (Collection $media, Get $get): Collection => $media->where(
            'custom_properties.gallery_id',
            $get('gallery_id'),
        ),
    );

Notes that matter in practice:

  • customProperties() is evaluated at upload time only. Media already in the collection keeps its old properties — changing gallery_id does not re-tag existing files. If you need re-tagging, do it in a ->saveRelationshipsUsing() hook or an observer.
  • Filtering by JSON path (custom_propeties.gallery_id) works on the in-memory Collection because Spatie casts custom_properties to array and Laravel's where() accepts dot notation on arrays.
  • Remember ->customProperties() on the column/entry side is not a thing; use ->filterMediaUsing() there.

4. Dynamic custom properties from the file itself

Useful for storing dimensions, EXIF, or a hash without a separate observer:

use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Spatie\Image\Image;

SpatieMediaLibraryFileUpload::make('image')
    ->image()
    ->customProperties(function (TemporaryUploadedFile $file): array {
        $image = Image::load($file->getRealPath());

        return ['height' => $image->getHeight(), 'width' => $image->getWidth()];
    });

Do the expensive work here (or in a queued conversion) rather than in a table column callback — columns are evaluated per row.

5. Conversions: display with conversion(), generate on the model

->conversion('thumb') only reads; the conversion must be registered in registerMediaConversions(). Filament falls back to the original file when the conversion is absent, which masks misconfiguration — check $record->getMedia('collection')->first()->hasGeneratedConversion('thumb') when an image looks suspiciously large.

Offload generated derivatives to a CDN bucket with ->conversionsDisk('s3') (this is separate from the originals' disk). Note ->responsiveImages() on the component and ->withResponsiveImages() on the collection are different switches; the collection-level one applies to every upload, so prefer it for consistency.

6. Reordering

SpatieMediaLibraryFileUpload::make('attachments')
    ->multiple()
    ->reorderable();

Requires ->multiple(). Order is written to order_column; read it back with ->orderBy('order_column') on the relation or by relying on Spatie's default Media ordering. If you sort manually elsewhere (e.g. latest()), reordering in the form appears to "not save".

7. Rich editor attachments

This is the most integration-heavy feature and the one most likely to bite you:

class Post extends Model implements HasRichContent
{
    use InteractsWithRichContent;

    public function setUpRichContent(): void
    {
        $this->registerRichContent('content')
            ->fileAttachmentProvider(
                SpatieMediaLibraryFileAttachmentProvider::make()
                    ->collection('content-file-attachments')
                    ->preserveFilenames(),
            );
    }
}

Provider-level knobs: ->collection(), ->preserveFilenames(), ->mediaName(fn (TemporaryUploadedFile $file) => ...), ->customProperties([...]).

The contract is strict:

  • The rich content attribute (content) must be nullable in the database.
  • The collection is owned by the provider — Filament prunes unused media from it on save. Never mix editor attachments with other files in the same collection.
  • mediaName() is the lever for human-readable download filenames and for avoiding collisions when preserveFilenames() is on.

8. Integrating into panels

Put the field in a Section alongside a Grid like any other upload; nothing special is needed for tenancy. For "global defaults", configure once instead of per-field:

use Filament\Forms\Components\SpatieMediaLibraryFileUpload;

SpatieMediaLibraryFileUpload::configureUsing(
    fn (SpatieMediaLibraryFileUpload $component) => $component->maxSize(10240),
);

9. Testing pattern

Fake the disk the collection uses, then drive the Livewire component — asserting on media, not on a column:

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use function Pest\Livewire\livewire;

Storage::fake('public');

livewire(EditUser::class, ['record' => $user->id])
    ->fillForm(['avatar' => [UploadedFile::fake()->image('avatar.jpg')]])
    ->call('save')
    ->assertHasNoFormErrors();

expect($user->refresh()->getMedia('avatars'))->toHaveCount(1);

Storage::fake('public') must match the collection's disk, not just FILESYSTEM_DISK. When testing a rich editor, assert on getMedia('content-file-attachments') and re-save the model twice to prove pruning doesn't delete live attachments.


Gotchas and Tips

Disk and visibility are Spatie's job, not Filament's. The base FileUpload's ->directory() and ->visibility() are ignored by this component (the README says so explicitly). Path comes from Spatie's path_generator/disk_name config or the model's getMediaDirectoryName(); disk comes from the collection's useDisk() or ->disk(). The README's disk section actually shows FileUpload::make('attachment')->disk('s3') — a copy/paste trap; the equivalent is SpatieMediaLibraryFileUpload::make(...)->disk('s3'), and the disk must be one Spatie can resolve.

Two disk configs fight each other. Filament's FILESYSTEM_DISK/panel config governs the default upload disk, Spatie's config/media-library.php governs collections. The plugin deliberately prefers Filament's to stay consistent across packages. If files land in the wrong place, check both before touching code.

Private files are not supported out of the box. Spatie has no first-class private upload path. The documented workaround is an S3 bucket policy plus ->visibility('private') so Filament emits temporary URLs. On a local disk you'll get 403s on generated URLs — don't try to patch visibility() alone.

Default collection is default. A SpatieMediaLibraryImageColumn::make('avatar') with no ->collection() shows only uncollected media. If your uploads use ->collection('avatars'), the column must too — or use ->allCollections(). This is the single most common "my image column is blank" bug.

Namespaces moved in v4/v5. Get/Set are now Filament\Schemas\Components\Utilities\Get and ...\Set, not Filament\Forms\Get. Old blog snippets will fatal. Same class of trap: RichEditor attachment classes live under Filament\Forms\Components\RichEditor\FileAttachmentProviders\... and the model concerns under Filament\Forms\Components\RichEditor\Models\Concerns\....

N+1 on table columns. Each SpatieMediaLibraryImageColumn touches the media relation per row. Add ->modifyQueryUsing(fn (Builder $query) => $query->with('media')) (or withCount) on the table query once a listing grows past a screen.

Conversions are synchronous by default. Filament uploads trigger conversion generation during save; with responsiveImages() or several conversions this makes "Save" feel slow and occasionally trips max_execution_time on shared hosting. Either make conversions ->nonQueued() for small derivatives, or push uploads to a queue and show placeholders in the meantime.

Unregistered collections silently under-configure. Adding to a collection you never declared in registerMediaCollections() generally works, but you lose singleFile(), disk, conversions, and withResponsiveImages() — producing duplicate avatar rows nobody can explain. Declare every collection explicitly.

Soft deletes keep the files. Spatie removes media assets when the Media model is deleted, but a soft-deleted parent leaves both the record and the files. Add a forceDeleted hook or a scheduled cleanup for user-generated media, or you'll grow an orphaned bucket.

filterMediaUsing() receives a plain Collection, not a query. Fine for a handful of files; if a record has thousands of media rows, filter in modifyQueryUsing on the relation instead so the DB does the work.

Reordering silently no-ops without ->multiple(), and any manual ->orderBy(...) elsewhere on the relation will override order_column.

The rich editor provider deletes things. Because it prunes the collection on save, a second field pointing at the same collection will have its files removed on the next save. One collection per attribute, always.

Extension points are narrow but real: subclass SpatieMediaLibraryFileUpload to add your own defaults, or use configureUsing(). For the rich editor, swap the provider class (extend SpatieMediaLibraryFileAttachmentProvider) rather than post-processing in a model observer — the provider already owns naming and cleanup.

Version pinning: composer require ... -W is required because the plugin pins the Filament major; without -W a stale filament/filament constraint blocks the install. Upgrade the plugin and Filament together — never independently.

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.
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections
webx-ui/module-catalog-feeds
makidizajnerica/laravel-dto
matviib/scheduler
webx-ui/templates
anodyne/blade-tabler-icons
bref/laravel-websockets
anodyne/blade-hugeicons
1994/flatpack-statamic