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

Advanced Nova Media Library Laravel Package

marshmallow/advanced-nova-media-library

Advanced media library for Laravel Nova. Manage images and files through a polished Nova field, organize assets in folders, and attach reusable media to your Eloquent models with conversions and responsive images. Built for developers who need more than Nova's default file field.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Verify compatibility before anything else. This is a low-adoption fork (0 stars/dependents), so read composer.json first rather than trusting the README:

composer show marshmallow/advanced-nova-media-library

Check three things:

  • The supported laravel/nova major (^4.0 vs ^5.0) and spatie/laravel-medialibrary (^10 vs ^11). A mismatch here is the single most common reason the field renders but silently fails to save.
  • The installed version. Require 5.3.2 or later, exactly — never 5.3.1. That release can permanently delete media from a collection on save (details in Gotchas). If you are already on 5.3.1, upgrade before your next deploy touches a resource with this field.
  • The namespace. Most forks keep upstream's namespace so they're drop-in replacements; some rename it. Look at autoload.psr-4 and pick whichever is real:
use Marshmallow\AdvancedNovaMediaLibrary\Fields\Images; // fork namespace
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;        // upstream, often retained

The class names (Images, Media, Files) and the fluent API (singleMedia(), croppable(), conversion()) are inherited from ebess/advanced-nova-media-library, so upstream docs and StackOverflow answers apply ~95% of the time — but not to the save-path bugfixes, which are fork-only.

2. Install the Spatie prerequisites. The Nova field is only a UI shell over Media Library — nothing works until the base package is set up:

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate
php artisan storage:link

Set the disk in config/media-library.php ('disk_name' => 'public' for local, s3 for cloud) and make sure models live on the same disk you intend to serve from.

3. Prepare the model — implement HasMedia and register collections whose names match the field's second argument:

use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;

class Post extends Model implements HasMedia
{
    use InteractsWithMedia;

    public function registerMediaCollections(): void
    {
        $this->addMediaCollection('cover')->singleFile();
        $this->addMediaCollection('gallery');
    }
}

4. Add the field to the Nova resource. Images::make(label, collectionName) — the second argument is the collection, not a database column:

public function fields(NovaRequest $request)
{
    return [
        Images::make('Cover', 'cover')
            ->singleMedia()
            ->rules('required')
            ->showOnIndex()
            ->conversion('thumb'),

        Images::make('Gallery', 'gallery')
            ->rules('max:8'),
    ];
}

First end-to-end use case: upload a cover image on a Post resource, see the thumbnail in the index table, then open the record and save it again without touching the media field. That second save is the one that exercises handleMedia() → removeDeletedMedia() → re-add, which is where 5.3.1 destroyed data and where 5.3.2 now validates. If the image survives an untouched re-save, your wiring and your package version are both correct. Then render it with $post->getFirstMediaUrl('cover', 'thumb').

