NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Forms for Laravel

File uploads

Map a field to the file or image type and the MediaFileResolver attaches the upload as media on the submission row — image variants for images, passthrough for other files, served back via signed or temporary URLs (private by default). Define the field:

Forms::define('kyc', 'Identity check')
    ->public()
    ->group('documents', 'Documents', function (GroupBuilder $g): void {
        $g->field('passport', 'Passport')->file()->rules(['required', 'file', 'max:10240']);
    })
    ->create();

The upload input uses the same nested path as any other field — remember the multipart encoding:

<form method="POST" action="{{ route('kyc.store') }}" enctype="multipart/form-data">
    @csrf
    <input type="file" name="kyc[documents][passport]">
    <button type="submit">Upload</button>
</form>

Reading attachments

$g->field('passport', 'Passport')->file();

// the submission row is the media owner:
$row = Forms::find('kyc')->fields->firstWhere('key', 'passport')->submissions->first();
$row->attachments();               // Collection<Media>
$row->attachmentUrl();             // first attachment's URL: signed if private, public if public
$row->attachmentTemporaryUrl();    // always a short-lived signed URL

// or through the resolver:
$resolver = Forms::find('kyc')->fields->firstWhere('key', 'passport')->resolver();
$resolver->fromStorage();          // the stored media UUID
$resolver->url();                  // same as attachmentUrl()

The row stores the media UUID as its value, so fromStorage() and the reader return that UUID. More helpers on the row and the resolver:

$row->hasAttachments();            // bool
$row->attachmentUrls();            // list<string> — every attachment
$row->attachmentBucket();          // 'attachment' (config forms.media.bucket)

$resolver->temporaryUrl();         // short-lived signed URL via the resolver
$resolver->url($user);             // scope the lookup to one sender

Upload settings

Everything lives under forms.media — bucket, visibility, disk, mime allowlist, size limit, responsive widths and signed-URL lifetime:

// config/forms.php
'media' => [
    'bucket' => 'attachment',
    'visibility' => 'private',                 // or 'public'
    'disk' => env('FORMS_MEDIA_DISK'),         // null = by visibility (see private_disk)
    'private_disk' => env('FORMS_MEDIA_PRIVATE_DISK', 'local'), // private uploads, never web-served
    'accepted_mime_types' => ['application/pdf', 'image/jpeg', 'image/png'],
    'max_file_size' => 10 * 1024 * 1024,       // bytes
    'responsive_widths' => [480, 960],         // null = the media default ladder
    'temporary_url_lifetime' => 10,            // minutes; null = the media default
],

Force-deleting a submission row purges its attachment media, so hard deletes don’t orphan stored files. Soft deletes keep the files.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.