1 / 12

📖 Les Formulaires Symfony 6.4

Guide Complet pour Débutants

🔒

Sécurité

Protection automatique contre les attaques CSRF et XSS

✅

Validation

Validation automatique des données côté serveur

♻️

Réutilisabilité

Un formulaire utilisable dans plusieurs contextes

🛠️

Maintenance

Modifications centralisées et faciles

💡 Objectif de ce guide : Apprendre à créer des formulaires Symfony complets, de la création de l'entité au template final.

🎯 Cas d'usage concrets

Formulaires avec persistance

  • 📝 Inscription utilisateur
    Création de compte avec validation
  • 👤 Mise à jour profil
    Modification des informations personnelles
  • 📄 Création d'articles
    Ajout de contenu avec rich text
  • 🗂️ Gestion administrative
    CRUD complet pour les entités

Formulaires sans persistance

  • 📧 Formulaires de contact
    Envoi d'emails directement
  • 🔍 Filtres de recherche
    Recherche et filtrage de données
  • 📊 Rapports dynamiques
    Génération de statistiques
  • 🔐 Authentification
    Connexion et réinitialisation

📋 Étape 1 : Créer l'entité

Qu'est-ce qu'une entité ?
Une entité représente une table dans votre base de données. C'est le "modèle" qui définit quelles informations vous voulez stocker.
<?php
// src/Entity/Article.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity]
class Article
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank(message: 'Le titre est obligatoire')]
    #[Assert\Length(
        min: 5,
        max: 255,
        minMessage: 'Le titre doit contenir au moins {{ limit }} caractères'
    )]
    private ?string $title = null;

    #[ORM\Column(type: 'text')]
    #[Assert\NotBlank(message: 'Le contenu est obligatoire')]
    private ?string $content = null;
    
    // Getters et setters...
}
💡 Annotations de validation importantes :
• #[Assert\NotBlank] : champ obligatoire
• #[Assert\Length] : contraintes de longueur
• #[Assert\Email] : validation email

📋 Étape 2 : Créer le FormType

Qu'est-ce qu'un FormType ?
C'est une classe qui définit la structure de votre formulaire : champs, types, options, validation...
<?php
// src/Form/ArticleType.php
namespace App\Form;

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

class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, [
                'label' => 'Titre de l\'article',
                'attr' => [
                    'placeholder' => 'Saisissez le titre',
                    'class' => 'form-control'
                ]
            ])
            ->add('content', TextareaType::class, [
                'label' => 'Contenu',
                'attr' => [
                    'rows' => 10,
                    'class' => 'form-control'
                ]
            ])
            ->add('submit', SubmitType::class, [
                'label' => 'Publier',
                'attr' => ['class' => 'btn btn-primary']
            ]);
    }
}

🔧 Types de champs courants

Champs de base

// Texte simple
TextType::class

// Zone de texte
TextareaType::class

// Email avec validation
EmailType::class

// Mot de passe
PasswordType::class

// Nombre
NumberType::class

// Date
DateType::class

Champs avancés

// Liste déroulante
ChoiceType::class

// Relation avec entité
EntityType::class

// Upload de fichier
FileType::class

// Case à cocher
CheckboxType::class

// Boutons radio
RadioType::class

// Collection dynamique
CollectionType::class
💡 Exemple avec options :
->add('category', ChoiceType::class, [
    'choices' => [
        'Technologie' => 'tech',
        'Sport' => 'sport',
        'Culture' => 'culture'
    ],
    'placeholder' => 'Choisir une catégorie',
    'required' => false
])

📋 Étape 3 : Créer le contrôleur

Qu'est-ce qu'un contrôleur ?
C'est la classe qui contient la logique : elle reçoit les requêtes HTTP, traite les données, et retourne une réponse.
<?php
// src/Controller/ArticleController.php
namespace App\Controller;

