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 File Upload Field Laravel Package

genealabs/nova-file-upload-field

A Laravel Nova field that adds a flexible file upload input to your resources, with support for multiple files, storage disks, and customizable validation. Drop it into any Nova resource to replace the default file field.

View on GitHub
Deep Wiki
Context7

Getting Started

Treat this package as a fork-first dependency, not a composer require-and-go. The repo is archived (last release 0.6.0, 2024-05-11, Laravel 11 support), has zero dependents, and makes no claim about Nova 5 / Laravel 12. Before writing a line of field config, get a branch you control.

1. Get the code under your control. Fork to your org, or vendor it in via a VCS/path repository so you can patch the Vue component when Nova's internals move:

"repositories": [
    { "type": "vcs", "url": "[email protected]:yourorg/nova-file-upload-field.git" }
]
composer require genealabs/nova-file-upload-field:dev-main

The package depends on the private, license-gated laravel/nova package, so Composer resolution only succeeds on a machine/CI runner that already has the Nova repository configured with credentials (auth.json or COMPOSER_AUTH). If composer require fails with "could not find package laravel/nova", that is a credentials problem, not a code problem.

2. Migrate the columns. The field persists a disk-relative path string — plan a nullable string for single-file mode, and text/json if you enable multiple files on one attribute (confirm what the field actually writes for your mode before finalising the migration; this is the first thing to verify in the spike).

Schema::table('documents', function (Blueprint $table) {
    $table->string('attachment')->nullable();
    $table->string('attachment_original_name')->nullable();
});

3. Wire it into a resource.

use GeneaLabs\NovaFileUploadField\FileUpload;

FileUpload::make('Attachment')
    ->disk('private')          // the whole reason to be here
    ->path('documents')        // custom prefix instead of the hardcoded nova/ prefix
    ->storeOriginalName('attachment_original_name')
    ->acceptedTypes(['application/pdf', 'image/*'])
    ->maxSize(10240)           // KB
    ->rules('required', 'max:10240')
    ->hideFromIndex();

First use case to reach for: an internal admin resource that must store files on a private, non-public disk with a custom path — the genuine gap versus Nova's built-in File/Image fields, which historically assumed the public disk and a nova/ prefix. If your requirement is "an upload field on the public disk", stop and use the native field; it inherits Nova's own compatibility guarantees for free.

Where to look first in the repo: src/FileUpload.php for the fluent API surface (disk(), path(), acceptedTypes(), maxSize(), storeOriginalName(), prunable()), the service provider for route registration, and the compiled Vue component to confirm whether validation is mirrored client-side only.

Implementation Patterns

Single-file vs. multi-file are different data shapes. Decide before you migrate. Single-file: one nullable string column, trivially diffable, trivially replaceable. Multi-file on one attribute: the field serialises an array, so you need a JSON/text column plus a cast, and you must decide whether the resource stores raw paths or a {path, original_name} structure. Multi-file is where in-house replacement later gets expensive — model that cost now.

Wrap it behind your own field class. This is the single highest-leverage pattern given the archived status. Subclass once, use your class everywhere in resources, and the eventual swap to a maintained field or an in-house implementation becomes a one-file change:

namespace App\Nova\Fields;

class PrivateFile extends \GeneaLabs\NovaFileUploadField\FileUpload
{
    public function __construct($name, $attribute = null, $resolveCallback = null)
    {
        parent::__construct($name, $attribute, $resolveCallback);

        $this->disk(config('filesystems.private_disk'))
            ->path('uploads')
            ->rules('file');
    }
}

Configure the disk, don't configure the field. The package correctly delegates storage policy to Laravel, so keep the field thin: filesystems.disks.private owns root, driver, and visibility. Never rely on the field to set ACLs — set 'visibility' => 'private' on the disk and verify with a real object URL that an unauthenticated request 403s.

