fomvasss/laravel-medialibrary-extension

Extension for use spatie/laravel-medialibrary
3,739 4
Install
composer require fomvasss/laravel-medialibrary-extension
Latest Version:6.5.1
License:MIT
Last Updated:Oct 4, 2026
Links: GitHub  ·  Packagist
Maintainer: fomvasss

Laravel Medialibrary Extension

License Build Status Latest Stable Version Total Downloads Quality Score

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 by id
  • strict_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.

lte3 media field

Contents

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 Request property (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() !!}

properties modal

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:

  1. 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 its id.
  2. 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 id works only for media of this model or an own temporary upload:
    • uploaded with user_id — only by the same user (the $user argument of mediaManageRefresh() or the authenticated one)
    • uploaded without user_id in 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)
  • delete and media_deleted remove only media of this model
  • user_id from 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.PDF
  • RandomFileNameGenerator — 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

Support

If this package is useful to you, consider supporting its development:

Monobank Ko-Fi USDT TRC20

USDT TRC20 address: THLgp6DxiAtbNHvgnKV56vk1L38UuUagKf

Related Packages

Version History

Version Released Laravel License
6.5.1 ^10.0|^11.0|^12.0|^13.0 MIT
6.5.0 ^10.0|^11.0|^12.0|^13.0 MIT
6.4.1 ^10.0|^11.0|^12.0|^13.0 MIT
6.4.0 ^10.0|^11.0|^12.0|^13.0 MIT
6.3.3 ^10.0|^11.0|^12.0|^13.0 MIT