Updated June 2026. Tested on Symfony 7.x and PHP 8.4. Part of the Techalyst Symfony series.

Forms are where a lot of an app's work happens: collecting input, mapping it onto your data, validating it, and showing errors. Symfony's Form component takes most of the repetition out of that. You describe a form once as a class, bind it to an entity, and a single method handles the whole submit-and-populate cycle. It is more structure than a raw HTML form, and it pays off the moment validation and editing enter the picture.

A form type class

A form is described by a form type class. You list the fields in buildForm, and tell it which entity it maps to in configureOptions:

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class)
            ->add('price', MoneyType::class)
            ->add('save', SubmitType::class);
    }

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

data_class is the binding. It says this form represents a Product, so each field maps to a property on the entity. The make:form command scaffolds this class from an entity for you.

Processing a submission

In the controller, you create the form around an entity instance, hand it the request, and check whether it was submitted and valid. The whole cycle is a handful of lines:

public function new(Request $request, EntityManagerInterface $em): Response
{
    $product = new Product();
    $form = $this->createForm(ProductType::class, $product);

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $em->persist($product);
        $em->flush();

        return $this->redirectToRoute('product_list');
    }

    return $this->render('product/new.html.twig', ['form' => $form]);
}

handleRequest does the heavy lifting. On a GET it does nothing and the empty form renders. On a POST it reads the submitted data, populates the $product entity with it, and runs validation. If isSubmitted and isValid are both true, your entity is already filled in and ready to save. The same code handles editing, you just pass an existing entity instead of a new one, and the form arrives pre-filled.

Rendering in Twig

In the template, you can render the whole form in one call and refine from there:

{{ form(form) }}

That outputs every field, its label, and any validation errors. When you need control over the markup, render piece by piece with form_start, form_row, and form_end, so you can wrap fields in your own HTML while still letting Symfony handle labels, values, and errors.

Where validation comes from

Notice the controller checks isValid but the form type lists no rules. That is deliberate. Validation lives on the entity as constraints, not on the form, so the same rules apply however the entity is created. The form just triggers them. We cover those in validation with constraints.

Wrapping up

A Symfony form is a form type class that lists fields and binds to an entity through data_class. In the controller you build it around an entity, call handleRequest, and check isSubmitted and isValid, at which point your entity is already populated and ready to persist. Render it with {{ form(form) }} or field by field for custom markup, and let validation live on the entity. The same form handles both creating and editing, which is most of why the component is worth the structure.