📝 Formulaires Symfony

Créer et gérer des formulaires puissants

Types de champs • Validation • Rendu • Traitement

Formation DWWM - 2026

Au programme 📋

# 🎯 Partie 1

Introduction aux Formulaires

Pourquoi utiliser les formulaires Symfony ?

HTML pur vs Formulaires Symfony 🆚

❌ HTML pur (fastidieux)

<form method="post">
    <input type="text" name="name">
    <!-- Validation manuelle -->
    <!-- Protection CSRF manuelle -->
    <!-- Gestion erreurs manuelle -->
    <!-- Mapping données manuelle -->
    <button type="submit">Envoyer</button>
</form>

<?php
// Dans le contrôleur
$name = $_POST['name'] ?? '';
if (empty($name)) {
    $errors[] = 'Nom requis';
}
if (strlen($name) < 3) {
    $errors[] = 'Trop court';
}
// ... encore 50 lignes de validation
?>

✅ Symfony Forms (automatisé)

// Contrôleur
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    // Données déjà validées et mappées !
    $entityManager->persist($article);
    $entityManager->flush();
    
    return $this->redirectToRoute('success');
}

// Template Twig
{{ form(form) }}
{# HTML + validation + CSRF automatique ! #}

✅ Avantages Symfony Forms :

Analogie : Le formulaire administratif 📋

Remplir un formulaire administratif 🏛️

Imagine que tu dois remplir un formulaire à la mairie :

Symfony = l'administration qui gère tout le processus !

# 🏗️ Partie 2

Créer un Formulaire Simple

Premier formulaire avec Symfony

Créer un formulaire avec make:form 🏗️

Commande

php bin/console make:form

Exemple interactif

$ php bin/console make:form

The name of the form class (e.g. GrumpyChefType):
> ArticleType

The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
> Article

created: src/Form/ArticleType.php

Success!

✅ Résultat : Fichier créé dans src/Form/ArticleType.php

Code du formulaire généré 💻

src/Form/ArticleType.php

<?php
namespace App\Form;

use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, [
                'label' => 'Titre',
                'attr' => ['placeholder' => 'Saisissez le titre']
            ])
            ->add('content', TextareaType::class, [
                'label' => 'Contenu',
                'attr' => ['rows' => 10]
            ])
            ->add('createdAt', null, [
                'label' => 'Date de création',
                'widget' => 'single_text'
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Article::class,
        ]);
    }
}

Comprendre la structure du formulaire 📖

buildForm() - Construire le formulaire

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder->add('nomDuChamp', TypeDeChamp::class, [
        'label' => 'Libellé',
        'required' => true,
        'attr' => ['class' => 'mon-css']
    ]);
}

configureOptions() - Configuration

public function configureOptions(OptionsResolver $resolver): void
{
    $resolver->setDefaults([
        'data_class' => Article::class,  // Lié à l'entité Article
    ]);
}

💡 data_class :

Indique quelle entité est liée au formulaire. Symfony fait automatiquement le mapping des champs !

Utiliser le formulaire dans un contrôleur 🎮

Créer un article

use App\Entity\Article;
use App\Form\ArticleType;
use Symfony\Component\HttpFoundation\Request;

#[Route('/article/new', name: 'article_new')]
public function new(Request $request, EntityManagerInterface $em): Response
{
    // 1. Créer une nouvelle instance
    $article = new Article();
    
    // 2. Créer le formulaire
    $form = $this->createForm(ArticleType::class, $article);
    
    // 3. Traiter la requête
    $form->handleRequest($request);
    
    // 4. Vérifier si soumis et valide
    if ($form->isSubmitted() && $form->isValid()) {
        // 5. Sauvegarder (les données sont déjà dans $article !)
        $em->persist($article);
        $em->flush();
        
        $this->addFlash('success', 'Article créé avec succès !');
        return $this->redirectToRoute('article_list');
    }
    
    // 6. Afficher le formulaire
    return $this->render('article/new.html.twig', [
        'form' => $form
    ]);
}

Afficher le formulaire dans Twig 🎨

Méthode 1 : Tout en une ligne (rapide)

{{ form(form) }}

Méthode 2 : Contrôle manuel (précis)

{{ form_start(form) }}
    
    {{ form_row(form.title) }}
    {{ form_row(form.content) }}
    {{ form_row(form.createdAt) }}
    
    <button type="submit" class="btn btn-primary">Enregistrer</button>

