Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Form Laravel Package

symfony/form

Symfony Form lets you build, process, and reuse HTML forms in PHP with a flexible, component-based API. Handle validation, data mapping, and rendering with ease in any Symfony or standalone app.

View on GitHub
Deep Wiki
Context7

Getting Started

symfony/form is a standalone component — you can use it without Symfony's framework, but nothing works until you assemble the factory and a few extensions yourself. In a Symfony app, symfony/framework-bundle wires all of this for you; in Laravel, you build it.

Install and bootstrap (framework-less):

composer require symfony/form symfony/validator symfony/http-foundation
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Validator\Validation;

$factory = Forms::createFormFactoryBuilder()
    ->addExtension(new HttpFoundationExtension())
    ->addExtension(new ValidatorExtension(Validation::createValidator()))
    ->getFormFactory();

If you are rendering HTML you also need symfony/twig-bridge and a Twig environment with FormExtension + the form theme registered (form_div_layout.html.twig). Without it, form_widget() throws "Unknown function" — the number one first-hour stumble.

Mind the version line. The 8.1 series is current (8.1.9 as of this writing). Everything documented below exists in 8.1; if you are pinned to 7.x by symfony/http-foundation, check each feature's "since" note before relying on it. FormFlow, for example, needs 8.1.x and specifically ≥ 8.1.9 (see Gotchas).

First use case: a contact form with server-side validation. Build it as a class, not inline, from day one:

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $b, array $options): void
    {
        $b->add('email', EmailType::class)
          ->add('message', TextareaType::class, ['constraints' => [new NotBlank(), new Length(max: 2000)]])
          ->add('send', SubmitType::class);
    }

    public function configureOptions(OptionsResolver $r): void
    {
        $r->setDefaults(['data_class' => ContactMessage::class]);
    }
}

Where to look first:

  • The component docs for standalone wiring; the FormType reference for options — you will live in this page.
  • AbstractType, FormBuilderInterface, FormEvents, DataTransformerInterface in the source; those four files explain 80% of behaviour.
  • The Form panel in the Symfony profiler if you're in a Symfony app — it shows the exact data → normalized → view pipeline per field.

Laravel reality check: Laravel's own FormRequest + Blade covers simple CRUD with less ceremony. Reach for symfony/form when you need the same form definition reused across contexts (web controller, API endpoint, CLI importer, admin panel), or when you want the type/data-transformer/event model. That is where it earns its keep.

Implementation Patterns

Type classes as the unit of reuse. One AbstractType per form, one AbstractType per reusable field (AddressType, MoneyRangeType). Compose with ->add('billing_address', AddressType::class). Inject services into the type's constructor (a repository for choice_loader, a slugger, an uploader) and register it with the form.type tag — in Laravel, autowire types through the container and register each with $factory->getType(...) indirectly by passing new MyType($dep) to $builder->add().

The handleRequest → isValid loop. Keep controllers dumb:

$form = $this->factory->create(ContactType::class, new ContactMessage());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
    $data = $form->getData(); // valid, mapped object
    // redirect after success
}

Bridge Laravel's request in one place — Laravel already ships symfony/http-foundation as an illuminate/http dependency:

$symfonyRequest = \Symfony\Component\HttpFoundation\Request::createFromBase($laravelRequest->duplicate());
$form->handleRequest($symfonyRequest);

Dynamic forms via events. The canonical pattern: listen to FormEvents::PRE_SUBMIT on the parent to add/replace a child field based on submitted data (country → city). Use PRE_SET_DATA to shape the initial form from the model. Use POST_SUBMIT to react to a value without touching mapping. Avoid PRE_SUBMIT for validation logic — validation belongs in constraints.

Collections. CollectionType::class with allow_add/allow_delete and entry_type. Remember by_reference => false plus an adder/remover pair on the entity, otherwise the collection won't update. Render prototype and clone it client-side.

Extensions for cross-cutting behaviour. A FormTypeExtension registered globally lets you apply a default (attr help text, a "required" star, a data-collector hook) to FormType::class everywhere. This is the intended extension point and far cleaner than subclassing every type.

