adamwathan/form
A standalone PHP form builder with optional Laravel integration. Build forms through an expressive, fluent API with automatic data binding, validation error display, and customizable markup, keeping views clean and testable.
This is a standalone HTML builder with no framework coupling — no service provider, no facade, no config file. That is the whole point of it, and also the first thing to understand: it is a string generator, not a Laravel form package.
Install it and look at src/FormBuilder.php first, then src/Elements/. Everything you will ever call is a method on FormBuilder that returns an element object; the element object is stringable, so {{ $form->text('name') }} in Blade just works.
composer require adamwathan/form
Because there is no auto-discovery, register a singleton yourself if you want one instance per request (useful so bind() and default values carry across a partial):
// AppServiceProvider::register()
$this->app->singleton(\Adamwathan\Form\FormBuilder::class);
First use case — a create form in a controller/Blade pair:
$form = app(\Adamwathan\Form\FormBuilder::class);
{!! $form->open()->action(route('users.store'))->method('POST') !!}
{!! $form->token() !!}
{!! $form->label('name', 'Name') !!}
{!! $form->text('name')->class('form-control') !!}
{!! $form->email('email')->class('form-control') !!}
{!! $form->password('password')->class('form-control') !!}
{!! $form->submit('Create')->class('btn btn-primary') !!}
{!! $form->close() !!}
If that feels like writing Rails helpers inside Blade, you have understood the package correctly.
Build once, reuse per request. The builder accumulates bound data and defaults. Instantiate it in the controller and pass it to the view rather than newing it inside a Blade loop, otherwise model binding and defaults silently reset.
Bind for edit forms. This is the single feature worth adopting the package for:
public function edit(User $user)
{
$form = app(FormBuilder::class)->bind($user);
return view('users.edit', compact('form'));
}
Once bound, $form->text('name') populates from $user->name, and $form->select('role') will pre-select the matching option. Binding accepts an object or an array, and works with Eloquent models because it just reads public properties / array keys — no interfaces, no contracts.
Chain attributes on the element, not the builder. Every element exposes a fluent attribute API: class(), id(), placeholder(), required(), readonly(), disabled(), data('key', 'value'), and a generic attribute('name', 'value'). Build small private helpers for your design system so you write ->class('form-control') in one place:
private function input($name, $type = 'text')
{
return $this->form->{$type}($name)->class('form-control')->id("field-{$name}");
}
Default values as a fallback layer. ->defaultValue('x') applies when nothing is bound and no value is set. Use it for create forms that share a view with the edit form; the precedence is value → bound model → default.
Old input is your job. The builder predates any Laravel-aware repopulation, so bridge it explicitly in a base controller or middleware:
protected function repopulate(FormBuilder $form): FormBuilder
{
foreach (session()->getOldInput() as $key => $value) {
// set value on the matching element, or keep a keyed map and apply per field
}
return $form;
}
In practice most teams just do ->value(old('name', $user->name ?? null)) per field, which is explicit and debuggable — arguably better than the magic anyway.
Wrap it in a Blade component or your own facade if you keep it. A <x-form.input name="email"> component that internally calls the builder means you can swap the implementation later without touching 60 views — important for a package archived in 2017.
Extension point: subclass FormBuilder. It resolves element classes and exposes its element methods directly, so adding $form->tel(...) or a custom <x-field> wrapper is a small subclass plus one method. Forking is realistic here: it is a handful of files under MIT, and vendoring it is faster than negotiating with an unmaintained dependency.
Integration tip: since output is raw HTML, always use {!! !!} for elements and {{ }} for user-supplied content. Never echo an unescaped value into ->class() or ->attribute() from request input.
str_contains-style updates, no security patches. Run it on a modern PHP version and expect deprecation notices from untyped property access. If it is load-bearing, fork it — the surface area is tiny.config/form.php, no Blade directives. Any "Laravel integration" you remember from blog posts was glue code someone wrote by hand.token() is not @csrf. Do not assume the token element reads Laravel's session or the XSRF-TOKEN cookie. Verify what it actually emits on your version, or just use @csrf and treat the form builder purely as a field renderer.->method('PUT') sets the method attribute, which is useless in a browser form. Pair it with an explicit _method hidden input ($form->hidden('_method')->value('PUT')) — and remember open()->method() itself only accepts GET/POST semantics in HTML.attribute() is your responsibility. Audit once.->class('x') replaces or appends, and whether there is a separate addClass, differs across these old builders. Dump the rendered string — var_dump((string) $form->text('name')->class('a')->class('b')) — before you build a design system on top of it.(string) $element calls render(); log that instead of digging through the DOM. This is the fastest way to answer any "why is this attribute missing" question.['us' => 'United States']) and check whether it expects keys, values, or a collection; old builders frequently assume arrays only, so ->pluck('name', 'id')->all() before handing over.checked unless the element compares against the exact value you set. Set ->value(1) explicitly and confirm the rendered HTML.How can I help you explore Laravel packages today?