{{ form_end(form) }}

Méthode 3 : Contrôle total (personnalisé)

{{ form_start(form) }}

<div class="form-group">
    {{ form_label(form.title) }}
    {{ form_widget(form.title) }}
    {{ form_errors(form.title) }}
</div>

<div class="form-group">
    {{ form_label(form.content) }}
    {{ form_widget(form.content) }}
    {{ form_errors(form.content) }}
</div>

<button type="submit">Enregistrer</button>

{{ form_end(form) }}

Fonctions Twig pour formulaires 🔧

Fonction Description HTML généré
form(form) Formulaire complet Tout le formulaire
form_start(form) Balise <form> ouvrante <form method="post">
form_end(form) Balise </form> fermante </form> + CSRF
form_row(form.field) Label + champ + erreurs Bloc complet
form_label(form.field) Juste le label <label>...</label>
form_widget(form.field) Juste le champ <input> ou <select>...
form_errors(form.field) Messages d'erreur <ul class="errors">
form_rest(form) Champs restants Champs non affichés
# 📦 Partie 3

Types de Champs Disponibles

Plus de 30 types de champs !

Champs de texte 📝

Type HTML Usage
TextType <input type="text"> Texte court (nom, titre...)
TextareaType <textarea> Texte long (description...)
EmailType <input type="email"> Adresse email
PasswordType <input type="password"> Mot de passe masqué
SearchType <input type="search"> Champ de recherche
UrlType <input type="url"> URL (http://...)
TelType <input type="tel"> Numéro de téléphone

Exemples

use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;

$builder
    ->add('name', TextType::class)
    ->add('email', EmailType::class)
    ->add('password', PasswordType::class)
;

Champs numériques 🔢

Type HTML Usage
IntegerType <input type="number"> Nombre entier
NumberType <input type="text"> Nombre décimal
MoneyType <input type="text"> Montant avec devise
PercentType <input type="text"> Pourcentage
RangeType <input type="range"> Curseur (slider)

Exemples avec options

$builder
    ->add('age', IntegerType::class, [
        'attr' => ['min' => 0, 'max' => 120]
    ])
    ->add('price', MoneyType::class, [
        'currency' => 'EUR'
    ])
    ->add('discount', PercentType::class, [
        'type' => 'integer'  // 75 = 75% (pas 0.75)
    ])
    ->add('rating', RangeType::class, [
        'attr' => ['min' => 1, 'max' => 5, 'step' => 1]
    ])
;

Champs date et heure 📅

Type HTML Usage
DateType <input type="date"> Date seulement
TimeType <input type="time"> Heure seulement
DateTimeType 2 inputs Date + heure
BirthdayType 3 selects Date de naissance

Exemples

$builder
    ->add('publishedAt', DateType::class, [
        'widget' => 'single_text',  // Input HTML5
        'label' => 'Date de publication'
    ])
    ->add('birthday', BirthdayType::class, [
        'placeholder' => [
            'year' => 'Année',
            'month' => 'Mois', 
            'day' => 'Jour'
        ]
    ])
    ->add('eventDateTime', DateTimeType::class, [
        'widget' => 'single_text',
        'html5' => true
    ])
;

Champs de choix (select, radio, checkbox) ✅

Type HTML Usage
ChoiceType <select> ou radio/checkbox Liste de choix
EntityType <select> Entités Doctrine
CountryType <select> Liste des pays
LanguageType <select> Liste des langues
CurrencyType <select> Liste des devises

ChoiceType - Select simple

$builder->add('status', ChoiceType::class, [
    'choices' => [
        'Brouillon' => 'draft',
        'Publié' => 'published',
        'Archivé' => 'archived'
    ]
]);

ChoiceType - Radio buttons

$builder->add('gender', ChoiceType::class, [
    'choices' => [
        'Homme' => 'male',
        'Femme' => 'female',
        'Autre' => 'other'
    ],
    'expanded' => true,  // Radio au lieu de select
]);

ChoiceType - Checkboxes multiples

$builder->add('interests', ChoiceType::class, [
    'choices' => [
        'Sport' => 'sport',
        'Musique' => 'music',
        'Lecture' => 'reading'
    ],
    'expanded' => true,  // Checkboxes
    'multiple' => true,  // Choix multiples
]);

EntityType - Relations Doctrine 🔗

Permet de sélectionner des entités Doctrine dans un select.

Sélectionner une catégorie (ManyToOne)

use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use App\Entity\Category;

$builder->add('category', EntityType::class, [
    'class' => Category::class,
    'choice_label' => 'name',  // Propriété à afficher
    'placeholder' => 'Choisir une catégorie'
]);

Sélection multiple (ManyToMany)

$builder->add('tags', EntityType::class, [
    'class' => Tag::class,
    'choice_label' => 'name',
    'multiple' => true,  // Choix multiples
    'expanded' => false  // false = select multiple, true = checkboxes
]);

Avec requête personnalisée

$builder->add('category', EntityType::class, [
    'class' => Category::class,
    'choice_label' => 'name',
    'query_builder' => function (CategoryRepository $repo) {
        return $repo->createQueryBuilder('c')
            ->orderBy('c.name', 'ASC');
    }
]);

Autres types utiles 🛠️

Type Usage
CheckboxType Case à cocher unique (true/false)
FileType Upload de fichier
HiddenType Champ caché
ButtonType Bouton simple
SubmitType Bouton submit
ResetType Bouton reset
CollectionType Collection d'objets (formulaires imbriqués)

Exemples

$builder
    ->add('acceptTerms', CheckboxType::class, [
        'label' => 'J\'accepte les conditions',
        'required' => true
    ])
    ->add('photo', FileType::class, [
        'label' => 'Photo de profil',
        'required' => false
    ])
    ->add('save', SubmitType::class, [
        'label' => 'Enregistrer'
    ])
;
# ✅ Partie 4

Validation des Données

Contraintes et messages d'erreur

Validation dans l'entité 📋

Les contraintes se définissent directement dans l'entité avec des attributs PHP.

src/Entity/Article.php

use Symfony\Component\Validator\Constraints as Assert;

class Article
{
    #[ORM\Column(length: 255)]
    #[Assert\NotBlank(message: 'Le titre ne peut pas être vide')]
    #[Assert\Length(
        min: 5,
        max: 255,
        minMessage: 'Le titre doit faire au moins {{ limit }} caractères',
        maxMessage: 'Le titre ne peut pas dépasser {{ limit }} caractères'
    )]
    private ?string $title = null;

    #[ORM\Column(type: Types::TEXT)]
    #[Assert\NotBlank]
    #[Assert\Length(min: 20)]
    private ?string $content = null;

    #[ORM\Column(length: 180, unique: true)]
    #[Assert\NotBlank]
    #[Assert\Email(message: 'L\'email {{ value }} n\'est pas valide')]
    private ?string $email = null;

    #[ORM\Column]
    #[Assert\Positive(message: 'Le prix doit être positif')]
    private ?float $price = null;
}

