spatie/laravel-medialibrary
Associate files with Eloquent models via a simple API: add uploads or local paths to media collections, store them on any Laravel filesystem, and generate image manipulations for images and PDFs. Ideal for managing media in Laravel apps.
Install the package and publish its assets:
composer require spatie/laravel-medialibrary
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"
php artisan migrate
php artisan storage:link
Look first at config/media-library.php, the published migration, and Spatie\MediaLibrary\MediaCollections\Models\Media. Then read the package docs at https://spatie.be/docs/laravel-medialibrary/v11.
Add media support to a model by implementing HasMedia and using InteractsWithMedia:
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class User extends Authenticatable implements HasMedia
{
use InteractsWithMedia;
public function registerMediaCollections(): void
{
$this->addMediaCollection('avatar')->singleFile();
}
}
A first practical use case is a user avatar. Validate the upload in a form request, attach it, and render it:
$user->addMediaFromRequest('avatar')->toMediaCollection('avatar');
<img src="{{ $user->getFirstMediaUrl('avatar') }}" alt="{{ $user->name }}">
That is the core day-to-day loop: define a collection on the model, add a file to it, then read URLs from the model.
Configure collections and conversions on the model. Keep media rules close to the owning model:
public function registerMediaCollections(): void
{
$this->addMediaCollection('avatar')
->singleFile()
->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);
$this->addMediaCollection('images');
$this->addMediaCollection('downloads')
->useDisk('s3');
}
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumb')
->width(368)
->height(232)
->performOnCollections('avatar', 'images')
->nonQueued();
$this->addMediaConversion('large')
->width(1200)
->withResponsiveImages()
->performOnCollections('images');
}
Use collection config for disk choice, and the explicit disk argument only when needed. Prefer useDisk('s3') in registerMediaCollections() so controllers stay simple. If a collection can go to different disks, toMediaCollection('downloads', 's3') works.
Normalise upload handling in controllers or actions. Use Laravel validation for size and MIME type first; the package is storage/manipulation, not upload validation:
$request->validate([
'image' => ['required', 'image', 'max:5120'],
]);
$media = $post->addMediaFromRequest('image')
->usingName($request->input('title'))
->withCustomProperties(['alt' => $request->input('alt')])
->toMediaCollection('images');
Other useful adders include addMedia($request->file('image')), addMediaFromUrl(), addMediaFromDisk(), addMediaFromBase64(), and addMediaFromString().
Retrieve media consistently. Use getFirstMedia() when you need the model, getFirstMediaUrl() for a URL, and getMedia() for galleries:
$avatarUrl = $user->getFirstMediaUrl('avatar', 'thumb');
$images = $post->getMedia('images');
$firstImage = $post->getFirstMedia('images');
$srcset = $post->getFirstMedia('images')?->getSrcset('large');
For private S3 files, generate temporary URLs rather than exposing the disk path:
$url = $media->getTemporaryUrl(now()->addMinutes(10), 'thumb');
Expose media deliberately in APIs. Do not rely on raw model serialization for public APIs. Map media to a small DTO-like array:
'images' => $post->getMedia('images')->map(fn (Media $media) => [
'url' => $media->getUrl(),
'thumb' => $media->getUrl('thumb'),
'alt' => $media->getCustomProperty('alt'),
])->values(),
Use custom properties for editorial metadata. withCustomProperty(), setCustomProperty(), and getCustomProperty() are useful for alt text, captions, credits, and external IDs without adding another table.
Queue conversions in production, but keep critical conversions synchronous where needed. The package can queue conversions; ensure workers are running. For small avatar thumbnails or tests, nonQueued() avoids missing-conversion surprises. After changing conversion definitions, regenerate existing files with:
php artisan media-library:regenerate
Clean up intentionally. singleFile() collections replace the previous file when a new one is added. For multi-file collections, remove individual items with $media->delete() or wipe a collection with $model->clearMediaCollection('images'). If deleting a model should preserve files, use deletePreservingMedia() where appropriate.
getFirstMediaUrl() returns a path but the browser 404s, check php artisan storage:link and APP_URL. For S3, check bucket visibility and whether you need temporary URLs.getFirstMediaUrl() returns an empty string when no media exists. Use a fallback in Blade or a default asset; do not assume it returns null.acceptsMimeTypes() filters collections, but it is not a replacement for request validation. Validate size, MIME type, and image dimensions where relevant.nonQueued() for conversions that must exist immediately.config/media-library.php, especially custom path generators, file namers, or media model bindings, run php artisan config:clear locally and redeploy config cache correctly.getUrl(), getTemporaryUrl(), and getFirstMediaUrl() rather than concatenating storage/ paths. This keeps local, S3, and CDN setups portable.php artisan media-library:regenerate after changing widths, formats, or conversion names.UploadedFile::fake()->image('avatar.jpg') works well; Storage::fake('public') isolates files. If conversions are queued, either use Queue::fake() plus assertions or mark the conversion nonQueued() for deterministic tests.Media models, path generators, file namers, image generators, MIME type detectors, and observers can be bound in config and overridden in the service container. This is the right place to customise naming, storage paths, or generated URLs without forking the package.deletePreservingMedia() when records are soft-deleted or archived but files must remain.How can I help you explore Laravel packages today?