neutron/form-bundle
Neutron Form Bundle streamlines form creation and management in PHP applications with reusable form components, validation, and flexible rendering. It simplifies submissions and customization while integrating smoothly with Symfony-style workflows.
First, the framing. neutron/form-bundle is a Symfony bundle, not a Laravel package. The namespace is Neutron\FormBundle, it boots via Symfony's Bundle/FrameworkBundle mechanism, and it depends on symfony/form, symfony/framework-bundle, and Twig. There is no service provider, no config/form.php, no facade, no vendor:publish assets. composer require neutron/form-bundle will resolve in a Laravel app and then do absolutely nothing.
So for a Laravel developer the honest workflow is: read it as a reference implementation, borrow the pattern, don't adopt the code.
Minimal steps:
src/Form/Type/ — that directory is the entire value proposition. One form type per RIA widget (date picker, autocomplete/chosen select, rich editor), each declaring its own validation constraints and the CSS/JS assets it needs.Resources/views/ — this is where each type injects its assets into the rendered form, guarded so they're emitted once. That "asset emission" trick is the reusable part.composer.json — Symfony 2.x-era constraints, likely PHP 5.x. Confirm in a clean container before spending any more time (composer require in a throwaway Laravel 11 app is a one-hour answer that de-risks everything).First legitimate use case, in Laravel terms — replace one hand-rolled JS field with a Blade component that declares its own asset:
{{-- resources/views/components/form/select2.blade.php --}}
@props(['name', 'options' => [], 'selected' => null])
<select name="{{ $name }}" {{ $attributes->merge(['class' => 'select2']) }}>
@foreach ($options as $value => $label)
<option value="{{ $value }}" @selected($value == $selected)>{{ $label }}</option>
@endforeach
</select>
@once
@push('styles')
<link rel="stylesheet" href="{{ asset('vendor/select2/select2.min.css') }}">
@endpush
@push('scripts')
<script src="{{ asset('vendor/select2/select2.min.js') }}"></script>
<script>document.querySelectorAll('.select2').forEach(el => $(el).select2());</script>
@endpush
@endonce
That's ~20 lines and reproduces the bundle's core idea: the field component owns its widget and its assets, and the layout emits them once.
1. A field-type registry instead of scattered Blade partials.
The bundle's structural win is that a field is a class, not a template fragment. Mirror it:
interface FieldType
{
public function name(): string; // 'select2', 'datepicker'
public function view(): string; // blade component
public function rules(): array; // Laravel validation rules
public function assets(): array; // ['js' => [...], 'css' => [...]]
}
Register them in a small service container binding (app/FieldTypes.php or a #[Tag]-style collection). New widget = new class, no edits to existing forms. This is the extension point the bundle actually got right, and it survives the port.
2. Schema-driven form definitions.
The "define fields, validation, and layout once, render anywhere" pitch maps cleanly onto a config array or JSON column:
// config/forms/onboarding.php
return [
'fields' => [
['type' => 'text', 'name' => 'company', 'rules' => ['required', 'max:255']],
['type' => 'select2','name' => 'country', 'rules' => ['required', 'in:US,CA,GB']],
['type' => 'date', 'name' => 'start_on','rules' => ['nullable', 'date']],
],
];
Then build rules programmatically at submit time:
$rules = collect($schema['fields'])->mapWithKeys(fn ($f) => [$f['name'] => $f['rules']])->all();
$data = Validator::make($request->all(), $rules)->validate();
Keep the schema dumb (arrays/scalars) so it can live in a DB column, a config file, or a JSON API response.
3. Validation parity, translated not copied.
The bundle leans on Symfony's Constraint classes. Don't port them literally — map them:
| Symfony | Laravel |
|---|---|
NotBlank |
required |
Length(min,max) |
min:n / max:n |
Email |
email |
Choice |
in:a,b,c |
Regex |
regex:/…/ |
Put the mapping in the field type (rules() returns Laravel rules directly). This is where most of the "free validation" value gets realized.
4. Multi-step / repeatable fields.
sections key in the schema and render one section per request (session-backed draft, or Alpine tabs if no persistence is needed). Avoid duplicating submit logic per step — one FormController with a step parameter.wire:model="rows.{{ $i }}.field") or Alpine x-for. This is the one area where the original bundle's server-rendered approach genuinely struggles; don't fight it, use Livewire.5. Asset deduplication at the layout level.
Follow the bundle's contract: components push, the layout stacks. In layouts/app.blade.php:
@stack('styles')
...
@stack('scripts')
Keep @once inside each component. If you outgrow this, promote widget JS to entry points in resources/js/app.js and let Vite handle dedupe — but start with @once @push.
6. If you truly must run the bundle.
Only realistic shape: a standalone Symfony app (or Docker service) exposing the rendered forms as HTML/JSON, called from Laravel over HTTP. Treat it as an external system, not a dependency. This is only defensible for legacy preservation, never greenfield.
vendor/neutron/form-bundle and register nothing. Don't chase phantom "configuration" — there is none. If you're not running Symfony, remove it and move on.@once + @push ordering. @once guarantees the block renders once, but only if the component renders before the layout's @stack('scripts'). Forms rendered inside a modal or a deferred Livewire component will push too late. Fix: render the component in the base view, or hoist required scripts into app.js.@csrf into the widget's own form/meta tag, or read X-CSRF-TOKEN from a meta tag in your fetch wrapper.rules(), you get a form that renders new fields but validates nothing. Unit-test the schema → rules derivation, not the rendered HTML.assets() on the field type) is the design decision that makes the pattern maintainable. Keep it as an interface contract; never let a Blade partial hardcode a <script> tag that another partial also includes.app/Support/Forms, drop the Composer constraints, pin transitive deps, and set a sunset date.How can I help you explore Laravel packages today?