grafite/forms
Grafite Forms is a Laravel package for building and managing dynamic forms. It provides a simple API for defining fields, rendering forms, validating input, and handling submissions, making it easy to add configurable, database-backed forms to your application.
Install via Composer and publish the package assets:
composer require grafite/forms
php artisan vendor:publish --provider="Grafite\Forms\FormsServiceProvider"
This drops a config/forms.php (theme, wrapper classes, field defaults) and the Blade partials the renderer uses. Read config/forms.php first — it tells you what markup the package will emit and where you can swap templates. As of v6.0.0 the default theme targets Bootstrap 5, so if your app is on Bootstrap 4, Tailwind, or no CSS framework at all, that decision is made in config/views before you write a single form class.
Then generate or hand-write a form class that extends Grafite\Forms\Forms\Form:
namespace App\Forms;
use Grafite\Forms\Forms\Form;
class ContactForm extends Form
{
public $fields = 'name:text, email:email, message:textarea';
public $actions = 'submit';
}
Render it in Blade with the facade or by echoing the instance:
{!! Form::open(ContactForm::class) !!}
The first real use case is a CRUD form bound to an Eloquent model: pass the model into the form and the package pre-fills values, wires the PUT/PATCH method spoof, and posts back to the conventional route. That's where the "magic" pays off — you stop hand-writing old() / $model->field ternaries.
One form class per resource. PostForm, UserForm, SettingsForm — keep them declarative and free of query logic. Everything about what the form contains lives in the class; everything about who and when lives in the controller.
Model binding for edit screens. Instantiate the form with the model (or via Form::make(PostForm::class, $post)). The package maps field names to model attributes, so title:text becomes <input value="{{ $post->title }}"> and the form auto-selects PUT when the model exists. New records get the POST branch for free.
Validate outside the form. Use a Laravel FormRequest and $request->validated() before you persist. The package renders fields, not rules; trying to bolt validation into the form class couples your HTTP concern to presentation and makes testing harder. On redirect-back-with-errors, ensure the theme partial prints @error($field) — check the published view if errors aren't showing. With the Bootstrap 5 theme this should map onto is-invalid + .invalid-feedback, but confirm it in the published partials rather than assuming.
Fluent per-field tweaks beat rewriting the field string. Keep the class-level $fields as the skeleton, then adjust individual fields for classes, placeholders, or options:
$form->field('email')->setAttribute('placeholder', '[email protected]');
$form->field('role')->options(Role::pluck('name', 'id'));
Actions and buttons are configured on the form ($actions), so cancel links and extra submits stay consistent across the app. Build a small base form class with your default actions and theme, and extend it everywhere.
File uploads need the form flagged as multipart and enctype emitted by the theme — confirm it in config/forms.php, then handle storage in the controller like any other uploaded file.
Theming (Bootstrap 5 in v6). Override the package partials rather than editing vendor files. Publish views, keep your own copies under resources/views/vendor/..., and adjust wrapper classes there; the form classes stay untouched when design changes. Bootstrap 5 dropped .form-group and .form-row in favour of spacing utilities (mb-3, row g-3), renamed .form-text helpers, and moved custom controls to .form-check / .form-select — so any BS4-era overrides you carry forward need a pass, not a copy-paste.
Mixed CSS stacks. If your app is Tailwind-first and you only want Bootstrap on admin screens, treat the theme as a per-form concern: publish a second partial set and select the theme on the form class (or a base class) rather than globally rewriting config/forms.php. That keeps the v6 BS5 defaults usable without forcing Bootstrap on your whole frontend.
^6.0 as an upgrade with an unknown blast radius: read the actual CHANGELOG/diff for config keys, renamed partials, and removed methods before bumping in a real app. Pin ^5 if you're not ready to re-do markup.resources/views/vendor/... copies still win — you will not see Bootstrap 5 markup until you re-publish and diff them. This is the number-one cause of "the upgrade did nothing." Publish to a temp path or back up the directory, re-publish, then merge by hand..form-group wrappers, invalid states, checkbox/radio markup, or select rendering is the most likely thing to break. Snapshot the rendered markup before and after the upgrade and diff it — that's your regression test.field() API or define fields as an array rather than fighting the string.{!! !!}; never interpolate user-supplied labels or option text directly into the field DSL. Escape labels in the controller or use the fluent setters.config/forms.php run php artisan config:clear; after re-publishing views run php artisan view:clear. A stale cache is the usual cause of "my theme change did nothing" — and during a v6 upgrade you'll likely hit both at once.$form->render()) and asserting on it — faster than clicking through a browser and grepping the DOM. Do this before the upgrade, so you have a baseline to compare against.illuminate/* constraints — but a single recent release is still thinner ground than a steady cadence. Check the constraints against your Laravel version, and keep the form classes as plain objects you can render and test yourself so a future stall is survivable.->forAdmin() toggling fields), and write unit tests against the produced HTML. That's the cheapest way to keep the package's magic from becoming unexplainable behaviour six months from now — and the cheapest way to validate a major upgrade.How can I help you explore Laravel packages today?