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

Configuration

The published config/forms.php lets you swap the package models, register field resolvers, map field types to value types, and tune uploads and reviews. In full:

return [
    // Override any model with your own subclass.
    'models' => [
        'form' => \RoundlyConsulting\Forms\Models\Form::class,
        'group' => \RoundlyConsulting\Forms\Models\Group::class,
        'field' => \RoundlyConsulting\Forms\Models\Field::class,
        'submission' => \RoundlyConsulting\Forms\Models\Submission::class,
        'form_submission' => \RoundlyConsulting\Forms\Models\FormSubmission::class,
    ],

    // Key type of the polymorphic sender columns: "bigint", "uuid" or "ulid" — anything else throws.
    'key_type' => env('FORMS_KEY_TYPE', 'bigint'),

    // Map a field `type` to the resolver that reads/writes its value.
    // `default` is used for any type without an explicit mapping. Each value must be
    // a class implementing Resolver; anything else throws.
    'fields' => [
        'default' => \RoundlyConsulting\Forms\Resolvers\DefaultResolver::class,
        'file' => \RoundlyConsulting\Forms\Resolvers\MediaFileResolver::class,
        'image' => \RoundlyConsulting\Forms\Resolvers\MediaFileResolver::class,
    ],

    // Map a field `type` to the attributes AttributeType used to cast stored
    // values and type-check submissions. Unlisted types (e.g. `time`) read back
    // exactly as stored. A value that isn't an AttributeType case throws.
    'field_types' => [
        'number' => 'integer',   // whole numbers; `decimal` / `float` map to 'float'
        'range' => 'integer',
        'float' => 'float',
        'decimal' => 'float',
        'checkbox' => 'boolean',
        'boolean' => 'boolean',
        'toggle' => 'boolean',
        'date' => 'datetime',
        'datetime' => 'datetime',
        'multiselect' => 'array',
        'checkboxes' => 'array',
        'tags' => 'array',
    ],

    // Media settings for file/image fields (backed by media-library-for-laravel).
    'media' => [
        'bucket' => 'attachment',
        'visibility' => 'private',
        'disk' => env('FORMS_MEDIA_DISK'),
        'private_disk' => env('FORMS_MEDIA_PRIVATE_DISK', 'local'),
        'accepted_mime_types' => null,
        'max_file_size' => null,
        'responsive_widths' => null,
        'temporary_url_lifetime' => null,
    ],

    // Submission review via approvals-for-laravel. Disabled by default.
    'approvals' => [
        'enabled' => env('FORMS_APPROVALS_ENABLED', false),
    ],

    // Forms defined declaratively and synced to the DB with `php artisan forms:sync`.
    'definitions' => [],
];

Every key

KeyDefaultPurpose
models.formModels\FormModel used for forms (the package model or a subclass; any other class throws).
models.groupModels\GroupModel used for groups.
models.fieldModels\FieldModel used for fields.
models.submissionModels\SubmissionModel used for per-field submission rows.
models.form_submissionModels\FormSubmissionAggregate grouping a submission’s rows (the approvals subject).
key_typebigintKey type of the polymorphic sender columns: bigint, uuid or ulid (FORMS_KEY_TYPE); blank = not set → bigint; any other value throws.
fields.defaultDefaultResolverResolver for any field type without its own mapping. Every fields.* entry must be a Resolver class; a blank entry is unmapped; anything else throws.
fields.file / fields.imageMediaFileResolverStores the upload as media on the submission row.
field_types12 mappingsField type → AttributeType for typed reads and validation. time is left unmapped on purpose and reads back as stored. An unmapped type — or one mapped to a blank value — reads as a string; a mapped value that isn’t an AttributeType (integr) throws.
media.bucketattachmentMedia bucket the submission row registers uploads into (a string; blank = not set → attachment).
media.visibilityprivateprivate (signed streaming) or public; anything else throws.
media.disknullDisk for every upload (FORMS_MEDIA_DISK); null or blank = by visibility — private → media.private_disk, public → the media library default.
media.private_disklocalNon-public disk for private uploads and their variants when media.disk is null (FORMS_MEDIA_PRIVATE_DISK).
media.accepted_mime_typesnullRestrict accepted mime types to a list of non-empty strings (null or [] = open).
media.max_file_sizenullMax upload size in bytes, at least 1 (null = media default).
media.responsive_widthsnullResponsive image widths, positive integers (null = media default ladder).
media.temporary_url_lifetimenullSigned URL lifetime in minutes, at least 1 (null = media default).
approvals.enabledfalseRoute submissions through the approvals engine (FORMS_APPROVALS_ENABLED). Read strictly: true/1/on/yes or false/0/off/no; anything else throws.
definitions[]Declarative form definitions synced by forms:sync (each entry an array).

Environment

Three settings are env-driven, so the most common changes need no published config:

FORMS_KEY_TYPE=bigint
FORMS_MEDIA_PRIVATE_DISK=s3-private   # a non-public disk for private uploads
# FORMS_MEDIA_DISK=                   # leave unset to choose the disk by visibility
FORMS_APPROVALS_ENABLED=true

Set key_type before you migrate: it decides the column type of the polymorphic sender columns on submissions and form_submissions, and every model you use as a sender must share that key type. A blank value (FORMS_KEY_TYPE=) is not set and keeps bigint; any value other than bigint, uuid or ulid throws an InvalidConfigurationException.

Strict settings

A key that is not set — absent, null, or blank like a host’s FORMS_MEDIA_DISK= — takes its default; an optional one (media.disk, media.max_file_size, media.responsive_widths, media.temporary_url_lifetime) stays unset, and a blank fields.* or field_types.* entry is unmapped. Every value that is set must fit, or it throws RoundlyConsulting\PackageToolkit\Exceptions\InvalidConfigurationException naming the key:

  • Integers accept an int or a plain integer string — every env value is a string — so 10MB or 5.5 throws rather than being ignored.
  • A non-string bucket or disk name throws, and so does a media.visibility typo — it no longer reads as private.
  • A junk list entry throws — accepted_mime_types, responsive_widths or a definitions entry that isn’t an array.
  • Every fields.* entry must be a Resolver class, and every field_types value an AttributeType case.
  • FORMS_APPROVALS_ENABLED accepts true/1/on/yes or false/0/off/no; anything else throws.

php artisan about renders a broken setting as INVALID instead of failing.

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.