Download the PHP package ramondev/formcraft without Composer
On this page you can find all versions of the php package ramondev/formcraft. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download ramondev/formcraft
More information about ramondev/formcraft
Files in ramondev/formcraft
Package formcraft
Short Description A declarative form library for Laravel: field definitions, validation, theming, multi-step wizards, and Artisan scaffolding.
License MIT
Informations about the package formcraft
Formcraft — Complete Usage Guide
Formcraft is a declarative form library for Laravel. You declare a form's fields once as a PHP class, and Formcraft handles HTML rendering, server-side validation, theming (Bootstrap/Tailwind/your own), file uploads, model binding, repeated field groups, and multi-step wizards.
This guide covers everything needed to use the package day to day: install,
build forms, render them, validate and save data, upload files, theme them,
and wire up wizards — based on the actual package source (ramondev/formcraft).
Table of contents
- Installation
- Quick start
- Defining fields
- Field option reference
- Rendering a form
- Validating a submission
- Reading submitted values
- Saving to an Eloquent model
- File & image uploads
- Group fields (nested/repeated data)
- Form Sets ("add another" repeated forms)
- Multi-step wizards (
FormWizard) - Theming
- Customizing a form at runtime
- Artisan commands
- Full worked example
- Behavior notes & gotchas
1. Installation
Install Formcraft straight from Composer:
FormServiceProvider registers automatically via Laravel package
auto-discovery — no manual config/app.php edit needed. After installing,
php artisan list should show make:form and make:theme.
To update to a newer version later:
Requirements: PHP ^8.1, Laravel (illuminate/*) ^10–^13, and
intervention/image ^4.2 (used for resizing image uploads).
2. Quick start
Every form is a class extending Formcraft\FormProcessor that implements
setFields():
In a controller:
In a Blade view, just echo the form — every field renders itself:
You can also scaffold a new form with Artisan (see §15):
3. Defining fields
setFields() must return a LaravelFormTemplate, built from a closure that
receives a FormFields builder and returns an associative array of
field_name => $f->SomeField(...). The array key is the field's name
(used for the HTML name/id, the validation key, and the model attribute
on save).
Available field builders
| Method | Renders as | Notes |
|---|---|---|
InputField(type: ...) |
<input type="..."> |
Generic — set any HTML input type yourself |
CharField() |
<input type="text"> |
Plain text |
EmailField() |
<input type="email"> |
Adds an email validation rule automatically |
PasswordField() |
<input type="password"> |
Not cached in old() by default; can auto-hash (see below) |
IntegerField() |
<input type="number"> |
Adds a numeric validation rule automatically |
DateField() |
<input type="date"> |
Adds a date validation rule automatically |
CheckBoxField() |
<input type="checkbox"> |
Use checked/value to control state |
RadioField() |
<input type="radio"> |
Render several with the same field name and different value |
TextareaField() |
<textarea> |
|
SelectField() |
<select> |
Pass options (see below) |
FileField() |
<input type="file"> |
Handles uploads through FileManager |
ImageField() |
<input type="file"> |
FileField specialised for images — restricts accept, supports resizing/thumbnails |
GroupField() |
wraps child fields in a <fieldset> |
Nesting / repeating structures — see §10 |
4. Field option reference
Every builder above shares this common set of keyword arguments (pass only the ones you need — all have sensible defaults):
| Option | Type | Default | Purpose |
|---|---|---|---|
required |
bool | true |
Adds HTML required + a required validation rule |
requiredIf |
callable(array $submittedData): bool |
null |
Dynamically overrides required per-submission (both the rendered attribute and the validation rule) |
label |
string | field name, prettified | Visible label/legend text |
showLabel |
bool | true |
Set false to skip rendering a label entirely |
labelAttr |
array | [] |
Extra HTML attributes on the <label>/<legend> (e.g. ['class' => 'font-bold']) |
attr |
array | [] |
Extra HTML attributes on the input itself |
value |
string | "" |
A fixed value to render (overridden by cached/old input and initial values when applicable) |
default |
mixed or callable | null |
Fallback value used only when there's no submitted/initial value |
id |
string | field name | Override the HTML id |
placeholder |
string | null |
Placeholder text |
minLength / maxLength |
int | null |
Adds minlength/maxlength HTML attrs + min:/max: validation rules |
pattern |
string | null |
HTML pattern attribute |
autocomplete |
string | null |
HTML autocomplete attribute |
datalist |
array | [] |
List of suggestion strings; renders a <datalist> and wires it via list= |
readonly / disabled |
bool | null |
HTML flags |
cache |
bool | true (false for PasswordField) |
Whether to repopulate the field from Laravel's old() input after a failed submission |
translate |
bool | false |
Runs the label/placeholder through Laravel's __() helper |
widget |
callable(ObjectClass $fields): string |
null |
Fully custom renderer for this one field — bypasses the active theme entirely |
ignore |
bool | false |
Excludes the field from rendering, validation, values(), and save() |
saveAs |
string | null |
Model attribute (or dotted relation path, e.g. "profile.bio") to write this field's value to on save() |
validate |
callable(ObjectClass $submittedData): true\|string |
null |
Custom validation callback; return true to pass or an error string to fail |
validateRule |
array | [] |
Extra Laravel validation rule strings/objects appended to the auto-generated rules |
validateMessageRule |
array | [] |
Custom Laravel validation messages, passed straight to Validator::make() |
validateMessageAttribute |
string|callable | null |
Custom :attribute name used inside validation messages. For a multiple: true field (e.g. 'photo[]' => $f->ImageField(multiple: true, ...)) a plain string covers both the field's own key and the per-item wildcard rules. Pass a fn (int $index): string instead to name each item individually — e.g. fn ($i) => 'Photo ' . ($i + 1) turns the default photo.0 into Photo 1, photo.1 into Photo 2, and so on, based on how many items were actually submitted. |
Type-specific options
DateField
format— appended as a second Laraveldaterule parameter.
PasswordField
hashValue(bool, defaulttrue) — when saving viasave(), the value is run throughHash::make()before being written to the model.
IntegerField
min,max— numeric bounds (validation + HTML attrs).step— HTMLstepattribute.
SelectField
options— array of either plain scalars (['a', 'b']) or associative arrays:['value' => ..., 'text' => ..., 'attr' => [...], 'selected' => bool].valuecan itself be a two-element array[storedValue, displayLabelWhenResolved]if you want the raw HTML value to differ from whatvalues()returns.multiple(bool) — renders a multi-select and expects/returns an array.
FileField
multiple(bool) — allow multiple files.accepts(array) — HTMLacceptlist (e.g.['.pdf', '.docx']).fileMimes(array) — adds a Laravelmimes:validation rule.min/max— min/max validation for each file's size, in kilobytes (Laravel's file-size rule). Withmultiple: truethis still applies per file, not to how many files were uploaded.minFiles/maxFiles— only meaningful withmultiple: true; validates the number of files uploaded (Laravel'sarrayrule combined withmin:/max:), separate frommin/maxabove.uploadTo(string, default'upload') — destination directory.imageSize(string, e.g."800x800") — resize dimensions (images only).thumbnail(string, e.g."150x150") — also generate a thumbnail (images only).oldFile(string) — a previously-stored filename to delete once the new upload succeeds.fileType(string, default'file') — adds a matching validation rule (ImageFieldsets this to'image'automatically).filename(string) — save the upload under this name instead of an auto-generated one (the original extension is appended automatically if you don't include it).thumbnailTo(string) — destination directory for the thumbnail; defaults touploadTowhen omitted, so the thumbnail can live in its own folder.thumbnailName(string) — save the thumbnail under this name instead of the default"thumb_" + filename.oldThumbnail(string) — a previously-stored thumbnail filename to delete (fromthumbnailTo) once the new upload succeeds — the thumbnail equivalent ofoldFile.thumbnailSaveAs(string) — model attribute the thumbnail's filename is written to onsave(), alongside the main file'ssaveAs(see below).
ImageField
- Same as
FileField, buttype/fileTypeare pre-set to image handling, andaccepts/fileMimesdefault to common image extensions (jpg, jpeg, png, gif, webp, bmp) unless you pass your ownaccepts.
Example — a multi-photo upload capped at 4 files, 2 required minimum, each up to 2MB, with a friendly attribute name in every validation message:
The [] suffix on the field key is what tells the browser to submit
multiple files under the same name; internally Formcraft strips it, so
$form->values()->photo and error messages both refer to the field as
photo, not photo[].
Want each photo named individually instead of one shared name (so a message reads "Photo 2 must be an image" rather than "Photos must be an image")? Pass a callable instead of a string — it's called once per submitted file with that file's index:
This turns the default photo.0 / photo.1 / ... keys Laravel would
otherwise show into Photo 1 / Photo 2 / ... in every message for that
item. It works the same way on an array-typed GroupField's own
validateMessageAttribute (naming each row, e.g. "Row 1", "Row 2").
GroupField
fields(array) — child fields, built the same way as top-levelsetFields().valueType(string, default"all") —"all"— a plain fieldset; children behave like independent top-level fields."array"— children are named as an array (name[0][city]) and their combined value comes back as an array under the group's own key — use this for repeating nested data."single"— all children share one input name/value (rare; used for composite single-value widgets).
applyRuleToFields(array) — extra validation rules applied to every child whenvalueTypeis"array"(as"key.*"rules).minLength/maxLength(int) — only meaningful whenvalueTypeis"array"; validates the number of items in the array (Laravel'smin:/max:combined witharray), not the length of any single value. Use this for things like "add between 1 and 5 line items".validateMessageAttribute(string) — the friendly name used in place of the raw key in validation messages. ForvalueType: "array"groups this covers both the group's own key ("items") and the per-item wildcard rules fromapplyRuleToFields("items.*"). To rename an individual child field's key in messages (e.g."items.name"→"Item name"), setvalidateMessageAttributeon that child field itself, the normal way ($f->CharField(validateMessageAttribute: 'Item name')) — it's picked up automatically, including for nested groups.groundName— internal; lets nested groups compute dotted array names correctly.
5. Rendering a form
Whole form at once — a FormProcessor instance has a __toString(),
so you can just echo/print it (e.g. {!! $form !!} in Blade). Each field is
wrapped and rendered according to the active theme.
One field at a time — access any field as a property to get a compiled
object with ->label, ->input, and ->errors:
Each of ->label, ->input also has its own __toString(), so you can
echo them directly ({!! $form->email->input !!}), or read finer detail off
them: $form->email->input->name, ->id, ->value, ->is_required, etc.
6. Validating a submission
validate(array $data, array $files = [])— stages the submitted data (and optionally files) for validation; doesn't run anything yet.is_validated(): bool— runs every field's Laravel validation rules (built automatically from the field options in §4, plus anyvalidateRuleyou added) and returns whether all fields passed.errors(): ObjectClass— afteris_validated()returnsfalse, use:$form->errors()->all()— flat array of"field" => "combined message".$form->errors()->messages()—"field" => [message, message, ...].$form->errors()->get(['field'])— messages for one specific field.$form->errors()->first()/->last()— first/last error message overall.
If you render the form again after a failed submission (without redirecting),
fields marked cache: true (the default, except PasswordField) automatically
repopulate from Laravel's old() input.
7. Reading submitted values
values() resolves each field's submitted value — including matching a
SelectField's stored option back to its declared value, and substituting
uploaded files with their generated filenames once uploadFiles() has run.
Detecting changes against an initial value (e.g. an edit form pre-filled
via initialValue()):
8. Saving to an Eloquent model
save() copies every fillable field's resolved value onto the bound model
(calling ->save() on it at the end). Control exactly what gets copied:
setFillableAttribute(array $keys)— only copy these field keys (default: every key present in the submitted data).setExceptAttribute(array $keys)/addToExceptAttribute(array $keys)— exclude specific keys from being copied.changeSaveKey(['formField' => 'model_attribute'])— write a field's value to a different model attribute name (overrides a field's ownsaveAsoption for this instance).- A field's own
saveAs: 'profile.bio'option (see §4) writes into an already-loaded relation instead of a literal attribute — if any segment of the path isn't already set on the model, that assignment is silently skipped rather than erroring. setDefaultAttributeValue(['created_by' => $userId])— attributes applied to the model before the field values are copied over, useful for values that don't come from the form itself.
setModelAttribute() performs the copy without calling ->save(), if you
need to do more to the model first.
Passwords: a PasswordField with hashValue: true (the default) is
automatically run through Hash::make() when copied onto the model.
9. File & image uploads
Basic upload
Declare the field:
Then, in your controller, explicitly trigger the upload (this is a separate step from validation/save, so you control exactly when files hit disk):
uploadFiles(bool $returnWithPath = false)— uploads every file field. By default only the generated filename is stored; passtrueto store the full"path/filename"instead.getUploadedFiles(): ObjectClass— inspect what was uploaded, keyed by field name (nested by group, for group fields).- Non-image files are moved as-is; image files are opened and optionally
resized to
imageSize. - Passing
oldFile(the previous filename) deletes that old file once the new upload succeeds.
Custom filenames
By default an uploaded file gets an auto-generated unique name. Set
filename to force your own instead — the original extension is appended
automatically if you don't include one:
Thumbnails: separate folder, own filename, deletable, saved to the DB
Add thumbnail (the resize dimensions) to have a thumbnail generated
alongside the main upload. Four more options give you full control over it:
With thumbnail set, the field's resolved value is no longer a plain
filename string — it becomes ['filename' => ..., 'thumbnail' => ...], so:
thumbnailSaveAs makes save() write both filenames onto the model in one
call — the main filename to the field's usual saveAs, and the thumbnail's
filename to whatever attribute thumbnailSaveAs names:
If a field has thumbnail set but no thumbnailSaveAs, only the main
filename is written on save — the thumbnail filename is still available via
values()/getUploadedFiles() if you want to handle it yourself.
Using FileManager directly
Under the hood, uploads go through Formcraft\FileManager, which you can
also use standalone (e.g. outside of a form, for an ad-hoc upload):
Every FileManager method can also be called statically, e.g.
FileManager::removeFile('uploads/avatars/old.jpg').
10. Group fields (nested/repeated data)
Use GroupField to nest several fields under one key.
Plain grouping (valueType: "all", the default) — just visual/logical
grouping; children behave like normal top-level fields:
Repeated/array data (valueType: "array") — for something like "add up
to N line items", where the submitted HTML uses array-style names
(items[0][name], items[1][name], ...) and the group's resolved value is
an array of child arrays:
Here, minLength/maxLength cap the number of rows (1 to 5 items), and
validateMessageAttribute renames "items"/"items.*" to "Line items" in
error messages, while the name field's own validateMessageAttribute
renames just "items.name" to "Item name".
Validation rules for array-typed groups are generated per index
automatically (items.*.name, items.*.qty), and values()/save()
flatten group children's values into the top-level result, so
$form->values()->items gives you the full array of rows.
Groups can be nested inside groups; access a nested field's props at
definition time via a "parent/child" path with changeProps() (see §14).
11. Form Sets ("add another" repeated forms)
FormSet builds several independent copies of a whole FormProcessor
class — useful when each "row" needs its own full validation/theme rather
than being one group of fields.
Key FormSet methods:
| Method | Purpose |
|---|---|
set(): FormProcessor[] |
Every instance, ready to render (->setId/->setName populated) |
getField(int $index): ?FormProcessor |
One specific instance |
initialValue(array $values) |
Pre-fill each instance from $values[$index] |
setTheme($theme) |
Apply one theme to every instance |
validate(array $data, array $files = []) |
Splits a flat submission back out per-instance by its {label}_{index}_ name prefix |
is_validated(): bool |
True only if every instance validates |
errors(): ObjectClass |
->all()/->messages()/->first()/->last(), each keyed by index; ->toListKey() flattens to "field.index" => message" |
uploadFiles() |
Uploads files for every instance |
save(): array |
Saves every instance, keyed by index |
removeField(int $id) |
Drop an instance (e.g. "remove this row") |
changeProps(int $index, string $key, $value) |
Tweak one instance's field |
each(callable $callback) |
Run $callback($form) against every instance (e.g. to setModel() on each) — a beforeProcess() alias also exists for backward compatibility |
Redisplaying errors after a failed submission
A FormSet you build to render the page (e.g. in your create() action)
is a fresh set of FormProcessor instances — it has no memory of a
previous failed validate() call, even one from the very same request
cycle a moment ago. If you return back()->withErrors(...)->withInput()
after validation fails and then redisplay the page, its errors won't show
up unless you explicitly re-validate the new instances from the flashed
old() input:
This mirrors what FormWizard::form() already does internally for a
single step. If your view tracks a dynamic row count (an "add row" button),
also flash/read that count (e.g. a hidden item_count input) so the
redisplay rebuilds the same number of rows the person actually submitted.
12. Multi-step wizards (FormWizard)
FormWizard drives a sequence of FormProcessor "steps" — one submitted
and validated at a time, stored in the session, with everything available
once all steps are complete.
Other methods: stepKeys(), completedSteps(), goToStep($step) (jump
back, un-completing that step and everything after it — stored data is kept
so it still pre-fills), and reset() (clear all progress).
13. Theming
A theme controls the CSS classes injected into labels/inputs and how a field's label/input/errors are wrapped together — without you touching your field definitions.
Built-in themes
| Theme | Class | Style |
|---|---|---|
| Plain (default) | Formcraft\Support\PlainTheme |
No CSS framework — the library's original bare markup |
| Bootstrap 5 | Formcraft\Support\BootstrapTheme |
.form-label/.form-control/.form-select, .form-check-* for checkboxes/radios, .invalid-feedback errors |
| Tailwind | Formcraft\Support\TailwindTheme |
Utility classes directly on elements — no build step or @tailwindcss/forms plugin required |
| Card (example) | Formcraft\Support\CardTheme |
A framework-free "floating card" style meant to be copied and adapted |
Applying a theme
Per instance:
As a form's standing default, override defaultTheme() in your
FormProcessor subclass:
A field's own widget option (§4) always wins over the theme for that one
field — it bypasses theming entirely.
Writing your own theme
Implement Formcraft\Support\FormTheme (or run php artisan make:theme YourName — see §15):
14. Customizing a form at runtime
These let you tweak an already-defined form on a per-instance basis, without
touching setFields():
| Method | Purpose |
|---|---|
appendFields(callable $callback) |
Add extra fields: $form->appendFields(fn ($f) => ['extra' => $f->CharField()]); |
changeProps(string $key, array $keyValue) |
Override one or more props on an existing field: $form->changeProps('email', ['required' => false]); — for a nested group field, use a "parent/child" path |
changeFieldName(array $mapping) |
Rename how a field's input/data key is looked up (useful when the submitted payload uses different keys than your field names) |
changeSaveKey(array $mapping) |
See §8 — redirect where a field's value is written on save() |
setFillableAttribute / setExceptAttribute / addToExceptAttribute |
See §8 |
setDefaultAttributeValue(array $default) |
See §8 |
initialValue(object\|array $value) |
Pre-fill fields (and enable dirty()/isDirty()) from an existing record |
15. Artisan commands
make:form — scaffold a new form class
| Option | Purpose |
|---|---|
--fields="key:type,..." |
Pre-populate fields; recognized types: char/text, email, password, integer/int/number, date, checkbox, radio, textarea, select, file, image, group |
--theme=BootstrapTheme |
Adds a defaultTheme() override to the generated class (accepts a short name resolved under Formcraft\Support\, or a fully-qualified class name) |
--path=Forms |
Where under app/ to place the class (default app/Forms) |
--namespace= |
Override the namespace instead of deriving it from --path |
--force |
Overwrite an existing file |
make:theme — scaffold a new theme class
Generates a class implementing FormTheme with all three required methods
stubbed out and commented, under app/{--path} (default app/Themes).
16. Full worked example
Form definition (app/Forms/RegistrationForm.php):
Controller:
Blade view (resources/views/register.blade.php):
That's it — Formcraft renders every field with Bootstrap classes, validates
on submit, uploads and resizes the avatar, hashes the password, and saves
the new User.
17. Behavior notes & gotchas
A few implementation details worth knowing as you use the package:
values()vs raw$requestinput: always read submitted data through$form->values(), not the raw request — it resolvesSelectFieldoption matching and substitutes uploaded files' final filenames onceuploadFiles()has run.- Upload order matters: call
uploadFiles()afteris_validated()passes and beforesave()(or before readingvalues()for a file field), so the resolved value is the new filename rather than the raw uploaded file object. PasswordFieldisn't cached by default (cache: false) — on a failed submission the password field is intentionally left blank rather than repopulated fromold().removeFile()/thumbnail cleanup: deleting the old main file and the old thumbnail are two explicit, independent operations —oldFile(fromuploadTo) andoldThumbnail(fromthumbnailTo). Set both when you want both cleaned up;FileManager::removeFile()never guesses a thumbnail's path from the main file's.- File field values change shape once a thumbnail is configured: a
FileField/ImageFieldwithoutthumbnailresolves to a plain filename string, same as always. Addthumbnailand it resolves to['filename' => ..., 'thumbnail' => ...]instead (fromvalues()andgetUploadedFiles()) — code that reads that field's value as a plain string needs updating if you add a thumbnail to an existing field. DateField's extravalidateRule: avalidateRulearray you pass toDateFieldis appended as one nested array element alongside the automaticdate(and optionalformat) rules, rather than merged flat into the rule list — pass any extra Laravel date-related rule as a single array item if you rely on this option.- Group field naming: for
valueType: "array"groups, HTML input names use bracket syntax (items[0][name]), while Laravel validation andvalues()/save()use dot syntax (items.0.name) — you don't need to convert between them yourself, but keep it in mind if you're inspecting raw request data. ignore: truefields are skipped everywhere: they don't render, validate, appear invalues(), or get written onsave().
All versions of formcraft with dependencies
illuminate/support Version ^10.0|^11.0|^12.0|^13.0
illuminate/console Version ^10.0|^11.0|^12.0|^13.0
illuminate/validation Version ^10.0|^11.0|^12.0|^13.0
illuminate/http Version ^10.0|^11.0|^12.0|^13.0
intervention/image Version ^4.2