Data transformers. When the model value and the view value differ (enum ↔ string, cents ↔ "12,50", DateTime ↔ Y-m-d), write a DataTransformerInterface and attach with addModelTransformer/addViewTransformer. Money, DateTime, Enum and the file upload transformer are all implementations of the same idea. Prefer them to empty_data callbacks. Enum guessing (8.1.9, #66861) now reaches private properties declared on parent classes — if you map an inherited enum property that is private in the base class, the automatic EnumType transform now resolves instead of falling through to a string/ChoiceType guess. Re-run your form tests after the bump if you had a manual transformer standing in for that case; it may now be redundant (or double-applied).

Multistep forms. FormFlow (7.4 / 8.0, symfony/form 8.1+ recommended) manages step navigation and session state instead of hand-rolling hidden step fields. Pin ≥ 8.1.9. 8.1.8 (#66164) fixed flow creation, step fallback, backward navigation and reset; 8.1.9 (#66776) adds validation of the current step's data — previously you could advance past a step whose fields were individually invalid. Rely on the flow's own validation rather than hand-rolled isValid() calls on sub-forms, and re-test your wizard's "can I proceed?" behaviour: steps that used to advance will now block. Earlier 8.1.x patches covered the "skipped last step still marked finished" case (8.0.0-RC3 / 7.4.0-RC3). If you tried FormFlow on 8.1.0–8.1.7 and gave up on the back button, retest on 8.1.9 before writing your own step machinery.

Testing. $form = $factory->create(...); $form->submit([...]); and assert on isValid(), getErrors(), getData(). No HTTP layer needed; this is a strong argument for the component over request-validated alternatives. If you extend Symfony's own ValidatorExtensionTrait in custom test helpers, note that 8.1.8 dropped the deprecated any() matcher there (#66283) — your trait subclasses keep working, but any test shim that stubbed the matcher will need adjusting.

Gotchas and Tips

Composer version lockstep. All symfony/* packages must share a major version. Laravel pins symfony/http-foundation to ^7.x on some release lines; if so, composer require symfony/form:^8 will fail or force an unintended framework upgrade. Check composer show symfony/http-foundation before choosing. Symfony 8 requires PHP ≥ 8.4 — a real constraint in a Laravel 11/12 app on 8.2.

handleRequest() before isValid(). isValid() throws LogicException on an unsubmitted form. Also: do not call handleRequest() twice in the same request, and call clearMissing => false only for PATCH-style partial updates.

Empty data is not null. A non-required, non-compound text field submits ''; compound forms submit []. If your data_class has a non-nullable typed property, mapping blows up. Fix with empty_data on the field or a nullable property. Same class of bug appears with CollectionType — empty collection submission requires empty_data => [] on the collection field.

Expanded + multiple ChoiceType with sparse indices. Partial submits produced array_flip() warnings in older patches; fixed in 8.1.0-BETA2. Same for EnumType choice_label with grouped choices — patches landed in 7.3.8/6.4.30.

Optional parents and required ChoiceType placeholders. If a required ChoiceType (placeholder included) sits inside an optional parent form, the placeholder used to be dropped from the rendered options when the parent was marked not-required. Fixed in 8.1.9 (#66647). If you had a 'placeholder' => ... silently not rendering in a nested/optional context, upgrade rather than adding an empty-string choice by hand.

CSRF in APIs and Livewire/Alpine posts. csrf_protection defaults to true. For stateless JSON endpoints set ['csrf_protection' => false] on the root type, or use csrf_token_id + csrf_field_name to align with Laravel's _token. Mismatched token IDs surface as "The CSRF token is invalid" with no further detail.

Validation needs ValidatorExtension. Without it, constraints on options are silently ignored — a nasty failure mode because the form still builds and submits. The duplicate-error bug when ValidatorExtension was instantiated more than once is fixed in 7.4.7 / 8.0.7 / 6.4.35; align patches accordingly.

Errors are attached shallowly. $form->getErrors() does not include child errors. Use getErrors(true) or iterate FormErrorIterator recursively when rendering a summary. getErrors(true, false) gives nested iterators by field — useful for mapping to a JSON:API-style error bag.

Debugging the pipeline. When a value "doesn't come through": check $form->getExtraData() (unmapped/unexpected fields), $form->getViewData() (post-view-transformer, i.e. the string), $form->getNormData() (post-model-transformer), and $form->getData(). Nine times out of ten the discrepancy is a transformer running twice or a wrong getParent().

Don't mutate data in listeners during submit. Editing event->getData() in POST_SUBMIT has no effect on mapping and confuses the profiler. Map in transformers, decide in POST_SUBMIT.

Performance. Resolving choices to objects on every request is the classic hot spot — use choice_loader with a lazy closure or an EntityType with query_builder. Also avoid PRE_SET_DATA listeners that eagerly add dozens of collection children; the fix in 7.4.9/8.0.9 for preserving children added by PRE_SET_DATA exists precisely because that pattern is common but fragile.

Newer options worth knowing. input => 'date_point' on DateType/TimeType/DateTimeType (7.4+) gives DateTimeImmutable instead of the default mutable object. 8.1 adds handle_missing_data to opt into MissingDataHandler for absent forms. CurrencyType gained active_at, not_active_at, legal_tender in the same release line — check those before maintaining your own currency list. Note that active_at => null previously broke CurrencyType; fixed in 8.1.9 (#66859). Explicitly passing null to mean "no reference date" is now a supported way to say "don't filter by active status", so prefer it to omitting the option when the value comes from config/customer record.

Translation churn. 8.1.8 and 8.1.9 are overwhelmingly translation releases: 8.1.8 reviewed validator strings across nl, sq, es, pl, sv, pt_BR, mk, ru, sr, id, ar, hr, it and fr; 8.1.9 continued with tl, bg, el, ur, ja, tr and fa, and fixed malformed placeholders (#66488) — a %name%/{{ value }} arity mismatch in a catalog can surface as an sprintf/trans error or a garbled message, so an upgrade can genuinely fix a crash, not just change wording. Harmless if you use the shipped catalogues — but if you have overridden validator strings in lang/ or matched on message text in assertions or error-mapping code, re-check those fixtures after the bump; wording changed in several locales. Prefer matching on message keys/constraint types rather than translated strings.

Session contamination. If you use FormFlow (session-backed) with entities holding non-serializable services, older patches leaked them into the session; fixed in 7.4.7/8.0.7. Store IDs in the flow, not entities. 8.1.9 (#66598) fixes FormFlow mutating session data written before 8.1 — flows persisted by a 7.x/8.0 deployment are now read and rewritten safely instead of being clobbered. If you are upgrading an app with live, in-flight wizard sessions, deploy 8.1.9 rather than 8.1.8, and still verify a half-finished flow after the deploy rather than assuming stored step data is laid out identically.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib