vmphobos/x-form
x-form is a Laravel/PHP package for defining and rendering forms with a clean, reusable API. It helps organize fields, handle validation and form state, and keep form logic maintainable across Blade views and application code.
Install and pin.
composer require vmphobos/x-form:2.1.0
Pin the exact version, not ^2.0. The v2.1.0 release notes contain no changelog or migration guidance, so floating constraints mean you absorb undocumented changes on the next composer update.
Look here first, in this order:
README.md — the only authoritative usage source.src/ — the service provider (component registration), any Form/Field classes, and the validation bridge. This tells you whether the abstraction is thin (good) or owns your data model (bad).resources/views/ — the Blade components. Read these before styling anything; you will almost certainly publish and override them.tests/ — the most reliable documentation for a package with a thin README.php artisan vendor:publish --provider="Vmphobos\XForm\XFormServiceProvider"
Inspect what tags appear; publish config and views separately if the package supports it.
First use case — a create form. The package is described as "one line" Blade components, so the smallest useful increment is a three-field form rendered from a fluent definition. The exact method names below are illustrative — confirm them against src/ before committing to a shape:
// app/Livewire/ContactForm.php or a controller
use Vmphobos\XForm\Form;
$form = Form::make()
->post(route('contact.store'))
->fields(fn ($f) => $f
->text('name')->label('Name')->rules('required|max:255')
->email('email')->label('Email')->rules('required|email')
->textarea('message')->label('Message')->rules('required')
);
<x-form :form="$form" />
Do this once in a scratch route before wiring it into a real feature. You want to see the generated HTML, the name attributes, and whether validation errors and old() input repopulate — that last part is the usual failure point in form packages.
Centralize definitions, don't inline them. Put form definitions in app/Forms/*.php (or static methods on the form's backing model) and return them from a single method. Both the controller and the Blade view consume the same object, so a field rename is one edit, not a hunt through templates.
final class PostForm
{
public static function make(?Post $post = null): Form
{
return Form::make()
->model($post)
->action($post ? route('posts.update', $post) : route('posts.store'))
->method($post ? 'PUT' : 'POST')
->fields(fn ($f) => $f
->text('title')->rules('required|max:255')
->select('status')->options(PostStatus::cases())->rules('required')
->textarea('body')->rules('required')
);
}
}
Create/edit share one definition via model binding. Pass null for create and the model for edit. Verify the package handles casting (enums, datetime, JSON columns) — if it doesn't, normalize in the model's accessors rather than special-casing the view.
Keep validation ownership in one place. The package advertises "Laravel-native validation integration." Pick one of two modes and stick to it:
->rules() calls feed $request->validate() in the controller. Do not also declare a FormRequest with the same rules; drift is guaranteed.The first is more idiomatic for this package, but it couples your validation source of truth to a 0-star dependency. If the rules are business-critical, keep a FormRequest as the authority and treat the fluent rules as display hints.
Livewire: bind to public properties, not the form object. If the package targets Livewire, the natural pattern is one public property per field plus a rules() method on the component:
class ContactForm extends Component
{
public string $name = '';
public string $email = '';
public string $message = '';
protected function rules(): array
{
return ['name' => 'required|max:255', 'email' => 'required|email', 'message' => 'required'];
}
public function submit(): void
{
$this->validate();
// persist
$this->reset();
}
}
Render the package's field components inside the Livewire view with wire:model. Confirm the components forward wire:model — if they strip unknown attributes, you'll need the published views.
Extend field types, don't fork. Register custom field components in the published config or service provider and keep them in app/View/Components/Form/. A custom field that composes the package's base component survives upgrades; a forked one does not.
Testing. Feature-test the POST path independently of the component (that's framework behavior), and use assertSee on labels and assertSee('name="title"', false) on rendered output to catch silent markup regressions after upgrades.
git diff v2.0.0..v2.1.0 in the vendor checkout or on GitHub. Absence of a breaking-change notice is not evidence there isn't one.old() and error repopulation immediately. A form package that renders fields but doesn't restore input after a failed validation redirect is unusable in practice. Test: submit invalid data, confirm the redirect back shows previous values and per-field error text.@csrf and @method('PUT') must be emitted by the component or documented as your responsibility. A missing @csrf produces a 419 that looks like an auth bug.wire:key. If you loop field components (repeatable rows, line items), add a stable wire:key per iteration or Livewire will reuse DOM nodes and mangle values.items[0][name] vs items.0.name). Get this wrong and validation passes while the database gets nothing.php artisan view:clear, then inspect the raw HTML in the browser. If you've published views, ddd($form) at the top of the published Blade file to see the form object's shape.FormRequest syntax. The realistic scenario for a 0-star package is that you replace it in 18 months; the cost of that swap should be measured in hours, not a refactor.How can I help you explore Laravel packages today?