Skip to Content

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 par make:form) : on ajoute des champs (->add('email', EmailType::class)).
  • Dans le contrôleur : $this->createForm(...), puis $form->handleRequest($request), puis if ($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 FormType est 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 FormType décrit les champs, handleRequest remplit et valide l’objet, le rendu Twig génère le HTML.

🎯 Objectifs

  • Créer un FormType avec 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

TypeRendNote
TextType<input type="text">texte simple
EmailType<input type="email">
IntegerType / NumberTypenombre
TextareaType<textarea>
ChoiceTypeselect / radios / checkboxeslistes de choix
EnumTypeselect à partir d’une enummappe une backed enum
DateTimeType / DateTypedate/heurewidget: 'single_text' = input HTML5
CheckboxTypecase à cocherbooléen
EntityTypeselect d’entitésex. choisir un Provider
FileTypeuploadchapitre 9.5
SubmitTypebouton 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’enctype pour 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 avec isCsrfTokenValid()). 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 DTOdata_class, entité directe vs DTO, et le pattern recommandé.