Fluent form builder
The Forms facade exposes a fluent builder that defines a whole form — groups and fields — in one transactional call, auto-assigning each order by declaration sequence:
use RoundlyConsulting\Forms\Facades\Forms;
use RoundlyConsulting\Forms\GroupBuilder;
$form = Forms::define('contact', 'Contact us')
->public()
->expiresAt(now()->addDays(30))
->group('details', 'Your details', function (GroupBuilder $g): void {
$g->field('name', 'Name')->rules(['required', 'string'])->help('Full name');
$g->field('email', 'Email')->type('email')->rules(['required', 'email']);
})
->group('message', 'Message', function (GroupBuilder $g): void {
$g->field('body', 'Message')->type('textarea')->rules(['required']);
})
->create(); // returns the Form, persists the structure, dispatches FormCreatedOn the form builder: public(bool $public = true), expiresAt(CarbonInterface), group($key, $name, $callback) and the terminal create(). Inside the callback, GroupBuilder::field($key, $name) returns a FieldBuilder. New fields default to the text type.
Typed field shortcuts
Thin sugar over type()/rules()/options() for the common field kinds:
$g->field('email', 'Email')->email()->required();
$g->field('bio', 'Bio')->textarea();
$g->field('terms', 'Terms')->checkbox(); // type=checkbox, rule=boolean
$g->field('age', 'Age')->number(); // type=number, rule=numeric; whole numbers (3.5 fails)
$g->field('dob', 'DOB')->date(); // type=date, rule=date
$g->field('role', 'Role')->select(['a' => 'A', 'b' => 'B']);
$g->field('avatar', 'Avatar')->file(); // type=file (media-backed upload)Every field builder method
| Method | Effect |
|---|---|
type(string) | Sets the field type (default text). |
help(string) | Help text shown with the field. |
options(array) | Choice options, e.g. ['CZ' => 'Czechia']. |
rules(array, array $messages = []) | Laravel validation rules, plus optional per-rule messages. |
messages(array) | Per-rule messages, merged with existing ones. |
required() | Adds the required rule. |
autofill(string) | An Autofill class name or a literal default. |
visibleWhen(field, value, op) | Show the field only when the condition matches; field is a key or group_key.field_key. |
requiredWhen(field, value, op) | Show and require the field only when the condition matches. |
order(int) | Override the auto-assigned position — 0 included. |
email() | type email + email rule. |
textarea() | type textarea. |
checkbox() | type checkbox + boolean rule. |
number() | type number + numeric rule; whole numbers only (3.5 fails). |
date() | type date + date rule. |
select(array) | type select + options. |
file() | type file — a media-backed upload. |
Custom validation messages
Pass per-field messages as the second argument to rules(), or via messages(). They’re stored on the field and applied by the validator, keyed by rule name:
$g->field('email', 'Email')->rules(['required', 'email'], [
'required' => 'We really need your email.',
]);
// or add messages separately — they merge with any passed to rules()
$g->field('age', 'Age')
->number()
->required()
->messages(['numeric' => 'Please enter your age as a number.']);Overriding order
Groups and fields are ordered by declaration. Override a position with ->order(n) on a group (inside its callback) or on a field — ->order(0) included:
Forms::define('survey', 'Survey')
->group('intro', 'Introduction', function (GroupBuilder $g): void {
$g->order(10); // override the group's position
$g->field('nps', 'How likely are you to recommend us?')
->number()
->order(5); // override the field's position
$g->field('name', 'Name')->order(0); // an explicit 0 is kept
})
->create();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.