Le composant Validator de Symfony n'a pratiquement pas changé de philosophie depuis des années : on écrit une classe de contrainte, un validator associé, on les enregistre dans le container, et pour tout ce qui sort du cadre standard on se rabat sur la contrainte Callback — verbeuse dès qu'il s'agit d'exprimer une règle de trois lignes. Deux semaines de développement de juin à juillet 2026 apportent trois évolutions convergentes sur ce composant : une manière d'inliner une closure de validation sans construire la violation à la main, une façon de rendre Validation invocable pour l'utiliser dans un pipeline PHP 8.5, et une cascade plus fine du group sequence vers les objets référencés. Aucune de ces trois pull requests n'est encore mergée au moment d'écrire ces lignes — les trois ciblent Symfony 8.2 et sont encore en revue sur GitHub — mais le code proposé est stable, testé, et donne une idée fiable de l'API à venir. On les passe en revue avec le vrai diff de chaque PR, pas des suppositions.
Le problème que la contrainte Callback traîne depuis toujours
Pour valider une règle ad hoc — une date de livraison qui doit être dans le futur, un code postal cohérent avec un pays, une combinaison de deux champs — la voie balisée reste la méthode de callback, déclarée avec l'attribut #[Assert\Callback] et appelée avec le contexte d'exécution en paramètre. Le développeur doit alors construire lui-même la violation, avec buildViolation(), atPath() et addViolation(), pour une règle qui, dans l'absolu, ne fait qu'une comparaison booléenne.
<?php
// Style actuel — Symfony 8.1 et versions antérieures
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class Order
{
public \DateTimeImmutable $deliveryDate;
#[Assert\Callback]
public function validateDeliveryDate(ExecutionContextInterface $context): void
{
if ($this->deliveryDate <= new \DateTimeImmutable('today')) {
$context->buildViolation('La date de livraison doit être dans le futur.')
->atPath('deliveryDate')
->addViolation();
}
}
}Fonctionnel, mais lourd pour ce que ça exprime : une classe entière, une méthode dédiée, un contexte d'exécution injecté pour une seule ligne de logique métier. C'est exactement ce point de friction que vise la première des trois PR.
PR #64779 : une closure inline qui construit sa violation toute seule
La pull request #64779, intitulée « Allow to inline Callback constraints », n'introduit pas une nouvelle contrainte comme on aurait pu s'y attendre : elle enrichit la contrainte Callback existante avec une option message. Le principe est simple. Si message est renseigné et que la closure retourne false, le validateur construit automatiquement une violation avec ce message, sous un nouveau code d'erreur dédié Callback::NOT_SATISFIED_ERROR. Si message n'est pas renseigné, le comportement historique est conservé à l'identique — la valeur de retour de la closure est simplement ignorée, exactement comme aujourd'hui. C'est un ajout strictement rétrocompatible.
<?php
// Proposition PR #64779 — nécessite PHP 8.5 pour une closure statique dans un attribut
use Symfony\Component\Validator\Constraints as Assert;
class Order
{
#[Assert\Callback(
static function (\DateTimeImmutable $value): bool {
return $value > new \DateTimeImmutable('today');
},
message: 'La date de livraison doit être dans le futur.',
)]
public \DateTimeImmutable $deliveryDate;
}Le diff de la PR ajoute un test explicitement marqué #[RequiresPhp('>=8.5.0')], ce qui confirme que ce style d'écriture — une closure static directement passée en argument d'un attribut — s'appuie sur le support des expressions constantes plus permissives introduit par PHP 8.5, et pas sur une nouveauté du Validator lui-même. La closure garde la même signature que la méthode de callback classique : elle peut recevoir la valeur, le contexte d'exécution et le payload de la contrainte, tous optionnels. Cette bascule vers des attributs qui embarquent directement un comportement PHP prolonge une tendance qu'on documentait déjà avec #[AsMessage] et #[WhenNot] en Symfony 7.2 : de moins en moins de classes dédiées, de plus en plus de logique portée par l'attribut lui-même, au plus près du code qu'il annote.
PR #64844 : Validation devient invocable pour le pipe operator
PHP 8.5 introduit l'opérateur pipe |>, qui prend la valeur à sa gauche et l'injecte comme unique argument du callable à sa droite, de gauche à droite, sans variable intermédiaire ni parenthèses imbriquées — on avait détaillé sa mécanique complète, avec l'extension URI et #[NoDiscard], dans un article dédié. Le souci pour Symfony : jusqu'ici, la classe Validation ne pouvait servir que de factory statique. Sa méthode Validation::createCallable() retourne bien un callable, mais on ne pouvait pas écrire new Validation(...) directement dans un pipeline, puisque son constructeur était déclaré private — la classe était volontairement non instanciable.
<?php
// Avant — Symfony 8.1, style factory
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Validation;
$validate = Validation::createCallable(new NotBlank(), new Email());
$email = $validate($dto->getEmail());Besoin d'un expert Symfony ?
Réserver un appel →<?php
// Proposition PR #64844 — Validation instanciable et invocable
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Validation;
$email = $dto->getEmail() |> new Validation(new NotBlank(), new Email());Le diff est chirurgical : le constructeur privé disparaît, la classe reçoit une propriété $constraints et une méthode __invoke(mixed $value): mixed. Cette méthode valide $value contre les contraintes — ou le ValidatorInterface — passées au constructeur, puis retourne la valeur inchangée si elle est valide. Si elle ne l'est pas, elle lève une ValidationFailedException contenant la valeur et la liste des violations — pas de retour silencieux, ce qui est cohérent avec le pipe operator, incapable par nature de faire un branchement conditionnel. Autrement dit, dans une pipeline |>, la validation devient une étape qui laisse passer la valeur ou interrompt l'exécution par exception, exactement comme le ferait un middleware.
Un point à garder en tête avant de compter dessus : la discussion sur la PR n'est pas unanime. Un des mainteneurs historiques du composant objecte que ce changement serait « purement cosmétique », puisque Validation::createCallable() couvre déjà le même besoin, et qu'ouvrir le constructeur crée une ambiguïté avec les méthodes de fabrique statiques existantes de la classe. Une alternative — une classe dédiée plutôt qu'une modification de Validation — est évoquée sans consensus à ce stade. Concrètement : l'idée a de bonnes chances d'aboutir sous une forme ou une autre en 8.2, mais la signature exacte pourrait encore bouger avant le merge.
PR #64641 : la cascade du group sequence vers les objets référencés
Le troisième chantier est plus discret mais corrige un comportement qui piège régulièrement les validations imbriquées. Quand une classe déclare un group sequence — via l'attribut #[Assert\GroupSequence], ClassMetadata::setGroupSequence() ou GroupSequenceProviderInterface — les objets référencés marqués #[Assert\Valid] ne sont, par défaut, cascadés que dans le groupe Default. Résultat : des contraintes enregistrées sur un objet enfant pour l'un des groupes de la séquence ne sont jamais évaluées, silencieusement, même si le parent progresse correctement dans sa séquence.
<?php
// Proposition PR #64641 — opt-in explicite, aucun changement de comportement par défaut
use Symfony\Component\Validator\Constraints as Assert;
#[Assert\GroupSequence(groups: ['Group 1', 'Entity'], cascadeCurrentGroup: true)]
class Entity
{
#[Assert\Valid]
private Reference $reference;
}Le nouvel argument cascadeCurrentGroup, ajouté au constructeur de GroupSequence, indique au validateur de cascader vers l'objet référencé à la fois le groupe Default et le groupe de la séquence en cours d'évaluation, au lieu du seul Default. Il fonctionne aussi avec les séquences dynamiques renvoyées par GroupSequenceProviderInterface. Par défaut, ce drapeau vaut false : aucune application existante ne change de comportement tant qu'elle ne l'active pas explicitement, ce qui rend l'ajout rétrocompatible sans réserve.
Composer une validation ad hoc sans usine à gaz
Prises ensemble, ces trois propositions racontent la même histoire : écrire une règle de validation ponctuelle ne devrait pas obliger à créer une classe Constraint et son ConstraintValidator associé quand trois lignes de PHP suffisent, et la validation devrait pouvoir se composer dans un pipeline plutôt que d'être un aparté imperatif au milieu d'un service. La closure inline couvre le cas d'une règle unique posée directement sur une propriété. Validation invocable couvre le cas d'une validation insérée dans un flux de transformation — un DTO qui traverse plusieurs étapes avant de devenir une entité, par exemple. La cascade de group sequence couvre le cas, plus rare mais réel, d'une hiérarchie d'objets où chaque niveau doit respecter la même progression de groupes que son parent.
Ce que ces trois PR ne changent pas : une règle réutilisée à plusieurs endroits, ou qui mérite un test unitaire isolé de son contexte d'usage, reste mieux servie par une vraie classe Constraint. Les closures inline sont faites pour l'exception ponctuelle, pas pour devenir le nouveau standard de toute validation métier — le risque, sinon, est de disperser la logique de validation dans des attributs difficiles à retrouver et à tester isolément. Et puisque les trois PR sont encore ouvertes et en attente de revue au moment de la rédaction, la prudence reste de mise : il s'agit de suivre les « A Week of Symfony » à venir plutôt que de planifier une migration sur une API qui peut encore changer de forme avant le merge en 8.2.
Une contrainte de validation mal placée — ou pire, absente sur un endpoint qui accepte des entrées utilisateur — reste l'une des portes d'entrée les plus communes pour une injection ou une désérialisation malveillante, un point qu'on rappelait déjà dans notre checklist OWASP en 30 minutes pour Symfony. C'est précisément le genre de détail qu'on vérifie systématiquement lors d'un audit de code : où la validation métier est-elle réellement appliquée, où repose-t-elle sur une convention jamais testée, et où un attaquant pourrait-il glisser une valeur qu'aucune contrainte ne couvre vraiment. Si tu veux un diagnostic complet de ce que ton code Symfony laisse passer aujourd'hui, c'est le point de départ naturel avant d'ajouter la moindre nouvelle dépendance à ton Validator.
Besoin d'un expert Symfony ?
20 ans d'expérience sur l'écosystème PHP/Symfony.