Symfony FormType : le guide ultime des formulaires
Le composant Form de Symfony est l'un des plus riches et des plus complexes du framework. Il gere la creation de formulaires, la liaison de donnees, la validation, les transformations et le rendu HTML - le tout de maniere fortement typee et extensible. Ce guide couvre tout ce que vous devez savoir pour creer des formulaires robustes avec Symfony 7.2 et PHP 8.3.
Creer un FormType custom
La premiere chose a faire quand vous creez un formulaire dans Symfony, c'est de creer une classe FormType dediee. Ne definissez jamais vos champs directement dans le controller - c'est un anti-pattern qui rend le code impossible a tester et a reutiliser :
<?php
namespace App\Form;
use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
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 ProductType extends AbstractType
{
public function buildForm(
FormBuilderInterface $builder,
array $options
): void {
$builder
->add('name', TextType::class, [
'label' => 'Nom du produit',
'attr' => [
'placeholder' => 'Ex : T-shirt coton bio',
'maxlength' => 255,
],
'help' => 'Le nom apparaitra sur la fiche produit.',
])
->add('description', TextareaType::class, [
'label' => 'Description',
'required' => false,
'attr' => ['rows' => 5],
])
->add('price', MoneyType::class, [
'label' => 'Prix HT',
'currency' => 'EUR',
'divisor' => 100, // stocke en centimes
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Product::class,
]);
}
}
Vous pouvez generer automatiquement un FormType avec la commande php bin/console make:form du MakerBundle. La commande analysera votre entite et generera les champs correspondants.
Utiliser le formulaire dans un controller
<?php
namespace App\Controller;
use App\Entity\Product;
use App\Form\ProductType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/products/new', name: 'product_new', methods: ['GET', 'POST'])]
public function new(
Request $request,
EntityManagerInterface $em
): Response {
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($product);
$em->flush();
$this->addFlash('success', 'Produit cree avec succes.');
return $this->redirectToRoute('product_show', [
'id' => $product->getId(),
]);
}
return $this->render('product/new.html.twig', [
'form' => $form,
]);
}
}
Notez qu'a partir de Symfony 6.2+, vous pouvez passer l'objet $form directement a Twig sans appeler ->createView(). Symfony le fait automatiquement.
Les types de champs natifs essentiels
Symfony fournit des dizaines de types de champs. Voici les plus utilises avec leurs options cles :
TextType et TextareaType
$builder
->add('name', TextType::class, [
'label' => 'Nom',
'required' => true,
'trim' => true, // supprime les espaces
'empty_data' => '', // valeur si champ vide
])
->add('bio', TextareaType::class, [
'label' => 'Biographie',
'required' => false,
'attr' => ['rows' => 8, 'class' => 'wysiwyg'],
]);
ChoiceType
$builder->add('status', ChoiceType::class, [
'label' => 'Statut',
'choices' => [
'Brouillon' => 'draft',
'Publie' => 'published',
'Archive' => 'archived',
],
'expanded' => false, // false = select, true = radio/checkbox
'multiple' => false, // true = selection multiple
'placeholder' => 'Choisir un statut',
]);
EntityType
EntityType permet de selectionner une ou plusieurs entites Doctrine dans un formulaire. C'est le type a utiliser pour les relations ManyToOne et ManyToMany. Pour un guide complet, consultez notre article dedie sur EntityType et la selection d'entites.
use App\Entity\Category;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
$builder->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
'placeholder' => 'Selectionner une categorie',
'query_builder' => function (CategoryRepository $repo) {
return $repo->createQueryBuilder('c')
->where('c.active = true')
->orderBy('c.name', 'ASC');
},
]);
CollectionType
Pour les relations OneToMany editables (ajouter/supprimer des sous-elements dynamiquement) :
$builder->add('items', CollectionType::class, [
'entry_type' => OrderItemType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false, // important pour les orphanRemoval
'prototype' => true, // genere un template JS
]);
DateType et DateTimeType
$builder
->add('startDate', DateType::class, [
'widget' => 'single_text', // input type="date" HTML5
'input' => 'datetime_immutable',
'label' => 'Date de debut',
])
->add('deadline', DateTimeType::class, [
'widget' => 'single_text',
'input' => 'datetime_immutable',
]);
Events de formulaire : logique dynamique
Les events permettent de modifier le formulaire en fonction du contexte (donnees existantes, valeurs soumises, permissions). Les deux events les plus utilises sont PRE_SET_DATA et POST_SUBMIT :
<?php
namespace App\Form;
use App\Entity\Address;
use App\Repository\CityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddressType extends AbstractType
{
public function __construct(
private readonly CityRepository $cityRepository,
) {}
public function buildForm(
FormBuilderInterface $builder,
array $options
): void {
$builder->add('country', ChoiceType::class, [
'choices' => [
'France' => 'FR',
'Belgique' => 'BE',
'Luxembourg' => 'LU',
],
'placeholder' => 'Choisir un pays',
]);
// Ajouter le champ ville en fonction du pays initial
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event): void {
/** @var Address|null $address */
$address = $event->getData();
$country = $address?->getCountry();
$this->addCityField($event->getForm(), $country);
}
);
// Mettre a jour le champ ville quand le pays change
$builder->get('country')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event): void {
$country = $event->getForm()->getData();
$form = $event->getForm()->getParent();
$this->addCityField($form, $country);
}
);
}
private function addCityField(
FormInterface $form,
?string $country
): void {
$cities = $country
? $this->cityRepository->findByCountry($country)
: [];
$form->add('city', EntityType::class, [
'class' => City::class,
'choices' => $cities,
'choice_label' => 'name',
'placeholder' => $country
? 'Choisir une ville'
: 'Selectionnez d'abord un pays',
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Address::class,
]);
}
}
Ce pattern est extremement puissant pour les formulaires avec des champs dependants (pays/ville, categorie/sous-categorie, marque/modele). Le champ ville est recree automatiquement en fonction du pays selectionne.
Data Transformers
Les data transformers convertissent les donnees entre le format du formulaire (ce que l'utilisateur voit) et le format de l'application (ce que votre code manipule). Deux types existent :
- Model transformer - entre le modele (entite) et le format normalise.
- View transformer - entre le format normalise et la vue (HTML).
<?php
namespace App\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Transforme un tableau de tags en chaine separee par des virgules.
* @implements DataTransformerInterface<array, string>
*/
class TagsTransformer implements DataTransformerInterface
{
/**
* Model -> View : tableau -> chaine
*/
public function transform(mixed $value): string
{
if ($value === null) {
return '';
}
return implode(', ', $value);
}
/**
* View -> Model : chaine -> tableau
*/
public function reverseTransform(mixed $value): array
{
if ($value === null || $value === '') {
return [];
}
$tags = array_map('trim', explode(',', $value));
// Filtrer les tags vides et valider
$tags = array_filter($tags, fn(string $tag) => $tag !== '');
if (count($tags) > 10) {
throw new TransformationFailedException(
'Maximum 10 tags autorises.'
);
}
return array_values($tags);
}
}
// Utilisation dans un FormType :
$builder->add('tags', TextType::class, [
'label' => 'Tags (separes par des virgules)',
]);
$builder->get('tags')->addModelTransformer(
new TagsTransformer()
);
Form themes : personnaliser le rendu
Symfony fournit des form themes pour Bootstrap 5 et Tailwind CSS. Configurez-les globalement dans twig.yaml :
# config/packages/twig.yaml
twig:
form_themes:
- 'bootstrap_5_layout.html.twig'
# ou pour Tailwind :
# - 'tailwind_2_layout.html.twig'
Pour personnaliser un champ specifique, creez un bloc Twig nomme selon la convention _nomDuForm_nomDuChamp_widget :
{# templates/product/new.html.twig #}
{% form_theme form _self %}
{% block _product_name_widget %}
<div class="custom-input-wrapper">
{{ form_widget(form, {'attr': {'class': 'custom-input'}}) }}
<span class="input-icon">📦</span>
</div>
{% endblock %}
{% block content %}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.description) }}
{{ form_row(form.price) }}
<button type="submit" class="btn btn-primary">Enregistrer</button>
{{ form_end(form) }}
{% endblock %}
Validation integree
Le composant Form travaille main dans la main avec le composant Validator. Les contraintes de validation definies sur l'entite sont automatiquement verifiees lors de $form->isValid(). Pour un guide complet sur le Validator, consultez notre article sur les contraintes de validation.
<?php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Product
{
#[Assert\NotBlank(message: 'Le nom est obligatoire.')]
#[Assert\Length(
min: 3,
max: 255,
minMessage: 'Le nom doit faire au moins {{ limit }} caracteres.',
maxMessage: 'Le nom ne peut pas depasser {{ limit }} caracteres.',
)]
private string $name;
#[Assert\NotNull]
#[Assert\Positive(message: 'Le prix doit etre positif.')]
private float $price;
#[Assert\Length(max: 5000)]
private ?string $description = null;
}
Vous pouvez aussi ajouter des groupes de validation dans votre FormType :
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Product::class,
'validation_groups' => ['Default', 'product_creation'],
]);
}
Options custom dans un FormType
Vous pouvez ajouter vos propres options pour rendre un FormType configurable et reutilisable :
class ProductType extends AbstractType
{
public function buildForm(
FormBuilderInterface $builder,
array $options
): void {
$builder->add('name', TextType::class);
$builder->add('price', MoneyType::class, [
'currency' => $options['currency'],
]);
// Champ conditionnel
if ($options['include_seo']) {
$builder
->add('metaTitle', TextType::class, [
'required' => false,
])
->add('metaDescription', TextareaType::class, [
'required' => false,
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Product::class,
'currency' => 'EUR',
'include_seo' => false,
]);
$resolver->setAllowedTypes('currency', 'string');
$resolver->setAllowedTypes('include_seo', 'bool');
}
}
// Utilisation dans le controller :
$form = $this->createForm(ProductType::class, $product, [
'currency' => 'USD',
'include_seo' => true,
]);
Bonnes pratiques
- Toujours creer des classes FormType - jamais de
createFormBuilder()dans le controller. - Validation sur l'entite, pas dans le formulaire - sauf pour des regles specifiques au contexte du formulaire.
- Utilisez
data_class- le binding automatique evite le mapping manuel. - Attention a
by_reference- mettez-le afalsepour les CollectionType avec orphanRemoval. - Nommez vos FormType clairement -
ProductType,ProductEditType,ProductFilterType. - Testez vos formulaires - le composant Form fournit
TypeTestCasepour les tests unitaires.
Conclusion
Le systeme de formulaires de Symfony est l'un des plus complets de l'ecosysteme PHP. Les FormType custom, les events, les data transformers et les form themes vous donnent un controle total sur le cycle de vie du formulaire, de la construction au rendu. La courbe d'apprentissage est reelle, mais l'investissement paie sur la duree en termes de maintenabilite et de robustesse.
Pour serialiser les donnees de vos formulaires vers une API, decouvrez notre guide complet du Serializer. Besoin d'aide pour un projet avec des formulaires complexes ? Consultez mes services de developpement, les tarifs, ou contactez-moi pour en discuter.