Where to look first when something breaks: src/Fields/ (what's actually available in this version — and check handleMedia() first for save-path issues), config/ after php artisan vendor:publish (cropper settings, max file size), and the compiled assets in resources/js / dist if the crop UI is missing.

Implementation Patterns

One field per collection. Nova resources typically hold several independent collections rather than one omnibus field:

Images::make('Thumbnail', 'thumb')->singleMedia()->conversion('thumb'),
Images::make('Hero', 'hero')->singleMedia()->croppable(),
Images::make('Gallery', 'gallery')->croppable()->rules('max:20'),
Files::make('Downloads', 'downloads')->rules('mimes:pdf,zip'),

Single vs. multiple is a property of the collection, not just the field. ->singleMedia() on the field controls the UI, but ->singleFile() in registerMediaCollections() is what actually enforces it at the storage layer. Set both — otherwise an API write or a seeder can attach a second file and the UI will quietly show only the first.

Conversions are defined in the model, selected in the field. The field never defines image sizes; it only chooses which one to display:

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

// Nova
Images::make('Cover', 'cover')->conversion('thumb')  // used for index/detail preview

->conversion() only affects the Nova preview. Frontend code still calls getFirstMediaUrl('cover', 'large') explicitly.

Cropping workflow. ->croppable() enables the crop modal; the crop is stored as a separate conversion by default, or overwrites the original when you also call ->mustBeCropped(). Default aspect ratios and output sizes live in the package config. Practical rule: keep crops on a dedicated hero/cover collection and never on user-uploaded galleries — retroactive crop config changes will not regenerate existing files.

Validation rules attach to the uploaded files, so 'required', 'image', 'mimes:pdf', 'max:2048' (kilobytes) all behave as you'd expect. Count limits use the same max: syntax with different semantics — ->rules('max:8') on a multi-image field means at most 8 files, which is easy to confuse with a size limit. Combine them when needed: ->rules('max:8', 'max:5120').

Save-path validation is separate from rule validation (5.3.2+). Before touching stored media, handleMedia() now checks every submitted reference and throws a ValidationException on the field if any is unusable. This fires for null, '' (empty strings coerced by ConvertEmptyStringsToNull), and stale IDs. Treat it as a normal, user-facing validation error — surface the message, don't swallow it — and note the whole resource save is rejected, not just the media part:

// Nova's update runs in a DB::transaction; the exception rolls back everything.
// If a user reports "my other field edits didn't save", the media field blocked them.

Index-table display needs ->showOnIndex() plus a conversion; without a conversion you get the full-size original, which is a fast way to ship a 4 MB table row.

Reordering is drag-and-drop in the field when the collection is multi-file; the resulting order lives in the order_column on the media table, so $post->getMedia('gallery') returns them already sorted. Do not add ->sortable() to the field expecting it to sort the Nova index column by upload time — media ordering and column sorting are different concerns.

Using it from outside Nova (seeders, imports, API endpoints) — skip the field entirely and use Spatie directly:

$post->addMediaFromUrl($url)->toMediaCollection('cover');
$post->addMedia($path)->withCustomProperties(['alt' => $alt])->toMediaCollection('gallery');

This also sidesteps the handleMedia()/removeDeletedMedia() path entirely, which is worth preferring for anything that writes media programmatically.

Custom properties / alt text pass through the field with ->customProperties([...]) (static) or by resolving from the media model; the package surfaces them in the crop/edit UI. Retrieve with $media->getCustomProperty('alt') in Blade.

Reusable field presets. Because the fields are fluent, define helper methods on a base resource or a small trait to keep resources consistent:

protected function coverField(): Images
{
    return Images::make('Cover', 'cover')
        ->singleMedia()
        ->croppable()
        ->conversion('thumb')
        ->showOnIndex()
        ->rules('required');
}

Integration tip — Actions and custom tools. Anything that manipulates media should reuse the model's collection definitions rather than hardcoding collection strings. If you're copying media between models in a Nova action, use $source->getFirstMedia('cover') and $media->copy($target, 'cover') to avoid re-uploading bytes.

Gotchas and Tips

5.3.1 silently deletes media. Never install it; upgrade off it immediately. 5.3.1 changed addExistingMedia() to skip media references that no longer resolve instead of fataling. But handleMedia() calls removeDeletedMedia() first, and an unresolvable value is also missing from the ID list that method preserves — so the existing collection was emptied and nothing was added back, and the save committed. Before 5.3.1 the fatal propagated out of Nova's DB::transaction and rolled everything back: a 500, but no loss. 5.3.1 turned that into permanent data loss — it took down a production homepage within minutes of release.

The four inputs, verified against production data through handleMedia():

input 5.3.1 5.3.2
null no error, collection emptied ValidationException, media intact
stale id no error, collection emptied ValidationException, media intact
'' no error, survived ValidationException, media intact
valid id passes passes

If you or a client ran 5.3.1, audit the media table for gaps — compare media rows against backups or CDN/object-storage listings for affected collections. Recovery is only possible from backups or the original upload source; the package cannot restore them.

Long-lived forms are the stale-ID trigger. A Nova edit page left open in a tab while someone else (or a queued job) removes media will submit IDs that no longer exist. Under 5.3.2 the user gets a validation message and can retry; under 5.3.1 they'd have wiped the collection. Same applies to null/'' arriving from partial form posts, API calls hitting the field, or custom frontends that submit the raw field value — the middleware ConvertEmptyStringsToNull is what turns '' into null mid-flight.

Don't subclass around handleMedia() without knowing its order. 5.3.2's guarantee is that validation happens before removeDeletedMedia(). If you override fill()/handleMedia() for bespoke persistence, keep every destructive step after the validation call, or you reintroduce the 5.3.1 class of bug in your own code.

The field name is not a column. Images::make('Cover', 'cover') writes to the media table, not to a cover column on your model. Adding a migration column for it is a common first mistake; so is accidentally naming the field argument the same as an existing attribute, which makes Nova try to hydrate it and throw confusing "column not found" errors on save.

Missing storage:link is the #1 "images don't appear" cause. The record saves fine, the media row exists, but the URL 404s. Run php artisan storage:link and double-check APP_URL — media URLs are absolute and break behind a proxy or on a different domain than the one you're browsing.

Disk mismatch between upload and serve. If media-library.disk_name is s3 but the Nova preview uses a local URL (or vice versa), you get broken thumbnails that only fail in production. Verify with $media->getDiskDriverName() and $media->getUrl() in tinker.

Cropper UI missing or non-functional almost always means assets weren't published/built for this fork. Look for a publish tag in the service provider (php artisan vendor:publish with no tag will list what's available) and check whether the package expects npm run build in the host app or ships precompiled dist/ assets.