Contraintes de validation courantes ✅

Contrainte Description Exemple
NotBlank Non vide #[Assert\NotBlank]
NotNull Pas null #[Assert\NotNull]
Length Longueur min/max #[Assert\Length(min: 3, max: 50)]
Email Email valide #[Assert\Email]
Url URL valide #[Assert\Url]
Regex Expression régulière #[Assert\Regex('/^[0-9]{5}$/')]
Choice Valeur dans liste #[Assert\Choice(['oui', 'non'])]
Range Nombre entre min/max #[Assert\Range(min: 0, max: 100)]
Positive Nombre positif #[Assert\Positive]
DateTime Date/heure valide #[Assert\DateTime]

Validation dans le FormType ⚙️

On peut aussi ajouter des contraintes directement dans le formulaire.

src/Form/ArticleType.php

use Symfony\Component\Validator\Constraints as Assert;

public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder
        ->add('title', TextType::class, [
            'constraints' => [
                new Assert\NotBlank(),
                new Assert\Length(['min' => 5, 'max' => 255])
            ]
        ])
        ->add('email', EmailType::class, [
            'constraints' => [
                new Assert\NotBlank(),
                new Assert\Email()
            ]
        ])
        ->add('age', IntegerType::class, [
            'constraints' => [
                new Assert\Range([
                    'min' => 18,
                    'max' => 120,
                    'notInRangeMessage' => 'Vous devez avoir entre {{ min }} et {{ max }} ans'
                ])
            ]
        ])
    ;
}

💡 Où mettre les contraintes ?

Personnaliser les messages d'erreur 💬

Messages avec paramètres

#[Assert\Length(
    min: 5,
    max: 50,
    minMessage: 'Minimum {{ limit }} caractères (vous en avez {{ value|length }})',
    maxMessage: 'Maximum {{ limit }} caractères'
)]
private ?string $username = null;

Paramètres disponibles

Paramètre Description
{{ value }} Valeur saisie
{{ limit }} Limite définie
{{ min }} Minimum
{{ max }} Maximum

Affichage dans Twig

<div class="form-group">
    {{ form_label(form.username) }}
    {{ form_widget(form.username) }}
    
    {# Les erreurs s'affichent automatiquement #}
    {{ form_errors(form.username) }}
</div>
# 💾 Partie 5

Traiter la Soumission

handleRequest et isValid

Cycle de vie d'un formulaire 🔄

  1. Création de l'entité
    $article = new Article();
  2. Création du formulaire
    $form = $this->createForm(ArticleType::class, $article);
  3. Traitement de la requête
    $form->handleRequest($request);
  4. Vérification soumission
    if ($form->isSubmitted())
  5. Vérification validation
    if ($form->isValid())
  6. Traitement des données
    Les données sont déjà dans $article !
  7. Sauvegarde
    $entityManager->flush();

Exemple complet - CREATE 🆕

#[Route('/article/new', name: 'article_new')]
public function new(Request $request, EntityManagerInterface $em): Response
{
    $article = new Article();
    $form = $this->createForm(ArticleType::class, $article);
    
    $form->handleRequest($request);
    
    if ($form->isSubmitted() && $form->isValid()) {
        // Les données du formulaire sont automatiquement dans $article
        // grâce au data_class dans ArticleType
        
        $em->persist($article);
        $em->flush();
        
        $this->addFlash('success', 'Article créé !');
        
        return $this->redirectToRoute('article_show', [
            'id' => $article->getId()
        ]);
    }
    
    return $this->render('article/new.html.twig', [
        'form' => $form
    ]);
}

Exemple complet - UPDATE ✏️

#[Route('/article/{id}/edit', name: 'article_edit')]
public function edit(
    Article $article,  // ParamConverter récupère l'article
    Request $request, 
    EntityManagerInterface $em
): Response {
    $form = $this->createForm(ArticleType::class, $article);
    
    $form->handleRequest($request);
    
    if ($form->isSubmitted() && $form->isValid()) {
        // Pas besoin de persist() car $article existe déjà en base
        $em->flush();
        
        $this->addFlash('success', 'Article modifié !');
        
        return $this->redirectToRoute('article_show', [
            'id' => $article->getId()
        ]);
    }
    
    return $this->render('article/edit.html.twig', [
        'form' => $form,
        'article' => $article
    ]);
}

💡 ParamConverter :

Symfony convertit automatiquement {id} en objet Article si vous le typez dans les paramètres !

Messages flash (feedback utilisateur) 💬

Dans le contrôleur

$this->addFlash('success', 'Opération réussie !');
$this->addFlash('error', 'Une erreur s\'est produite.');
$this->addFlash('warning', 'Attention !');
$this->addFlash('info', 'Information importante.');

Dans le template (base.html.twig)

{% for message in app.flashes('success') %}
    <div class="alert alert-success">
        {{ message }}
    </div>
{% endfor %}

{% for message in app.flashes('error') %}
    <div class="alert alert-danger">
        {{ message }}
    </div>
{% endfor %}

✅ Bonne pratique : Toujours donner un feedback à l'utilisateur après une action !

# 📤 Partie 6

Upload de Fichiers

Gérer les images et documents

Ajouter un champ FileType 📁

Dans le formulaire

use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;

$builder->add('photo', FileType::class, [
    'label' => 'Photo (JPG, PNG)',
    'mapped' => false,  // Ne pas mapper à l'entité directement
    'required' => false,
    'constraints' => [
        new File([
            'maxSize' => '2M',
            'mimeTypes' => ['image/jpeg', 'image/png'],
            'mimeTypesMessage' => 'Veuillez uploader une image JPG ou PNG valide',
        ])
    ]
]);

⚠️ mapped => false :

Le fichier n'est pas directement mappé à l'entité. On le traite manuellement dans le contrôleur.

Traiter l'upload dans le contrôleur 💾

use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\String\Slugger\SluggerInterface;

#[Route('/article/new', name: 'article_new')]
public function new(
    Request $request, 
    EntityManagerInterface $em,
    SluggerInterface $slugger
): Response {
    $article = new Article();
    $form = $this->createForm(ArticleType::class, $article);
    
    $form->handleRequest($request);
    
    if ($form->isSubmitted() && $form->isValid()) {
        // Récupérer le fichier uploadé
        $photoFile = $form->get('photo')->getData();
        
        if ($photoFile) {
            // Nom de fichier unique et sécurisé
            $originalFilename = pathinfo(
                $photoFile->getClientOriginalName(), 
                PATHINFO_FILENAME
            );
            $safeFilename = $slugger->slug($originalFilename);
            $newFilename = $safeFilename.'-'.uniqid().'.'.$photoFile->guessExtension();
            
            // Déplacer le fichier
            try {
                $photoFile->move(
                    $this->getParameter('photos_directory'),  // public/uploads/photos
                    $newFilename
                );
            } catch (FileException $e) {
                $this->addFlash('error', 'Erreur lors de l\'upload');
            }
            
            // Stocker le nom dans l'entité
            $article->setPhotoFilename($newFilename);
        }
        
        $em->persist($article);
        $em->flush();
        
        return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
    }
    
    return $this->render('article/new.html.twig', ['form' => $form]);
}

Configuration des uploads ⚙️

config/services.yaml

parameters:
    photos_directory: '%kernel.project_dir%/public/uploads/photos'

Créer le dossier

mkdir -p public/uploads/photos

Dans l'entité

class Article
{
    #[ORM\Column(length: 255, nullable: true)]
    private ?string $photoFilename = null;

    public function getPhotoFilename(): ?string
    {
        return $this->photoFilename;
    }

    public function setPhotoFilename(?string $photoFilename): void
    {
        $this->photoFilename = $photoFilename;
    }
}

Afficher l'image dans Twig

{% if article.photoFilename %}
    <img src="{{ asset('uploads/photos/' ~ article.photoFilename) }}" 
         alt="{{ article.title }}">
{% endif %}
# 🔐 Partie 7

Protection CSRF

Cross-Site Request Forgery

C'est quoi le CSRF ? 🤔

Analogie : L'usurpation d'identité bancaire 🏦

Le CSRF = quelqu'un fait une action en ton nom sans que tu le saches :

Solution CSRF : Un jeton secret unique par formulaire que seul le vrai site connaît !

Protection CSRF automatique 🛡️

✅ Bonne nouvelle : Symfony protège automatiquement tous les formulaires avec CSRF !

Token généré automatiquement

{{ form_start(form) }}
    {# Un champ caché avec le token CSRF est ajouté automatiquement #}
    <input type="hidden" name="_token" value="xyz123abc...">
    
    {{ form_row(form.title) }}
    {{ form_row(form.content) }}
    
{{ form_end(form) }}

Vérification automatique

$form->handleRequest($request);

// Symfony vérifie automatiquement le token CSRF
if ($form->isSubmitted() && $form->isValid()) {
    // Si on arrive ici, le token est valide !
}

💡 Pas besoin de code supplémentaire : La protection CSRF est active par défaut !

Désactiver CSRF (rarement nécessaire) ⚠️

Dans certains cas spécifiques (API), on peut désactiver CSRF :

Dans le FormType

public function configureOptions(OptionsResolver $resolver): void
{
    $resolver->setDefaults([
        'data_class' => Article::class,
        'csrf_protection' => false,  // ← Désactiver CSRF
    ]);
}

⚠️ DANGER : Ne désactivez CSRF que si vous savez exactement ce que vous faites ! Gardez-le activé pour les formulaires web classiques.

# 🎨 Partie 8

Personnaliser le Rendu

Thèmes et templates personnalisés

Thèmes de formulaire intégrés 🎨

Symfony propose des thèmes pour s'adapter aux frameworks CSS populaires.

Bootstrap 5 (recommandé)

{# config/packages/twig.yaml #}
twig:
    form_themes:
        - 'bootstrap_5_layout.html.twig'

Bootstrap 4

twig:
    form_themes:
        - 'bootstrap_4_layout.html.twig'

Foundation

twig:
    form_themes:
        - 'foundation_5_layout.html.twig'

Tailwind (avec bundle)

composer require tales-from-a-dev/flowbite-bundle
# Puis configurer dans twig.yaml

Personnaliser un champ spécifique 🎯

Ajouter des classes CSS

$builder->add('title', TextType::class, [
    'attr' => [
        'class' => 'form-control mon-input',
        'placeholder' => 'Saisissez le titre',
        'data-tooltip' => 'Aide contextuelle'
    ]
]);

Personnaliser le label

$builder->add('title', TextType::class, [
    'label' => 'Titre de l\'article',
    'label_attr' => [
        'class' => 'font-bold text-lg'
    ]
]);

Ajouter de l'aide

$builder->add('email', EmailType::class, [
    'help' => 'Nous ne partagerons jamais votre email',
    'help_attr' => [
        'class' => 'text-muted small'
    ]
]);

Templates de formulaire personnalisés 🖌️

Créer un template personnalisé

templates/form/custom_form.html.twig

{% block form_row %}
    <div class="custom-form-group mb-4">
        {{ form_label(form) }}
        {{ form_widget(form) }}
        {{ form_errors(form) }}
        
        {% if help is defined %}
            <small class="form-help">{{ help }}</small>
        {% endif %}
    </div>
{% endblock %}

{% block _article_title_widget %}
    {# Widget personnalisé pour le champ 'title' de ArticleType #}
    <input type="text" {{ block('widget_attributes') }} class="special-input">
{% endblock %}

Utiliser dans un template

{% form_theme form 'form/custom_form.html.twig' %}

{{ form_start(form) }}
    {{ form_row(form.title) }}
    {{ form_row(form.content) }}
{{ form_end(form) }}
# 🎭 Partie 9

Formulaires Imbriqués

CollectionType et sous-formulaires

CollectionType - Collection d'objets 📦

Cas d'usage : Un formulaire Article avec plusieurs images.

Entités

class Article
{
    #[ORM\OneToMany(targetEntity: Image::class, mappedBy: 'article', cascade: ['persist'])]
    private Collection $images;

    public function __construct()
    {
        $this->images = new ArrayCollection();
    }
    
    // getters, adders, removers...
}

class Image
{
    #[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'images')]
    private ?Article $article = null;
    
    #[ORM\Column(length: 255)]
    private ?string $filename = null;
}

Créer les FormTypes imbriqués 🏗️

ImageType.php

class ImageType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('filename', TextType::class, [
                'label' => 'Nom du fichier'
            ])
            ->add('description', TextType::class, [
                'required' => false
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Image::class,
        ]);
    }
}

ArticleType.php avec CollectionType

use Symfony\Component\Form\Extension\Core\Type\CollectionType;

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class)
            ->add('content', TextareaType::class)
            ->add('images', CollectionType::class, [
                'entry_type' => ImageType::class,
                'entry_options' => ['label' => false],
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false,
            ])
        ;
    }
}