Validate twice, trust once. acceptedTypes() and maxSize() are UX affordances surfaced through FilePond and should be treated as advisory only. The security boundary is server-side: pair the field with explicit ->rules() (MIME type, size, extension), and if the content is sensitive, add an explicit content-sniff check (e.g. mimes: plus a finfo/getimagesize check for images). Client-side validation in a compiled Vue component is not an allowlist.

Combine with Nova's own field options rather than fighting them: ->rules(), ->nullable(), ->hideFromIndex(), ->hideWhenCreating(), and the policy methods (canSee, canUpdate) all compose normally because it is a plain Field subclass.

Make deletion explicit. Do not assume deleting the model removes the file. Write a model deleting hook or an observer that deletes the stored object(s), and test it:

static::deleting(function (Document $doc) {
    if ($doc->attachment) {
        Storage::disk($doc->attachment_disk)->delete($doc->attachment);
    }
});

Nova's prunable() operates on model records, not on arbitrary disk objects — confirm in your environment whether it actually cleans up orphaned uploads before you count on it. At any volume, orphans cost money and are hard to audit later.

Test the three paths that actually break. A Nova feature test should cover: (1) a successful upload lands on the expected disk/path, (2) an oversized or wrong-MIME upload is rejected server-side, (3) deleting the record removes the object. Everything else is UI polish.

For migration away: the data migration is cheap by design. The column holds a disk-relative path, so moving to a maintained media library or an in-house field is a column re-point plus a storage mapping exercise, not a schema rewrite. Keep the column and the disk contract decoupled from the field class and you keep that exit cheap.

Gotchas and Tips

The failure mode is silent. A stale compiled Vue component against a newer Nova typically fails as a blank, non-reactive, or invisible field in the browser — not a build error, not a boot error, not a red CI run. When a field "just doesn't show up" after a Nova or Laravel upgrade, suspect the package before you suspect your resource. Check the browser console, then re-publish assets (php artisan nova:publish) and hard-reload.

Composer resolution is a ticket, not a bug. laravel/nova is private and license-gated. Builds fail on fresh CI runners and new laptops that lack Nova credentials in auth.json/COMPOSER_AUTH. Bake that into onboarding docs or your build image.

Server-proxied uploads hit PHP and proxy limits before they hit the field. This package uploads through the app, so upload_max_filesize, post_max_size, PHP max_file_uploads, and any nginx/ALB client_max_body_size all apply. Symptom: a FilePond upload that sits at 100% and then returns a 413, or a ValidationException with no message. If you need presigned S3 direct-to-storage for large files or volume, this is the wrong tool — there is no direct-to-storage path here.

Visibility mismatch is the realistic worst case. A disk that looks private but resolves objects publicly (a bucket policy, a CDN in front, visibility unset) is a data-exposure incident with no upstream maintainer to blame. Assert the ACL explicitly in a test, not just in config.

Multi-file mode changes the column contract. If you later switch single→multiple (or back), you are doing a data migration, not a config change. Pick once and document it in the resource.

storeOriginalName() needs its own column and is not a filesystem operation — it persists the client-supplied filename alongside the object key. Treat that string as untrusted user input wherever you render it (sanitise, escape, never interpolate into a shell path).

Extension points are narrow, which is fine. You can subclass the PHP field, override the fluent defaults, and swap the compiled Vue component in your fork. There is no plugin/middleware surface. If your requirement is image transformations, variants, virus scanning, signed URLs, or i18n — this package will not grow those features; upstream is archived.

Pin and own it or don't take it. If this ships, pin the exact version, name an owner, write a runbook covering disk config and upload limits, and open the replacement ticket the same day you approve the adoption. A dev-main constraint on an archived repo is how you get a Friday-afternoon surprise.

Last check before committing: has anyone verified Nova 5 and PHP 8.3/8.4 in your own environment? There is no validated compatibility matrix and no community evidence to lean on. Timebox a spike with a written pass/fail criterion — if it does not pass cleanly, Nova's native File field behind a thin internal wrapper is the lower-risk answer, and this repo is better read as a reference implementation of the Nova field API than adopted as a dependency.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin