Chapitre 9.1 — Le composant Form
Partie 9 : Formulaires — Chapitre 1/5 Version de référence : Symfony 7.4.
⏱️ TL;DR
- Un formulaire se déclare dans une classe
FormType(générée parmake:form) : on ajoute des champs (->add('email', EmailType::class)).- Dans le contrôleur :
$this->createForm(...), puis$form->handleRequest($request), puisif ($form->isSubmitted() && $form->isValid()).- Rendu Twig :
{{ form(form) }}(tout d’un coup) ou champ par champ (form_row,form_widget,form_errors).- Protection CSRF automatique : chaque formulaire embarque un token vérifié à la soumission.
- Un
FormTypeest réutilisable (création, édition, API) et découplé du template.- Pour un dev Next.js : c’est react-hook-form + zod côté serveur, intégré : le
FormTypedécrit les champs,handleRequestremplit et valide l’objet, le rendu Twig génère le HTML.
🎯 Objectifs
- Créer un
FormTypeavec plusieurs types de champs. - Traiter une soumission avec
handleRequest. - Rendre un formulaire en Twig, globalement ou champ par champ.
- Comprendre la protection CSRF intégrée.
1. Déclarer un formulaire : le FormType
Un formulaire n’est pas du HTML écrit à la main : c’est une classe qui décrit les champs. On la génère :
php bin/console make:form BookingType// src/Form/BookingType.php
declare(strict_types=1);
namespace App\Form;
use App\Dto\CreateBookingInput;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\{EmailType, DateTimeType, IntegerType, SubmitType};
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class BookingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('customerEmail', EmailType::class, [
'label' => 'Email du client',
])
->add('start', DateTimeType::class, [
'label' => 'Début',
'widget' => 'single_text',
])
->add('seats', IntegerType::class, [
'label' => 'Places',
])
->add('submit', SubmitType::class, [
'label' => 'Réserver',
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => CreateBookingInput::class, // le formulaire remplit ce DTO (chapitre 9.2)
]);
}
}Chaque ->add() déclare un champ, son type et ses options (label, aide, contraintes, attributs HTML). Le formulaire est indépendant de tout template.
2. Les types de champs courants
| Type | Rend | Note |
|---|---|---|
TextType | <input type="text"> | texte simple |
EmailType | <input type="email"> | |
IntegerType / NumberType | nombre | |
TextareaType | <textarea> | |
ChoiceType | select / radios / checkboxes | listes de choix |
EnumType | select à partir d’une enum | mappe une backed enum |
DateTimeType / DateType | date/heure | widget: 'single_text' = input HTML5 |
CheckboxType | case à cocher | booléen |
EntityType | select d’entités | ex. choisir un Provider |
FileType | upload | chapitre 9.5 |
SubmitType | bouton d’envoi |
EnumType et EntityType sont particulièrement utiles pour BookLab :
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
->add('status', EnumType::class, ['class' => BookingStatus::class])
->add('provider', EntityType::class, [
'class' => Provider::class,
'choice_label' => 'name', // afficher le nom dans le select
])3. Traiter une soumission dans le contrôleur
Le cœur du flux : créer le formulaire, lui passer la requête, vérifier soumission et validité.
#[Route('/admin/bookings/new', name: 'admin_booking_new', methods: ['GET', 'POST'])]
public function new(Request $request, BookingCreator $creator): Response
{
$input = new CreateBookingInput();
$form = $this->createForm(BookingType::class, $input);
$form->handleRequest($request); // remplit $input depuis la requête (si POST)
if ($form->isSubmitted() && $form->isValid()) {
$booking = $creator->create($input); // délègue au service (contrôleur fin)
$this->addFlash('success', 'Réservation créée.');
return $this->redirectToRoute('admin_booking_index'); // Redirect après POST
}
return $this->render('booking/new.html.twig', [
'form' => $form, // affiché (GET, ou POST invalide → erreurs)
]);
}Le même contrôleur gère GET (afficher le formulaire vide) et POST (traiter). Si la validation échoue, on re-rend le template : le formulaire affiche alors ses erreurs et conserve les valeurs saisies.
4. Rendre le formulaire en Twig
Le plus simple, tout d’un coup :
{{ form(form) }}Ou champ par champ, pour contrôler la mise en page :
{{ form_start(form) }}
{{ form_row(form.customerEmail) }} {# label + widget + erreurs du champ #}
{{ form_row(form.start) }}
<div class="grid">
{{ form_label(form.seats) }}
{{ form_widget(form.seats) }}
{{ form_errors(form.seats) }}
</div>
{{ form_end(form) }}form_start/form_end: la balise<form>(avec la méthode, l’action, l’enctypepour les uploads, et le token CSRF).form_row: label + widget + erreurs d’un champ, d’un coup.form_label/form_widget/form_errors: les morceaux séparés, pour une mise en page fine.
5. La protection CSRF (automatique)
Chaque formulaire embarque un token CSRF caché, généré côté serveur et vérifié à la soumission. Si le token manque ou est invalide, le formulaire est invalide — ce qui bloque les attaques CSRF (un site tiers qui soumettrait un formulaire à votre place). Vous n’avez rien à faire : c’est actif par défaut sur les formulaires du composant Form.
⚠️ Piège : les actions hors composant Form (un
<form>écrit à la main, comme le bouton « Supprimer » du chapitre 7.4) ne bénéficient pas automatiquement de la protection CSRF. Il faut y ajouter un token manuellement (csrf_token()en Twig + vérification côté contrôleur avecisCsrfTokenValid()). Pour tout formulaire d’état, un token CSRF est obligatoire.
💡 Pour un dev Next.js : côté API stateless (JWT), on n’utilise pas de token CSRF (pas de cookie de session → pas de vecteur CSRF classique) — c’est le firewall
stateless(Partie 10). Le CSRF concerne le back-office avec session. Ne mélangez pas : formulaires HTML (session) → CSRF ; API JWT → pas de CSRF, mais auth par token.
✏️ Exercices
Exercice 1. Créez un ProviderType avec un champ name (texte) et un champ active (case à cocher), plus un bouton d’envoi.
✅ Solution
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['label' => 'Nom'])
->add('active', CheckboxType::class, ['label' => 'Actif', 'required' => false])
->add('submit', SubmitType::class, ['label' => 'Enregistrer']);
}(required: false sur une case à cocher pour autoriser « décoché ».)
Exercice 2. Dans quel ordre teste-t-on isSubmitted() et isValid(), et pourquoi les deux ?
✅ Solution
if ($form->isSubmitted() && $form->isValid()). isSubmitted() distingue l’affichage (GET) du traitement (POST) — inutile de valider un formulaire non soumis. isValid() vérifie ensuite que les contraintes sont respectées. Si soumis mais invalide, on re-rend le template : le formulaire affiche ses erreurs en conservant les valeurs saisies.
Exercice 3. Pourquoi le bouton « Supprimer » écrit en HTML à la main (chapitre 7.4) doit-il gérer le CSRF manuellement ?
✅ Solution
Parce que la protection CSRF automatique ne s’applique qu’aux formulaires du composant Form. Un <form> écrit à la main n’en fait pas partie : il faut y insérer un token CSRF (csrf_token('delete' ~ id) en Twig) et le vérifier côté contrôleur ($this->isCsrfTokenValid(...)). Sans ça, l’action de suppression serait vulnérable au CSRF.
🧠 Quiz de révision
1. Où déclare-t-on les champs d’un formulaire ?
Dans une classe FormType (méthode buildForm), via ->add('champ', Type::class, [options]). Générée par make:form.
2. Quelle est la condition standard pour traiter une soumission ?
if ($form->isSubmitted() && $form->isValid()) (après $form->handleRequest($request)).
3. Comment rend-on un formulaire champ par champ ?
Avec form_start/form_end, puis form_row (label+widget+erreurs) ou form_label/form_widget/form_errors séparés.
4. La protection CSRF est-elle automatique ?
Oui pour les formulaires du composant Form (token caché vérifié à la soumission). Non pour un <form> écrit à la main (à gérer manuellement).
5. L’API JWT de BookLab utilise-t-elle des tokens CSRF ?
Non : stateless (JWT), sans cookie de session → pas de CSRF classique. Le CSRF concerne le back-office avec session.
Prochain chapitre : 9.2 — Formulaires liés aux entités & aux DTO — data_class, entité directe vs DTO, et le pattern recommandé.