Afficher la collection dans Twig 🎨

{{ form_start(form) }}
    {{ form_row(form.title) }}
    {{ form_row(form.content) }}

    <h3>Images</h3>
    <div id="images-collection" data-prototype="{{ form_widget(form.images.vars.prototype)|e('html_attr') }}">
        {% for imageForm in form.images %}
            <div class="image-item">
                {{ form_row(imageForm.filename) }}
                {{ form_row(imageForm.description) }}
                <button type="button" class="remove-image">Supprimer</button>
            </div>
        {% endfor %}
    </div>

    <button type="button" id="add-image">Ajouter une image</button>

    <button type="submit">Enregistrer</button>
{{ form_end(form) }}

<script>
// JavaScript pour ajouter/supprimer dynamiquement des images
document.getElementById('add-image').addEventListener('click', function() {
    const collection = document.getElementById('images-collection');
    const prototype = collection.dataset.prototype;
    const index = collection.children.length;
    
    const newForm = prototype.replace(/__name__/g, index);
    collection.insertAdjacentHTML('beforeend', newForm);
});
</script>
# 🎓 Récapitulatif

Ce qu'on a appris

Workflow complet d'un formulaire 🔄

  1. Créer le FormType
    php bin/console make:form ArticleType
  2. Ajouter les champs
    $builder->add('title', TextType::class)
  3. Créer dans le contrôleur
    $form = $this->createForm(ArticleType::class, $article);
  4. Traiter la requête
    $form->handleRequest($request);
  5. Vérifier soumission et validation
    if ($form->isSubmitted() && $form->isValid())
  6. Sauvegarder
    $entityManager->flush();
  7. Afficher dans Twig
    {{ form(form) }}

