jameslkingsley/nova-media-library-field
A Laravel Nova field for managing media library attachments, letting you upload, browse, and select existing media on your models. It integrates with Spatie's Laravel Media Library for a smooth image and file picking experience in the Nova admin panel.
Before anything else: check compatibility. This package's last release was January 2019 and it is archived. That places it in the Nova 1.x/2.x + spatie/laravel-medialibrary v7/v8 era. Open composer.json in the repo and compare its nova and laravel-medialibrary constraints against your app before you composer require — on Nova 4+ / medialibrary v10+ nothing will work without a fork.
Install the package alongside Spatie Media Library:
composer require jameslkingsley/nova-media-library-field
Nova auto-discovers the service provider; if not, register it in config/app.php.
Prepare the model (this is table stakes for Spatie, and the field adds nothing here):
class Post extends Model implements HasMedia
{
use InteractsWithMedia;
public function registerMediaCollections(): void
{
$this->addMediaCollection('cover')->singleFile();
$this->addMediaCollection('gallery');
}
}
Publish/migrate Spatie's media table and link storage:
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations"
php artisan migrate
php artisan storage:link
Add the field to your Nova resource:
public function fields(Request $request)
{
return [
MediaLibrary::make('Cover', 'cover'),
];
}
Where to look first: the package README is thin, so read src/ — the field class, its resolve()/fill() methods, and its Vue component. That tells you the exact class namespace, constructor signature (label, collection), and which fluent methods (single(), multiple(), accept(), disk()) actually exist. Do not guess method names from other media fields; they are not interchangeable.
First use case: a single cover image on a Post resource. Create a post with an image, confirm a row appears in media with collection_name = 'cover', then edit the post and replace the image.
One field per collection. The field maps 1:1 to a Spatie collection name. A model with cover and gallery gets two fields; there is no combined "all media" field.
Keep single/multiple in sync with the collection definition. The field's mode and addMediaCollection()->singleFile() must agree. A single() field on a non-single collection leaves orphaned media; a multiple() field on a singleFile() collection silently keeps only the last upload.
Create forms are the fragile path. The model doesn't exist when the file is uploaded, so the field must stash the upload and attach it after the model is saved. If you see uploads vanish on create but work fine on update:
fill()/resolve() to see where it stashes the temp path.public function registerMediaCollections(): void { /* ... */ }
// In a PostObserver::created() or Model::created()
if ($path = request('cover_temp_path')) {
$post->addMedia($path)->toMediaCollection('cover');
}
FormRequest/Resource::store().Validation. Prefer validating in the resource's rules() / a FormRequest ('cover' => ['image', 'max:2048']) over the field's own ->rules(). Nova validates before save, so the input is a temp upload — validate the upload, not the media record.
Thumbnails and conversions. Define conversions in registerMediaConversions() and point the field at one if it supports ->conversion(); otherwise Nova previews load the original file. For galleries this is the difference between a snappy form and a 40 MB page.
Index and detail views. Media is not a column, so the field is form-only in practice. For a column in index, add an accessor:
public function getCoverUrlAttribute(): ?string
{
return $this->getFirstMediaUrl('cover', 'thumb') ?: null;
}
Then use Image::make(fn () => $this->cover_url) — and eager-load with('media') in indexQuery to avoid N+1.
Metadata (alt text, captions). The field manages files, not custom properties. Pair it with a text field and persist in an updating observer on the media model (config('media-library.media_model')), or override the field's fill() in a subclass.
Never treat the field value as a model attribute. Don't read it back from $model->cover, don't ->sortable() it, don't ->resolveUsing(). Always derive URLs via getFirstMediaUrl() / getMedia().
Seeding and imports bypass Nova entirely. Use $model->addMedia($path)->toMediaCollection('cover') — the field is a UI, not an API. This also means data migrations are safe.
fields(), resolve, fill, meta) changed materially after v2; expect the Vue component and possibly the PHP field to break on Nova 3+.php artisan storage:link, APP_URL mismatch behind a proxy, or a disk mismatch between config('nova.storage_disk'), filesystems.disks, and config('media-library.disk_name'). On S3 you need temporary URLs, not url().->canSee(fn () => $request->user()->can('update', $this)) for roles that shouldn't destroy assets.file_name. Use the media id or uuid.storage/logs/laravel.log, then insert a dd() in the field's upload/store handler to confirm the request even reaches it.->rules('required') on create is a trap when the field's value is a temp path — the required rule can pass while the attach step fails. Test the create path specifically, not just update.$post->getMedia('cover'), and check media.model_type / collection_name / disk directly. Nine times out of ten the row is there with the wrong collection or disk.Storage::fake('public') + UploadedFile::fake()->image('cover.jpg'), and drive Nova via Nova::actingAs($user); assert on $post->getMedia('cover')->count() rather than on rendered markup.resolve()/fill() to attach custom properties (alt text, credits) — cheaper than a parallel metadata model.ebess/advanced-nova-media-library (actively maintained, similar feature set) or a plain Image/File field plus addMedia(...)->toMediaCollection(...) in Resource::store()/update() gives you the same result with no archived dependency.How can I help you explore Laravel packages today?