fomvasss/laravel-medialibrary-extension
Laravel Medialibrary Extension
English | Українська
Form and API layer on top of spatie/laravel-medialibrary: declare the media collections of a model once and save everything a form or an API client sends — new files, deletions, sort order, alt/title and other custom properties, the main file — with one call.
$model->mediaManage($request)— save media from an HTML form (admin panel)$model->mediaManageRefresh($data)— save media from API data: files are uploaded beforehand as temporary media and then attached byidstrict_refresh— safe attach / delete for data that comes from a client- default image conversions for all models from config, main / active flags, owner (
user_id), file name generators, helpers for reading
Admin UI for it: the Lte3::mediaFile() / Lte3::mediaImage() fields of fomvasss/laravel-lte3.

Contents
- Requirements
- Installation
- Model
- Saving from a form
- Admin UI (laravel-lte3)
- Saving from an API: temporary uploads
- Reading media
- Conversions
- File names
- Configuration
- Upgrading
- Support
Requirements
- PHP 8.1+
- Laravel 10 – 13
- spatie/laravel-medialibrary 11
Installation
composer require fomvasss/laravel-medialibrary-extension
Publish spatie/laravel-medialibrary migrations and config:
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"
Publish the package config and migrations:
php artisan vendor:publish --provider="Fomvasss\MediaLibraryExtension\ServiceProvider"
The package migrations add columns is_main, is_active, user_id to the media table and create the media_temporaries table (owner model of temporary uploads). If your users have uuid keys, change user_id in the published migration to uuid() before migrating.
php artisan migrate
Model
Implement Fomvasss\MediaLibraryExtension\HasMedia\HasMedia and use the InteractsWithMedia trait (instead of the spatie ones). Declare the collections: the collection name is also the form field name.
<?php
namespace App\Models;
use Fomvasss\MediaLibraryExtension\HasMedia\HasMedia;
use Fomvasss\MediaLibraryExtension\HasMedia\InteractsWithMedia;
use Illuminate\Database\Eloquent\Model;
use Spatie\Image\Enums\Fit;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Article extends Model implements HasMedia
{
use InteractsWithMedia;
// one file per collection: a new upload replaces the old one
protected $mediaSingleCollections = ['image'];
// many files per collection
protected $mediaMultipleCollections = ['images', 'files'];
// optional: your conversions in addition to `default_conversions` from config
public function customMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('preview')
->performOnCollections('image', 'images')
->fit(Fit::Contain, 600, 600)
->format('webp');
}
}
All spatie/laravel-medialibrary methods (addMedia(), getMedia(), getFirstMediaUrl(), …) keep working.
Saving from a form
public function update(ArticleRequest $request, Article $article)
{
$article->update($request->validated());
$article->mediaManage($request);
// or via the facade: \MediaManager::manage($article, $request);
return back();
}
mediaManage() goes through every collection of the model and reads its fields from the request. Two formats are supported; each collection can use either of them.
Simple format
The file input has the collection name; service fields have suffixes (field_suffixes in config).
<!-- multiple collection `images` -->
<input type="file" name="images[]" multiple>
<!-- sort order of saved files: images_weight[<media id>] -->
<input type="hidden" name="images_weight[13]" value="0">
<input type="hidden" name="images_weight[15]" value="1">
<!-- delete saved files -->
<input type="hidden" name="images_deleted[]" value="15">
<!-- custom properties of saved files: images_custom[<media id>][<property>] -->
<input type="hidden" name="images_custom[13][alt]" value="Sunset over the sea">
<!-- single collection `image`: a new file replaces the current one -->
<input type="file" name="image">
<input type="hidden" name="image_custom[new][alt]" value="Cover"> <!-- properties of the new file -->
<input type="hidden" name="image_deleted" value="7"> <!-- delete the current one -->
Besides, media_deleted[] (deleted_request_input in config) deletes media of the model by id from any collection.
Custom properties of a new file in a multiple collection can't be sent in this format (it has no id yet) — use the expand format.
Expand format
A row per file — saved (id) or new (file) — with all its data. Lets you set properties and order of new files and the main file in one request.
<!-- saved file -->
<input type="hidden" name="files[0][id]" value="13">
<input type="hidden" name="files[0][weight]" value="1">
<input type="hidden" name="files[0][delete]" value="0">
<input type="hidden" name="files[0][title]" value="Price list 2026">
<!-- new file -->
<input type="file" name="files[1][file]">
<input type="hidden" name="files[1][weight]" value="0">
<input type="hidden" name="files[1][is_main]" value="1">
<input type="hidden" name="files[1][alt]" value="Scheme">
<!-- single collection: one row without an index -->
<input type="file" name="image[file]">
<input type="hidden" name="image[alt]" value="Cover">
Row keys:
| Key | |
|---|---|
id |
a saved media of the model |
file |
a new file (UploadedFile); in a single collection it replaces the current file |
url / path / base64 |
a new file from a URL, a local path or a base64 string |
weight |
order (order_column) |
delete |
1 — delete the media of id |
is_main |
1 — the main file of the collection (only one: the others are reset) |
is_active |
0 — hide without deleting (default 1) |
alt, title, … |
custom properties — only those listed in expand.allowed_custom_properties |
A collection named like a Symfony
Requestproperty (files,query,request,headers, …) works in the expand format since 6.4.1.
Single and multiple collections
- multiple (
$mediaMultipleCollections) — files are added; they are deleted only explicitly (*_deleted/delete). - single (
$mediaSingleCollections) — one file: a new upload deletes the previous one.
Validation
Simple format:
'images' => 'nullable|array',
'images.*' => 'file|image|max:10240',
'image' => 'nullable|image|max:10240',
Expand format: an element of files.* is an array, the file is in files.*.file. A rule that accepts both formats:
use Illuminate\Validation\Rule;
'files' => 'nullable|array',
'files.*' => Rule::forEach(fn ($value) => is_array($value) ? ['array'] : ['file', 'max:51200', 'mimes:jpg,png,pdf']),
'files.*.file' => 'nullable|file|max:51200|mimes:jpg,png,pdf',
Admin UI (laravel-lte3)
fomvasss/laravel-lte3 has ready fields that send exactly these formats: drop zone, previews before saving, image thumbnails, delete with restore, drag-and-drop sorting, a modal for custom properties.
{!! Lte3::formOpen(['action' => route('admin.articles.update', $article), 'model' => $article, 'files' => true]) !!}
{{-- simple format (default) --}}
{!! Lte3::mediaImage('images', $article, ['multiple' => true, 'custom_properties' => ['alt', 'title']]) !!}
{!! Lte3::mediaImage('image', $article, ['custom_properties' => ['alt']]) !!}
{{-- expand format: properties of new files, main file --}}
{!! Lte3::mediaFile('files', $article, [
'multiple' => true,
'format' => 'expand',
'main' => true,
'accept' => 'image/*,.pdf,.doc,.docx',
'custom_properties' => ['title', 'alt'],
]) !!}
{!! Lte3::formClose() !!}

