Forms
A form in Symfony is described by a class: which fields it has and which entity it belongs to. From that description the framework draws the markup, fills the object with submitted data, validates it and protects against request forgery.
The form class
php bin/console make:form PostType Post// src/Form/PostType.php
namespace App\Form;
use App\Entity\Post;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class, ['label' => 'Title'])
->add('body', TextareaType::class, ['label' => 'Text'])
->add('published', CheckboxType::class, [
'label' => 'Publish',
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => Post::class]);
}
}data_class ties the form to the entity: field names match its properties.
Field types
| Type | Field |
|---|---|
TextType | A line of text |
TextareaType | Multi-line text |
EmailType | |
PasswordType | Password |
IntegerType, MoneyType | Numbers |
ChoiceType | A list or radio buttons |
EntityType | Choosing a record from the database |
DateType | A date |
FileType | A file |
Handling in a controller
One method both shows the form and accepts it:
#[Route('/blog/new', name: 'blog_new')]
public function new(Request $request, EntityManagerInterface $em): Response
{
$post = new Post();
$form = $this->createForm(PostType::class, $post);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($post);
$em->flush();
$this->addFlash('success', 'The post was created');
return $this->redirectToRoute('blog_show', ['id' => $post->getId()]);
}
return $this->render('blog/new.html.twig', ['form' => $form]);
}handleRequest() writes the submitted data into $post. If the form has not been sent yet or has errors, the method reaches render() and shows the form — with the entered values and messages.
Rendering
The shortest way is to print every field at once with form_widget():
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit">Save</button>
{{ form_end(form) }}For your own layout print the fields one by one:
{{ form_start(form) }}
{{ form_row(form.title) }}
<div class="field">
{{ form_label(form.body) }}
{{ form_widget(form.body, {attr: {rows: 10}}) }}
{{ form_errors(form.body) }}
</div>
<button type="submit">Save</button>
{{ form_end(form) }}Editing
The same form works for changing a record — just pass an existing object:
#[Route('/blog/{id}/edit', name: 'blog_edit')]
public function edit(Post $post, Request $request, EntityManagerInterface $em): Response
{
$form = $this->createForm(PostType::class, $post);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->flush();
return $this->redirectToRoute('blog_show', ['id' => $post->getId()]);
}
return $this->render('blog/edit.html.twig', ['form' => $form]);
}CSRF protection
form_end() adds a hidden field with a token to the form, and Symfony checks it on submission. Nothing extra is needed.
Common mistake
If you print the fields by hand and forget form_end(form), the hidden token field does not reach the page — submission ends with the error The CSRF token is invalid. Always close a form with form_end(): it prints every field you did not print yourself.