->rules('required') on edit forms. Verify behavior in your version: some builds re-inject existing media into the request so required passes on update, others treat it as "no new files selected" and block saving. Test the edit-without-reupload path explicitly — on 5.3.2 the failure mode there is a field-level ValidationException, which is at least actionable, but it still blocks the save. If it trips, drop required and enforce presence with Rule::requiredIf(fn () => ! $model->hasMedia('cover')) on the underlying request.

Switching to ->singleMedia() leaves orphans. If the collection previously accepted many files, only the first is shown; the rest remain attached and will reappear if you revert the flag. Clean up with $model->clearMediaCollectionExcept('cover', $keep) in a one-off migration.

Abandoned uploads accumulate. Files uploaded into a form that's never submitted land in the temporary directory. Spatie's php artisan medialibrary:clean-temporary-uploads (or the equivalent command in your version) belongs in the scheduler.

Upper limits live in three places and all must agree: the field's ->rules('max:...'), the package config's max file size, and PHP's upload_max_filesize / post_max_size. When the second and third are too low, the failure is a bare 413 or a validation message with no field attached — check storage/logs/laravel.log and the browser network tab before touching the field definition.

Image processing jobs. Conversions marked ->nonQueued() render instantly but block the save; queued conversions need a running worker, and the Nova preview will show the original until the worker catches up. If thumbnails appear "late" or only after a refresh, that's a queue, not a bug in the field.

Debugging ladder, in order: (1) $model->getMedia('collection')->count() in tinker — if 0, the problem is registration or the save path, not the UI; (2) inspect the media table row for collection_name, disk, file_name; (3) $media->getUrl() vs. the rendered <img src>; (4) storage/logs/laravel.log for FileNotFoundException / DiskNotFoundException / ValidationException on the media field. For anything that looks like disappearing media, diff the media rows against your last backup before doing anything else — 5.3.1's damage is silent and already committed.

Extension points when upstream behavior doesn't fit: subclass the field and override fill()/resolve() for bespoke persistence (keeping destructive steps last); register a custom Media model via media-library.media_model to add accessors (e.g. an alt property); or bypass the field entirely for programmatic flows and use Spatie's API — the field is a thin wrapper, and fighting it is usually slower than not using it for that code path.

Pin the version — exactly, and to a known-good patch. With a single maintainer and no dependents, treat the package as unvetted for your stack. "marshmallow/advanced-nova-media-library": "5.3.2" (not ^5.3, which can resolve to the next regression), commit the lock file, and keep a note of the upstream ebess/advanced-nova-media-library equivalent so you can swap back if the fork stalls on a future Nova major. This release argues both ways: it shows the maintainer tests against production data and ships fast fixes, but it also shows a save-path regression reached a release and destroyed data — so pin hard, read release notes before bumping, and re-run the "save without touching the media field" check after every upgrade. If the fork diverges only in Nova-version support, upstream is the better long-term bet; if it carries fixes you need, vendor-patch rather than waiting on a release.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console