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

Nova Medialibrary Field Laravel Package

dmitrybubyakin/nova-medialibrary-field

A Laravel Nova field that integrates Spatie's Laravel Media Library, letting you upload, attach, and manage media files directly within Nova resources. Supports multiple collections, image conversions, and a clean, intuitive UI for faster admin workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin Nova adapter over spatie/laravel-medialibrary. Most behavior you will debug day-to-day is Spatie behavior, so learn the media library collections/conversions API first, then treat this field as the Nova UI for it.

Prerequisites

  • Laravel + Nova application
  • spatie/laravel-medialibrary installed and migrated
  • A model implementing Spatie\MediaLibrary\HasMedia
  • A Nova resource for that model

Check the package's composer.json against your Nova major before installing; Nova 4/5 compatibility is the first thing to verify.

Install

composer require dmitrybubyakin/nova-medialibrary-field

Install and migrate Spatie's media library if you have not already:

composer require spatie/laravel-medialibrary

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

php artisan migrate

Publish the package config only if you need to change defaults; otherwise the package defaults are usually fine:

php artisan vendor:publish
# select the nova-medialibrary-field provider

Model setup

Register the media collections exactly as Spatie expects. The collection name is what the Nova field will target.

use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class Post extends Model implements HasMedia
{
    use InteractsWithMedia;

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

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

Nova field

use DmitryBubyakin\NovaMedialibraryField\Fields\Medialibrary;

public function fields(NovaRequest $request)
{
    return [
        Medialibrary::make('Cover', 'cover')
            ->single()
            ->accept('image/*')
            ->preview()
            ->conversion('thumb'),

        Medialibrary::make('Gallery', 'gallery')
            ->multiple()
            ->sortable()
            ->accept('image/*')
            ->preview()
            ->conversion('thumb')
            ->croppable(),
    ];
}

First use case

Start with a single cover image on an existing resource:

  1. Add the cover collection with singleFile().
  2. Add Medialibrary::make('Cover', 'cover')->single()->accept('image/*')->preview().
  3. Save a record and upload an image.
  4. Render it:
<img src="{{ $post->getFirstMediaUrl('cover', 'thumb') }}" alt="">

Where to look first when something does not work:

  • The package README for the exact field methods supported by your installed tag.
  • config/media-library.php for disk, conversions, queueing, and URL generation.
  • The model's registerMediaCollections() / registerMediaConversions().
  • The media table row for the uploaded item: collection name, disk, custom_properties, generated_conversions.

Implementation Patterns

One field per collection

The second argument to Medialibrary::make() is the media collection name. Use separate fields for separate collections rather than trying to overload one collection with different semantics.

Medialibrary::make('Hero Image', 'hero')->single()->accept('image/*')->preview(),
Medialibrary::make('Downloads', 'downloads')->multiple()->accept('application/pdf'),

Single-file vs multi-file collections

Match the field mode to the Spatie collection definition:

// Collection
$this->addMediaCollection('avatar')->singleFile();

// Nova
Medialibrary::make('Avatar', 'avatar')->single()->accept('image/*')->preview(),

For galleries:

$this->addMediaCollection('gallery');
Medialibrary::make('Gallery', 'gallery')
    ->multiple()
    ->sortable()
    ->accept('image/*')
    ->preview()
    ->conversion('thumb'),

Use ->sortable() when editors need ordering. Spatie's order_column is the backing store.

Conversions and previews

Define conversions in Spatie, then point the field at the conversion used for display:

$this->addMediaConversion('thumb')
    ->width(300)
    ->height(300)
    ->sharpen(10);
Medialibrary::make('Images', 'images')
    ->multiple()
    ->preview()
    ->conversion('thumb'),

Keep conversion names stable. Renaming a conversion does not automatically regenerate existing media.

Cropping

Cropping is useful for hero images, avatars, and card thumbnails, but it depends on image conversions being configured correctly. Typical pattern:

Medialibrary::make('Cover', 'cover')
    ->single()
    ->accept('image/*')
    ->croppable()
    ->preview()
    ->conversion('thumb'),

If cropping is central to the editorial workflow, test it with the actual image driver used in production (gd vs imagick) and with the actual disk (public vs S3).

Custom metadata on media

If your version exposes per-media custom fields, use the field's fields() extension point to attach Nova fields to each media item (for example alt text or credits). This stores data in the media model's custom_properties JSON column.

Medialibrary::make('Gallery', 'gallery')
    ->multiple()
    ->fields(function () {
        return [
            \Laravel\Nova\Fields\Text::make('Alt text', 'alt'),
        ];
    }),

Treat this as an extension point and verify the exact signature in your installed version. If custom fields do not persist, inspect the custom_properties column and the media model's fillable/casts.

Disk and storage strategy

Let Spatie own the disk configuration:

// config/media-library.php
'disk_name' => env('MEDIA_DISK', 'public'),

For local development:

php artisan storage:link

For S3 or private storage, confirm that the Nova preview can generate a usable URL. Private disks may require temporary URLs, and the field's preview may behave differently than on a public disk.

Frontend retrieval

The Nova field is only the admin UI. Your application still uses Spatie's API:

$coverUrl = $post->getFirstMediaUrl('cover', 'thumb');

$gallery = $post->getMedia('gallery');

foreach ($gallery as $media) {
    echo $media->getUrl('thumb');
}

For responsive images or srcset, use Spatie's conversion URLs directly rather than relying on Nova previews.

Authorization and visibility

Since this is a standard Nova field, use Nova's visibility and authorization methods:

Medialibrary::make('Internal Assets', 'internal_assets')
    ->multiple()
    ->canSee(fn ($request) => $request->user()->isAdmin())
    ->canUpdate(fn ($request) => $request->user()->can('update', $this->resource)),

Do not rely on accept() for security. Treat it as browser/UI guidance only.

Queued conversions

For large images or slow storage, queue conversions:

// config/media-library.php
'queue_conversions_by_default' => true,

Then run a queue worker in every environment where uploads happen. If conversions are queued, the Nova preview may initially show the original until the conversion finishes.

Lifecycle hooks

Use Spatie media events for post-processing, optimization, or external sync:

  • MediaHasBeenAdded
  • MediaHasBeenDeleted
  • conversion generated events

This keeps the Nova field thin and moves business logic into listeners or observers.

Gotchas and Tips

Collection name mismatch is the most common failure

If uploads appear to succeed but nothing shows up, verify that:

  • The second argument to Medialibrary::make() matches the collection name exactly.
  • The collection is registered in registerMediaCollections().
  • The model uses InteractsWithMedia.
  • The media table row has the expected collection_name, model_type, and model_id.

single() on the field is not the same as singleFile() on the collection

Use both for true single-file semantics:

$this->addMediaCollection('cover')->singleFile();
Medialibrary::make('Cover', 'cover')->single(),

Without singleFile(), replacements can accumulate media records even if the UI presents a single slot.

accept() is not validation

The accept attribute is a browser hint. Users can bypass it, and API requests definitely can. Add server-side validation for MIME type, size, dimensions, or media count if those constraints matter. Also check PHP limits:

  • upload_max_filesize
  • post_max_size
  • max_file_uploads
  • web server body size limits

Large uploads often fail before Laravel or Nova sees the request.

Conversions must exist before the field references them

If ->conversion('thumb') points to a conversion that is not defined in registerMediaConversions(), previews will fall back to the original or appear broken. Define the conversion first, upload a fresh image, and confirm the generated_conversions JSON on the media row.

Changing conversions does not backfill old media

After changing widths, formats, or crop behavior, regenerate:

php artisan medialibrary:regenerate

Scope it to a model or IDs where possible. Regeneration can be slow and storage-heavy on large libraries.

Queued conversions can look like broken previews

If conversions are queued and the worker is not running, the original file exists but the thumb does not. Check:

  • queue connection
  • failed jobs
  • generated_conversions on the media record
  • whether the conversion was defined as nonQueued()

Cropping depends on the image driver and conversion setup

If the crop UI does not appear or crops do not apply:

  • Confirm the file is detected as an image.
  • Confirm a conversion is configured.
  • Check that GD or Imagick is installed and configured.
  • Test with the same disk used in production, especially S3.

Sorting requires the order column

->sortable() depends on Spatie's order_column. If you manually created or modified the media table, verify the column exists. If ordering behaves oddly, inspect order_column values directly.

Private disks and URLs

On S3 or other private disks, getUrl() may not be publicly accessible. Use temporary URLs where appropriate. Nova previews may work locally but fail in production if the disk visibility or URL signing is wrong.

Deleting and replacing media

Decide explicitly what happens when a user replaces an image:

  • Does the old media record get deleted?
  • Are old conversion files cleaned up?
  • Are orphaned files removed by a scheduled command?

Spatie provides APIs such as clearMediaCollection(), but replacement behavior depends on collection configuration and your own logic. Test replacement flows, not just first upload.

Custom fields need the right media model and casts

If per-media custom fields do not persist:

  • Inspect custom_properties on the media row.
  • Confirm the media model is the one configured by Spatie.
  • Check that the field's fields() callback is supported by your installed package version.
  • Verify casts and fillable settings if you use a custom media model.

Config caching

After changing package or media-library config:

php artisan config:clear
# or, if you deploy with cached config
php artisan config:cache

A stale config cache can make disk or queue changes appear to have no effect.

Nova and package version compatibility

This package has a modest ecosystem. Pin a tag that you have tested against your Nova major and Laravel version. Before upgrading Nova, check the package's composer.json constraints and recent releases. Because dependent count is low, edge cases may not be widely documented; reading the field source is often faster than searching for answers.

Test uploads with fakes

Use Laravel's storage fake and Spatie's testing helpers in feature tests:

Storage::fake('public');

$file = UploadedFile::fake()->image('cover.jpg');

// submit Nova request or call the media API directly

For Nova-specific tests, use Nova's testing helpers and assert the media collection and generated conversions rather than only asserting the HTTP response.

Keep business logic out of the field

The field is an admin adapter. Put image optimization, filename normalization, external CDN sync, and conversion policy into Spatie events, observers, or dedicated services. That keeps the Nova layer replaceable and makes the behavior testable outside the admin panel.

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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource