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 ProjectTypemake: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.)
<?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
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
{% 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.