Ce qui se passe
Un formulaire simple en Symfony demande aujourd'hui deux classes : le DTO qui porte les données, et le type de formulaire qui redécrit les mêmes champs juste à côté. Symfony 8.2 permet de supprimer la seconde, en posant deux attributs sur la première. Contribution de Benjamin Georgeault, PR #60563.
// src/Dto/UserDto.php
namespace App\Dto;
use Symfony\Component\Form\Attribute\AsFormType;
use Symfony\Component\Form\Attribute\FormField;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
#[AsFormType(options: ['label' => 'User'])]
class UserDto
{
// pas de type : il est deviné, comme avec $builder->add()
#[FormField]
public ?string $name = null;
#[FormField(EmailType::class)]
public ?string $email = null;
#[FormField(TextareaType::class, ['label' => 'Bio'])]
public ?string $bio = null;
// champ « publicName », mappé ici via l'option property_path
#[FormField(name: 'publicName')]
public ?string $internalName = null;
}Le nom de la classe s'utilise ensuite partout où un nom de type de formulaire est attendu :
$form = $this->createForm(UserDto::class, $userDto);Ce que ça change vraiment
L'intérêt n'est pas la concision, c'est le moment où l'erreur se voit. Symfony lit ces attributs en compilant le conteneur de services et fabrique un service de type de formulaire ordinaire. Un nom de champ en double échoue donc à la construction du conteneur, pas à la première requête qui affiche le formulaire.
Cela suppose que la classe soit scannée par le conteneur avec
l'autoconfiguration. La configuration livrée par défaut couvre src/ — mais
elle exclut src/Entity/, ce qui est à garder en tête si vos données
vivent là.
Les limites à connaître avant de migrer
- Les valeurs d'options doivent être des scalaires, des tableaux ou des énumérations. Les fonctions anonymes sont rejetées, même si PHP 8.5 les accepte dans un attribut.
- Pour passer un service, on utilise un objet
Reference, injecté normalement par le conteneur :
#[FormField(ChoiceType::class, [
'choice_loader' => new Reference(TagChoiceLoader::class),
])]
public array $tags = [];- Les chaînes littérales restent littérales : un
%dans'label' => '100% free'n'est pas pris pour un paramètre de conteneur. - Les attributs ne décrivent que des champs. Événements de formulaire,
transformateurs de données et champs calculés au moment de l'exécution
passent par une extension de type qui cible la classe de données, via
getExtendedTypes(). - Quand une classe portant
#[AsFormType]en étend une autre, l'ancêtre le plus proche portant aussi l'attribut devient le type parent et ses champs sont hérités ; sinon le parent estFormType.
À retenir
Rien n'est déprécié. Symfony le dit clairement : les classes de formulaire restent la bonne réponse dès qu'un formulaire demande autre chose qu'une liste de champs. La nouveauté vise les formulaires simples adossés à des DTO — c'est précisément là que la classe séparée n'apportait rien.
Source : New in Symfony 8.2: Attribute-based Forms, Symfony Blog, 17 septembre 2026.