Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

TypeField
TextTypeA line of text
TextareaTypeMulti-line text
EmailTypeEmail
PasswordTypePassword
IntegerType, MoneyTypeNumbers
ChoiceTypeA list or radio buttons
EntityTypeChoosing a record from the database
DateTypeA date
FileTypeA 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.

Thanks for reading!

Donate to the site — we work hard for you.