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 senderUpload 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 cryptoBy 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.