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

Form structure

A form has groups — think steps or sections — and each group has fields. Every record carries a key that is unique within its parent among live records (form keys are globally unique; a soft-deleted record frees its key). You can create the structure with the models directly:

use RoundlyConsulting\Forms\Models\Form;
use RoundlyConsulting\Forms\Models\Group;
use RoundlyConsulting\Forms\Models\Field;

$form = Form::create([
    'name' => 'My Form',
    'key' => 'my-form',
    'is_public' => true,        // optional, defaults to false
    'expires_at' => null,       // optional nullable datetime
]);

$group = Group::create([
    'form_id' => $form->getKey(),
    'name' => 'First Step',
    'key' => 'first-step',
    'order' => 0,
]);

$field = Field::create([
    'form_id' => $form->getKey(),
    'group_id' => $group->getKey(),
    'name' => 'Name',
    'key' => 'name',
    'help' => 'Enter your name',
    'type' => 'text',
    'validations' => ['required', 'string'],
    'options' => null,          // e.g. ['CZ' => 'Czechia', 'SK' => 'Slovakia']
    'autofill' => null,
    'order' => 0,
]);

Field paths and request shape

Each field exposes a dotted path() built from form.key, group.key and field.key (e.g. my-form.first-step.name). That path is the request key the resolver reads, so your HTML inputs are nested per form and group:

<form method="POST" action="{{ route('contact.store') }}">
    @csrf
    <input name="contact[details][name]">
    <input name="contact[details][email]" type="email">
    <textarea name="contact[message][body]"></textarea>
    <button type="submit">Send</button>
</form>

Relations and helpers

$field->path();             // 'my-form.first-step.name' — the request key

$form->groups;               // HasMany<Group>
$form->fields;               // HasManyThrough<Field> (through groups)
$form->submissions;          // HasMany<Submission> — one row per field per submission
$group->fields;              // HasMany<Field>
$field->form;                // BelongsTo<Form>
$field->group;               // BelongsTo<Group>

$form->isExpired();              // expires_at has passed
$form->isAcceptingSubmissions(); // is_public and not expired
$field->isRequired();            // has a plain 'required' rule (required_if & co. don't count)
$field->hasOptions();            // options set and non-empty
$field->hasValidations();        // validations set and non-empty
$field->resolver();              // the Resolver mapped to the field's type

Field columns options, validations, conditions and messages are cast to arrays. For anything beyond a one-off, prefer the fluent builder in the next section — it creates the whole structure in one transaction and assigns every order for you.

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.