fomvasss / laravel-medialibrary-extension
Extension for use spatie/laravel-medialibrary
Package info
github.com/fomvasss/laravel-medialibrary-extension
pkg:composer/fomvasss/laravel-medialibrary-extension
Requires
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- spatie/laravel-medialibrary: ^11.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.0|^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- 6.x-dev
- 6.5.1
- 6.5.0
- 6.4.1
- 6.4.0
- 6.3.3
- 6.3.2
- 6.3.1
- 6.3.0
- 6.2.0
- 6.1.0
- 6.0.1
- 6.0.0
- 5.x-dev
- 5.8.0
- 5.7.0
- 5.6.0
- 5.5.0
- 5.4.0
- 5.3.0
- 5.2.0
- 5.1.0
- 5.0.1
- 5.0.0
- 4.x-dev
- 4.9.0
- 4.8.0
- 4.7.0
- 4.6.1
- 4.6.0
- 4.5.0
- 4.4.0
- 4.3.0
- 4.2.0
- 4.1.0
- 4.0.2
- 4.0.1
- 4.0.0
- 3.1.0
- 3.0.0
- 2.0.0
- 1.2.0
- 1.1.0
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- dev-develop
- dev-fix_namespaces-1.2.0
- dev-fix_namespaces
This package is auto-updated.
Last update: 2026-10-04 17:12:52 UTC
README
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








