pitch/form
Pitch\Form is a Laravel package for defining and rendering forms from PHP, aimed at keeping form structure and markup organized in code. It is a small, low-adoption package, so review its documentation and maintenance status before using it in production.
This package wires the Symfony Form component into a Laravel application, so you get Symfony's form types, data mappers, and constraint-based validation while staying inside Laravel's container, routing, and Blade views. Treat it as an alternative to hand-rolled request validation + Blade partials when a form has enough structure (nested collections, dynamic fields, multi-step) that a form object pays for itself.
Minimal steps:
composer require ph-fritsche/symfony-form — the service provider is auto-discovered.php artisan vendor:publish --tag=symfony-form-config, then skim config/symfony-form.php first; it controls the form factory binding, CSRF handling, and the view/template resolution used when rendering.FormFactoryInterface) — that's the seam everything else goes through, and the thing you'll mock in tests.FormType class, render it in a Blade view, and handle it in a controller. That end-to-end path exercises field building, data mapping, validation, and error rendering — the four things that break.As of v1.2.0 you can skip the manual ['data' => $model] wiring in the common case: the form data (entity) can be resolved from attributes, so route-model binding flows into the form automatically. Start with the annotated-controller path if you're on PHP 8, and fall back to the explicit data option when you need to override it.
FormType as the unit of reuse. Put buildForm() in a dedicated class per resource (App\Forms\PostType), keep controllers thin. Constraints live on the type, not in the controller — that's the whole point of moving off $request->validate().
Controller triad. A typical action is: create the form with the factory, let the package bind the request, then branch on isSubmitted() && isValid(). On success call getData() and persist; on failure pass createView() to the view. Don't reach into $request->all() — the form is the input boundary.
Attribute-driven data resolution (new in v1.2.0). Annotate the controller argument/action with the package's form-data attribute and the entity is resolved for you, typically from the route parameter, instead of you passing ['data' => $post]. Pattern this buys you:
data in the options array should still win; use that as your escape hatch for non-routable cases (e.g. a "duplicate this post" screen).Collections and relations. Use CollectionType with entry_type for has-many relations, and EntityType-style choices for belongs-to. Pair with PRE_SUBMIT/POST_SUBMIT listeners for fields whose choices depend on another field's value — this is where the Symfony model earns its keep over manual validation.
Layering with Laravel validation. Prefer Symfony constraints for anything the form owns, and reserve Laravel FormRequest/Rule objects for request-level concerns (authorization, throttling). Running both against the same payload doubles your error surface.
Testing. Build the type through the container in a feature test and submit an array directly; assert on isValid() and getErrors(). This is faster and more precise than driving the HTML form, and it's the cheapest way to regression-test attribute resolution when you upgrade.
VerifyCsrfToken middleware can both be active. Pick one, disable the other in config/middleware — the failure mode (419s or "invalid CSRF token" form errors) looks like a routing bug and isn't.FormRequest, users see duplicate messages and you get two sources of truth for "is this valid". Pick a lane.data option is supplied. After adding or changing an attribute, run php artisan optimize:clear — cached/compiled container state and opcache can leave you resolving the old target, and the symptom is a mysteriously empty form rather than an exception. Attributes are not inherited from parent classes or interfaces, so annotate the concrete controller method.isSubmitted() and isValid() are separate for a reason; calling getData() before validation gives you unvalidated, partially-mapped input. Also remember isValid() returns false rather than throwing — check it, don't assume.$fillable/$guarded and accessor/mutator behavior interact with the data mapper. If a field silently doesn't persist, check the mapper config and the model's guard settings before suspecting the form.$form->getErrors(true) (deep) returns a FormErrorIterator; plain getErrors() only gives top-level errors and is why "the form is invalid but shows nothing" happens. dump($form->createView()) is the fastest way to see what the view layer is actually being handed.FormTypeExtension for cross-cutting behavior (help text, CSS classes, audit fields) beats copy-pasting options into every type. A FormTypeGuesser is the right hook if you want types inferred from model metadata.How can I help you explore Laravel packages today?