Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Creating Forms With Symfony Forms

Creating Forms With Symfony Forms

~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Symfony Forms is ONE of this framework's most powerful bundles: from ONE PHP class, an HTML form, server-side processing, AND validation (chapter 16) all emerge automatically – THREE tasks from a single source.

Generating a form type

php bin/console make:form ProjectType

make:form (from the maker-bundle, chapter 6) generates a skeleton under src/Form/ – we adapt it for our project. (Since our entity classes only come in block 4, we work without the data_class option for now and return an array instead.)

src/Form/ProjectType.php
<?php

declare(strict_types=1);

namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;

class ProjectType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'label' => 'Project Name',
            ])
            ->add('description', TextareaType::class, [
                'label' => 'Description',
                'required' => false,
            ])
        ;
    }
}

Using the form type in the controller

src/Controller/ProjectController.php
use App\Form\ProjectType;

#[Route('/projects/new', name: 'project_new', methods: ['GET', 'POST'])]
public function new(Request $request): Response
{
    $form = $this->createForm(ProjectType::class);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $data = $form->getData();
        // $data['name'] and $data['description'] contain the entered values

        $this->addFlash('success', 'Project created successfully!');
        return $this->redirectToRoute('project_index');
    }

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

handleRequest($request) is the decisive call: on a GET request, the form stays UNFILLED; on a POST request, it AUTOMATICALLY reads $request->request (chapter 10) and populates the form object – we no longer need to manually access individual POST fields.

isSubmitted() checks whether data was sent at all (i.e. POST instead of GET), isValid() checks validation – chapter 16 adds real validation rules, without which even empty fields currently count as "valid".

Rendering the form in Twig

templates/project/new.html.twig
{% extends 'base.html.twig' %}

{% block body %}
    <h1>New Project</h1>

    {{ form(form) }}
{% endblock %}

{{ form(form) }} renders the COMPLETE form – ALL fields, labels, validation errors, AND a submit button, including the <form> tag and CSRF protection (chapter 18) – with ONE single Twig function.

Fine-grained rendering for custom styling

form(form) is convenient to start with, but a real project usually needs more control over the HTML markup:

{{ form_start(form) }}
    <div class="form-field">
        {{ form_label(form.name) }}
        {{ form_widget(form.name) }}
        {{ form_errors(form.name) }}
    </div>

    <div class="form-field">
        {{ form_row(form.description) }}
    </div>

    <button type="submit">Save</button>
{{ form_end(form) }}

form_row(...) bundles label, widget, and errors for ONE field (as with the description field above) – the middle ground between the all-or-nothing function form(form) and full manual control via form_label/form_widget/form_errors individually (as with the name field).

Tipp: Rule of thumb: form(form) for quick prototypes and internal tools, form_row() per field for most production forms, individual form_label/form_widget/form_errors calls only for fields that are TRULY custom-styled.