use App\Entity\Article;
use App\Form\ArticleType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ArticleController extends AbstractController
{
    #[Route('/article/new', name: 'article_new')]
    public function new(Request $request, EntityManagerInterface $entityManager): 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 valide
        if ($form->isSubmitted() && $form->isValid()) {
            $article->setCreatedAt(new \DateTime());
            
            // 5. Sauvegarder
            $entityManager->persist($article);
            $entityManager->flush();
            
            $this->addFlash('success', 'Article créé !');
            return $this->redirectToRoute('article_show', ['id' => $article->getId()]);
        }
        
        // 6. Afficher le formulaire
        return $this->render('article/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

🔄 Cycle de vie du formulaire

  1. createForm()
    Crée l'instance du formulaire liée à l'entité
  2. handleRequest()
    Traite automatiquement les données POST du formulaire
  3. isSubmitted()
    Vérifie si le formulaire a été soumis par l'utilisateur
  4. isValid()
    Vérifie si les données respectent les règles de validation
  5. persist()
    Prépare l'objet pour la sauvegarde en base de données
  6. flush()
    Exécute réellement la sauvegarde en base de données
💡 Important : Pour la modification, pas besoin de persist() car l'objet existe déjà en base !

📋 Étape 4 : Créer le template

Qu'est-ce qu'un template ?
C'est un fichier Twig qui définit l'affichage HTML de votre formulaire.
{# templates/article/new.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Nouvel Article{% endblock %}

{% block body %}
<div class="container mt-5">
    <h1>Créer un nouvel article</h1>
    
    {# Messages flash #}
    {% for type, messages in app.flashes %}
        {% for message in messages %}
            <div class="alert alert-{{ type == 'success' ? 'success' : 'danger' }}">
                {{ message }}
            </div>
        {% endfor %}
    {% endfor %}
    
    {# Formulaire #}
    {{ form_start(form) }}
        {{ form_widget(form) }}
    {{ form_end(form) }}
</div>
{% endblock %}
💡 Fonctions Twig importantes :
• form_start(form) : balise d'ouverture
• form_widget(form) : affichage automatique
• form_end(form) : balise de fermeture

🎨 Rendu manuel vs automatique

Rendu automatique (rapide)

{{ form_start(form) }}
    {{ form_widget(form) }}
{{ form_end(form) }}
✅ Plus rapide à implémenter
✅ Moins de code à maintenir
❌ Moins de contrôle sur le design

Rendu manuel (contrôle)

{{ form_start(form) }}
    <div class="mb-3">
        {{ form_label(form.title) }}
        {{ form_widget(form.title) }}
        {{ form_errors(form.title) }}
    </div>
    
    <div class="mb-3">
        {{ form_label(form.content) }}
        {{ form_widget(form.content) }}
        {{ form_errors(form.content) }}
    </div>
    
    {{ form_widget(form.submit) }}
{{ form_end(form) }}
✅ Contrôle total du design
✅ Personnalisation avancée
❌ Plus de code à maintenir

⚠️ Erreurs fréquentes et solutions

Erreur : "Expected argument of type 'string', 'null' given"
Solution :
// ❌ Problème
private string $title;

// ✅ Solution : Rendre nullable
private ?string $title = null;

// ✅ Ou rendre facultatif
->add('title', TextType::class, [
    'required' => false,
])
Erreur : "Token CSRF invalide"
Solution : Toujours utiliser form_start() et form_end()
Erreur : "Cannot automatically determine child form name"
Solution :
// ❌ Problème
->add('category', EntityType::class, [
    'class' => Category::class,
])

// ✅ Solution
->add('category', EntityType::class, [
    'class' => Category::class,
    'choice_label' => 'name',
])

🎯 Exemple complet : Formulaire de contact

Objectif : Créer un formulaire de contact qui envoie un email (sans persistance en base)
// src/Entity/Contact.php (pas de mapping ORM)
class Contact
{
    #[Assert\NotBlank(message: 'Le nom est requis')]
    #[Assert\Length(min: 2, max: 50)]
    private ?string $name = null;

    #[Assert\NotBlank(message: 'L\'email est requis')]
    #[Assert\Email(message: 'L\'email n\'est pas valide')]
    private ?string $email = null;

    #[Assert\NotBlank(message: 'Le message est requis')]
    #[Assert\Length(min: 10)]
    private ?string $message = null;
    
    // Getters et setters...
}
// src/Form/ContactType.php
class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, ['label' => 'Votre nom'])
            ->add('email', EmailType::class, ['label' => 'Votre email'])
            ->add('message', TextareaType::class, ['label' => 'Message'])
            ->add('submit', SubmitType::class, ['label' => 'Envoyer']);
    }
}

🎯 Bonnes pratiques et conclusion

🔒

Sécurité

Toujours valider côté serveur, utiliser les groupes de validation

⚡

Performance

Optimiser les requêtes EntityType avec query_builder

🎨

UX/UI

Utiliser les attributs HTML5, placeholders utiles

🏗️

Organisation

Sous-formulaires pour entités complexes

🚀 Commandes utiles :
# Générer un formulaire
php bin/console make:form

# Créer une entité
php bin/console make:entity

# Générer un CRUD complet
php bin/console make:crud