Types de champs - Résumé 📊

Catégorie Types disponibles
Texte TextType, TextareaType, EmailType, PasswordType, UrlType, TelType, SearchType
Nombres IntegerType, NumberType, MoneyType, PercentType, RangeType
Date/Heure DateType, TimeType, DateTimeType, BirthdayType
Choix ChoiceType, EntityType, CountryType, LanguageType, CurrencyType
Autres CheckboxType, FileType, HiddenType, CollectionType
Boutons SubmitType, ButtonType, ResetType

Contraintes de validation - Résumé ✅

Contrainte Usage
NotBlank Champ obligatoire
Length(min, max) Longueur du texte
Email Format email valide
Url Format URL valide
Range(min, max) Nombre entre min et max
Positive / Negative Nombre positif/négatif
Choice([...]) Valeur dans une liste
Regex(pattern) Expression régulière
File(maxSize, mimeTypes) Upload de fichier

Fonctions Twig - Résumé 🎨

Fonction Usage
{{ form(form) }} Formulaire complet en une ligne
{{ form_start(form) }} Balise <form> ouvrante + CSRF
{{ form_end(form) }} Balise </form> + champs cachés
{{ form_row(form.field) }} Label + Widget + Erreurs
{{ form_label(form.field) }} Label uniquement
{{ form_widget(form.field) }} Champ uniquement (input, select...)
{{ form_errors(form.field) }} Messages d'erreur
{{ form_rest(form) }} Champs non encore affichés