Details: lte3 docs — mediaFile field.
Saving from an API: temporary uploads
For SPA / mobile clients the file is uploaded first, separately from the entity form:
- The client sends the file to your upload endpoint; the file is stored as a media of the temporary model (
MediaTemporary), and the response returns itsid. - The client sends the entity with this
id;mediaManageRefresh()moves the media to the model.
Upload endpoint:
use Fomvasss\MediaLibraryExtension\Actions\UploadMediaTemporaryFile;
public function upload(Request $request)
{
$request->validate(['file' => 'required|file|max:51200|mimes:jpg,png,webp,pdf']);
$media = (new UploadMediaTemporaryFile)->handle([
'file' => $request->file('file'),
'user_id' => $request->user()?->id, // owner — checked in strict mode
]);
return response()->json(['id' => $media->id, 'url' => $media->getUrl()]);
}
Saving the entity:
public function update(Request $request, Article $article)
{
$article->update($request->only('title', 'body'));
$article->mediaManageRefresh($request->only('image', 'images', 'media_deleted'));
}
{
"image": {"id": "<temporary media id>", "alt": "Cover"},
"images": [
{"id": "<temporary media id>", "weight": 0},
{"id": "<saved media id>", "weight": 1, "title": "Updated title"},
{"id": "<saved media id>", "delete": true}
],
"media_deleted": ["<media id>"]
}
Row keys are the same as in the expand format (id, weight, delete, is_main, is_active, custom properties); media_deleted (deleted_request_input in config) — ids to delete from any collection.
Strict mode
By default mediaManageRefresh() trusts every id in the data: any media can be attached to the model or deleted. That is fine for a trusted admin panel, but not for data from a client. Enable it in config/media-library-extension.php:
'strict_refresh' => true,
With it:
- attach by
idworks only for media of this model or an own temporary upload:- uploaded with
user_id— only by the same user (the$userargument ofmediaManageRefresh()or the authenticated one) - uploaded without
user_idin a request that came with the session cookie (public web form) — only from the same session. A session started for a request without cookies (e.g. Sanctum stateful API called with a Bearer token) is ignored: it does not survive to the next request - uploaded without both (stateless API) — by anyone who knows the
id, so keep media keys unguessable (uuid)
- uploaded with
deleteandmedia_deletedremove only media of this modeluser_idfrom the data is ignored
Other ids are silently skipped.
Clearing temporary uploads
Unattached temporary uploads older than temporary.cleartime minutes (a day by default) are deleted by:
\Fomvasss\MediaLibraryExtension\Actions\ClearMediaTemporary::doHandle();
Schedule it — in routes/console.php (Laravel 11+) or app/Console/Kernel.php (Laravel 10):
Schedule::call(fn () => \Fomvasss\MediaLibraryExtension\Actions\ClearMediaTemporary::doHandle())->daily();
Reading media
$article->getFirstMediaUrl('image'); // spatie
$article->getMyFirstMediaUrl('image', 'preview', '/img/no-image.png'); // with a default URL
$article->getMyFirstMediaFullUrl('image'); // absolute URL
$article->getMainMedia('images'); // is_main + is_active, otherwise the first
$article->getMainMediaUrl('images', 'preview', '/img/no-image.png');
$media = $article->getFirstMedia('image');
$media->getCustomProperty('alt');
$media->is_main; // bool
$media->user_id; // owner, if `use_auth_user` is on or `user_id` was passed
Conversions
Conversions from default_conversions in config are registered for all models with InteractsWithMedia; a conversion applies to the collections whose names match regex_perform_to_collections. The default is the thumb 100×100 webp for collections like image, photo, gallery, logo, avatar.
'default_conversions' => [
'thumb' => [
'quantity' => 75,
'fit' => \Spatie\Image\Enums\Fit::Crop,
'width' => 100,
'height' => 100,
'format' => 'webp',
'regex_perform_to_collections' => '/img|image|photo|gallery|scr|logo|avatar/i',
'non_queued' => true,
],
],
Model-specific conversions go to customMediaConversions() (see Model). The default quality is default_img_quantity, per model — setMediaQuality().
Fit modes on the same image resized to 320×480 (files in docs/medialibrary):
| Original | Contain |
Max |
Crop |
Fill |
FillMax |
Stretch |
|---|---|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
File names
filename_generator makes the name of a stored file:
DefaultFileNameGenerator(default) — slug of the original name:Price List (2026).PDF→price-list-2026.PDFRandomFileNameGenerator— 32 random characters, the extension is kept
Your own — a class with public static function get(string $originalName): string (FileNameGeneratorInterface).
Configuration
config/media-library-extension.php:
| Key | Default | |
|---|---|---|
filename_generator |
DefaultFileNameGenerator |
name of a stored file |
default_img_quantity |
85 |
default quality of conversions |
default_conversions |
thumb |
conversions for all models |
field_suffixes |
_weight, _deleted, _custom |
suffixes of service fields in the simple format |
deleted_request_input |
media_deleted |
field with ids to delete from any collection |
use_auth_user |
false |
write the authenticated user to media.user_id |
strict_refresh |
false |
strict mode of mediaManageRefresh() |
expand.allowed_custom_properties |
alt, title |
custom properties written from the expand format and API data |
temporary.model |
MediaTemporary |
owner model of temporary uploads |
temporary.cleartime |
1440 |
minutes after which an unattached temporary upload is deleted |
Upgrading
See UPGRADING.md and CHANGELOG.md.
Links
- spatie/laravel-medialibrary
- fomvasss/laravel-lte3 — admin fields for this package
Support
If this package is useful to you, consider supporting its development:
USDT TRC20 address:
THLgp6DxiAtbNHvgnKV56vk1L38UuUagKf
Related Packages
Generate a make:service command and add to make:model -s option to autogenerate
Version History
| Version | Released | Laravel | License |
|---|---|---|---|
| 6.5.1 | ^10.0| | MIT | |
| 6.5.0 | ^10.0| | MIT | |
| 6.4.1 | ^10.0| | MIT | |
| 6.4.0 | ^10.0| | MIT | |
| 6.3.3 | ^10.0| | MIT | |
| 6.3.2 | ^10.0| | MIT | |
| 6.3.1 | ^10.0| | MIT | |
| 6.3.0 | ^10.0| | MIT | |
| 6.2.0 | ^10.0| | MIT | |
| 6.1.0 | ^10.0| | MIT | |
| 6.0.1 | ^10.0| | MIT | |
| 6.0.0 | ^10.0| | MIT | |
| 5.8.0 | ^6.18| | MIT | |
| 5.7.0 | ^6.18| | MIT | |
| 5.6.0 | ^6.18| | MIT | |
| 5.5.0 | ^6.18| | MIT | |
| 5.4.0 | ^6.18| | MIT | |
| 5.3.0 | ^6.18| | MIT | |
| 5.2.0 | ^6.18| | MIT | |
| 5.1.0 | ^6.18| | MIT | |
| 5.0.1 | ^6.18| | MIT | |
| 5.0.0 | ^6.18| | MIT | |
| 4.9.0 | ^6.18| | MIT | |
| 4.8.0 | ^6.18| | MIT | |
| 4.7.0 | ^6.18| | MIT | |
| 4.6.1 | ^6.18| | MIT | |
| 4.6.0 | ^6.18| | MIT | |
| 4.5.0 | ^6.18| | MIT | |
| 4.4.0 | ^6.18| | MIT | |
| 4.3.0 | ^6.18| | MIT |
Showing the latest 30 of 44. See every release on Packagist