Bonnes pratiques 💡

  1. Un FormType par entité
    Réutilisable et organisé
  2. Validation dans l'entité
    Les contraintes métier restent avec le modèle
  3. Protection CSRF activée
    Sécurité par défaut, ne jamais désactiver sauf raison valable
  4. Messages flash après action
    Toujours donner un feedback utilisateur
  5. Redirection après POST
    Pattern PRG (Post-Redirect-Get) évite double soumission
  6. Utiliser les thèmes CSS
    Bootstrap, Foundation... pour un rendu pro
  7. Upload sécurisés
    Validation du type, taille, nom unique

Erreurs courantes à éviter ⚠️

❌ Erreur 1 : Oublier handleRequest()

$form = $this->createForm(ArticleType::class, $article);
// ❌ Oublié handleRequest() !
if ($form->isSubmitted()) { ... }

// ✅ BON
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { ... }

❌ Erreur 2 : Pas de redirection après POST

if ($form->isSubmitted() && $form->isValid()) {
    $em->flush();
    return $this->render('success.html.twig');  // ❌ Pas de redirect !
}

// ✅ BON
return $this->redirectToRoute('article_show', ['id' => $article->getId()]);

❌ Erreur 3 : Modifier le HTML sans form_end()

{{ form_start(form) }}
{{ form_row(form.title) }}
</form>  {# ❌ Pas de form_end() = pas de CSRF ! #}

// ✅ BON
{{ form_end(form) }}

Ressources pour aller plus loin 📚

Documentation Symfony Forms

🌐 https://symfony.com/doc/current/forms.html

Guide complet des formulaires

Types de champs

🌐 https://symfony.com/doc/current/reference/forms/types.html

Référence de tous les types disponibles

Validation

🌐 https://symfony.com/doc/current/validation.html

Toutes les contraintes de validation

Personnalisation

🌐 https://symfony.com/doc/current/form/form_customization.html

Personnaliser le rendu des formulaires

Ce que vous maîtrisez maintenant ! 💪

Compétences acquises :

💡 Prochaine étape : Sécurité et Authentification !

🎓 Félicitations !

Vous maîtrisez les Formulaires Symfony

Vous savez maintenant :

✅ Créer des formulaires puissants

✅ Valider automatiquement les données

✅ Gérer uploads et fichiers

✅ Protéger vos formulaires

💡 Conseil final :

Les formulaires Symfony peuvent sembler complexes au début, mais une fois maîtrisés, ils vous feront gagner un temps fou ! Pratiquez avec des formulaires réels.

Prochaine étape : Sécurité ! 🔐

Formation DWWM - 2026