ValidatorInterface et violations
Le Validator Symfony transforme des règles déclaratives en ConstraintViolationList — vide = valide.
Le composant Validator de Symfony repose sur une idée simple : tu déclares des règles
(les contraintes) sur tes objets ou valeurs, et le Validator les évalue. Le résultat est
toujours une ConstraintViolationList — si elle est vide, tout est valide.
ValidatorInterface — les quatre méthodes
interface ValidatorInterface
{
// Valide une valeur ou un objet complet
public function validate(
mixed $value,
Constraint|array|null $constraints = null,
string|GroupSequence|array|null $groups = null
): ConstraintViolationListInterface;
// Valide une seule propriété d'un objet existant
public function validateProperty(
object $object,
string $propertyName,
string|GroupSequence|array|null $groups = null
): ConstraintViolationListInterface;
// Valide une valeur comme si elle était dans une propriété donnée
public function validatePropertyValue(
object|string $objectOrClass,
string $propertyName,
mixed $value,
string|GroupSequence|array|null $groups = null
): ConstraintViolationListInterface;
// Démarre une validation contextuelle enchaînée
public function startContext(): ContextualValidatorInterface;
}
$violations = $validator->validate($object);if (count($violations) > 0) { throw new ValidationException($violations); }Si
count($violations) === 0, l'objet est valide. Ne jamais faire confiance à un objet
non validé.
ConstraintViolation — anatomie d'une violation
Chaque violation dans la liste est un objet ConstraintViolation avec ces propriétés :
| Getter | Type | Description |
|---|---|---|
getMessage() | string | Message traduit, prêt à afficher |
getMessageTemplate() | string | Message brut avec placeholders ({{ value }}) |
getParameters() | array | Valeurs des placeholders |
getPropertyPath() | string | Chemin vers la propriété violée (address.city) |
getInvalidValue() | mixed | La valeur qui a échoué |
getCode() | string|null | Code d'erreur UUID (ex. NotBlank::IS_BLANK_ERROR) |
getRoot() | mixed | L'objet racine validé |
getConstraint() | Constraint|null | La contrainte qui a généré la violation |
getCause() | mixed | Cause externe (ex. exception sous-jacente) |
ConstraintViolationList — la liste de résultats
ConstraintViolationList implémente Traversable + Countable + ArrayAccess.
Tu peux l'itérer, la compter, et accéder à ses éléments par index :
foreach ($violations as $violation) {
echo $violation->getPropertyPath() . ' : ' . $violation->getMessage() . PHP_EOL;
}
// Filtrer par code d'erreur
$blankErrors = $violations->findByCodes(NotBlank::IS_BLANK_ERROR);
$validator->startContext()->validate($a)->validate($b)->getViolations().
C'est plus efficace que de créer plusieurs listes et de les fusionner avec addAll().
Essaie le validateur ci-dessous avec différentes valeurs :
Remplis les champs et clique sur Valider pour voir les violations.
Contraintes built-in — Basic et String
Symfony livre une cinquantaine de contraintes prêtes à l'emploi — connaître les options avancées fait la différence.
Symfony fournit une cinquantaine de contraintes dans le namespace Symfony\Component\Validator\Constraints.
Connaître leurs options avancées évite d'écrire des contraintes custom inutiles.
NotBlank — normaliser avant de valider
#[Assert\NotBlank(
allowNull: false, // null est-il autorisé ? (défaut false)
normalizer: 'trim', // callable appliqué avant validation
message: 'Le champ ne peut être vide.'
)]
public string $name;
Tout callable PHP :
'trim', 'strtolower', ou une closure.
Appliqué à la valeur locale avant la contrainte — ne modifie pas l'objet.
Utile pour éviter les espaces parasites sans transformer la donnée.
Length — compter les bons caractères
#[Assert\Length(
min: 2,
max: 255,
exactly: null, // longueur exacte (remplace min+max)
countUnit: 'graphemes', // 'bytes' | 'codepoints' | 'graphemes'
)]
public string $username;
La différence entre les trois modes de comptage est visible sur les caractères accentués et les emojis :
| Valeur | bytes | codepoints | graphemes |
|---|---|---|---|
"café" | 5 | 4 | 4 |
"👨👩👧" | 25 | 5 | 1 |
"résumé" | 8 | 6 | 6 |
graphemes compte ce que l'utilisateur perçoit comme un
caractère. C'est le bon choix pour les champs texte visibles. bytes est utile pour
les contraintes de stockage (taille d'un champ VARCHAR(255) en UTF-8).
Email — choisir le bon mode
#[Assert\Email(
mode: 'html5', // 'html5' | 'strict' | 'html5-allow-no-tld'
message: '{{ value }} n\'est pas un email valide.'
)]
public string $email;
| Mode | Accepte | Usage |
|---|---|---|
html5 | Regex HTML5 (permissive) | Formulaires web standard (recommandé) |
strict | RFC 5321/5322 complet | Vérification stricte — peut rejeter des emails valides en pratique |
html5-allow-no-tld | html5 sans TLD obligatoire | Intranet, emails locaux (user@localhost) |
Regex — exclure un pattern
// Valide seulement si le pattern NE correspond PAS (match: false)
#[Assert\Regex(
pattern: '/[<>\'"]/',
match: false,
message: 'Les caractères spéciaux HTML ne sont pas autorisés.'
)]
public string $bio;
Par défaut (
match: true), la valeur est valide si elle correspond au pattern.
Avec match: false, la valeur est valide si elle ne correspond pas — c'est
une liste noire. Utile pour exclure des caractères dangereux.
Uuid — filtrer par version
#[Assert\Uuid(
versions: [4, 7], // n'accepte que les UUID v4 et v7
strict: true, // format strict avec tirets (défaut true)
)]
public string $id;
Ip — filtrer les plages d'adresses
#[Assert\Ip(
version: '4', // '4' | '6' | 'all' (défaut '4')
groups: 'no_priv', // exclure les IP privées (10.x, 192.168.x…)
)]
public string $clientIp;
no_priv (exclut les plages privées RFC 1918),
no_public (n'accepte que les IP privées/locales), no_res (exclut les
plages réservées). Ces options s'ajoutent à la version : '4_no_priv',
'6_no_priv', 'all_no_priv', etc.
Contraintes de comparaison et de collection
Range, Choice, Collection — les contraintes qui s'appuient sur des valeurs externes ou d'autres contraintes.
Ces contraintes comparent la valeur à des valeurs externes — une liste de choix autorisés, des bornes min/max, ou d'autres propriétés du même objet — ou appliquent des contraintes récursivement sur chaque élément d'une collection.
Range — bornes dynamiques avec PropertyPath
#[Assert\Range(
min: 0,
max: 100,
minMessage: 'Doit être au minimum {{ limit }}.',
maxMessage: 'Doit être au maximum {{ limit }}.',
)]
public int $score;
Les options minPropertyPath et maxPropertyPath permettent des bornes
dynamiques tirées d'une autre propriété de l'objet — utile pour les plages de dates :
// La date de fin doit être entre startDate et endDate de l'objet parent
#[Assert\Range(
minPropertyPath: 'startDate',
maxPropertyPath: 'endDate',
)]
public \DateTimeImmutable $checkInDate;
Choice — liste statique ou dynamique
// Liste statique
#[Assert\Choice(
choices: ['red', 'green', 'blue'],
multiple: false, // true = tableau de valeurs accepté
strict: true, // comparaison stricte (===)
)]
public string $color;
// Liste dynamique via callback
#[Assert\Choice(
callback: [ColorRepository::class, 'getValidColors'],
multiple: true,
min: 1, // au moins 1 choix sélectionné
max: 3, // au plus 3 choix
)]
public array $tags;
Un callable statique (tableau
[Classe::class, 'méthode'] ou chaîne de fonction).
Appelé sans argument, doit retourner le tableau des choix valides. Utile quand les choix
viennent d'une source dynamique (base de données, configuration).
Count — valider la taille d'une collection
#[Assert\Count(
min: 1,
max: 10,
divisibleBy: 2, // le nombre d'éléments doit être pair
)]
public array $items;
Collection — valider un tableau associatif
Collection est différente de Count — elle valide chaque clé
du tableau avec ses propres contraintes :
#[Assert\Collection(
fields: [
'name' => [new Assert\NotBlank(), new Assert\Length(max: 100)],
'email' => new Assert\Email(),
'age' => new Assert\Optional([new Assert\Range(min: 18)]),
],
allowExtraFields: false, // rejeter les clés non déclarées
allowMissingFields: false, // rejeter les clés manquantes
)]
public array $data;
Collection, toutes les clés sont
requises par défaut. Enveloppe une contrainte dans Assert\Optional([...])
pour la rendre optionnelle, ou dans Assert\Required([...]) pour être explicite.
All — appliquer des contraintes sur chaque élément
// Valider chaque email dans un tableau
#[Assert\All([
new Assert\NotBlank(),
new Assert\Email(),
])]
public array $recipients;
Les violations générées par All ont un propertyPath de la forme
recipients[0], recipients[2], etc. — l'index de l'élément invalide
est toujours visible.
All pour des tableaux de valeurs scalaires
ou des tableaux d'objets quand tu veux appliquer des contraintes directement sur les éléments.
Utilise #[Valid] pour cascader la validation vers des objets imbriqués (section suivante).
Groupes de validation
Les groupes permettent de déclencher des sous-ensembles de contraintes selon le contexte — inscription vs mise à jour de profil.
Par défaut, toutes les contraintes d'un objet sont évaluées en même temps. Les groupes permettent de séparer les contraintes en sous-ensembles activables selon le contexte — formulaire d'inscription, mise à jour du profil, validation stricte pour l'API.
Déclarer un groupe sur une contrainte
use Symfony\Component\Validator\Constraints as Assert;
final class User
{
#[Assert\NotBlank] // groupe Default
public string $name;
#[Assert\Email(groups: ['registration'])] // groupe registration seulement
public string $email;
#[Assert\Length(min: 12, groups: ['strict'])] // groupe strict seulement
public string $password;
}
Pour déclencher un groupe spécifique :
// Valide uniquement les contraintes du groupe 'registration'
$violations = $validator->validate($user, null, 'registration');
// Valide plusieurs groupes à la fois
$violations = $validator->validate($user, null, ['Default', 'registration']);
Toute contrainte sans
groups appartient au groupe 'Default'
(Constraint::DEFAULT_GROUP). Ce groupe appartient lui-même implicitement au groupe
portant le nom de la classe ('User' pour la classe User).
Valider avec le groupe 'User' inclut donc les contraintes 'Default'.
GroupSequence — validation séquentielle
Avec GroupSequence, les groupes sont évalués dans l'ordre.
Si une violation est trouvée dans le premier groupe, les groupes suivants ne sont pas évalués :
use Symfony\Component\Validator\Constraints\GroupSequence;
// Passer en troisième argument de validate()
$sequence = new GroupSequence(['Default', 'registration', 'strict']);
$violations = $validator->validate($user, null, $sequence);
On peut aussi placer la séquence directement sur la classe avec
#[GroupSequence] — elle s'applique alors à chaque appel validate($user)
sans avoir à passer la séquence explicitement :
#[Assert\GroupSequence(['Default', 'strict'])]
final class User { ... }
#[GroupSequence] sur une classe, la classe
n'appartient plus au groupe 'Default' — elle appartient au groupe portant son nom
(ex. 'User'). Pour référencer ses contraintes par défaut dans la séquence, on utilise
le nom de la classe, pas 'Default'.
Coche les groupes ci-dessous et observe quelles contraintes s'activent :
Active des groupes pour voir quelles contraintes s'exécutent.
Cascading et #[Valid]
#[Assert\Valid] propage la validation vers les objets imbriqués — sans lui, les sous-objets ne sont jamais validés.
Quand un objet contient des sous-objets, le Validator ne les explore pas
automatiquement. Sans #[Assert\Valid], les contraintes sur l'adresse d'un utilisateur
sont silencieusement ignorées — l'objet passe comme valide.
#[Assert\Valid] — déclencher le cascading
final class User
{
#[Assert\NotBlank]
public string $name;
#[Assert\Valid] // cascade la validation vers Address
public Address $address;
}
final class Address
{
#[Assert\NotBlank]
public string $city;
#[Assert\Length(min: 5, max: 5)]
public string $zipCode;
}
Sans #[Assert\Valid], valider $user ne vérifie jamais
$user->address->city. Avec, les violations de Address apparaissent dans
la liste avec un propertyPath de la forme address.city.
Cascading et collections avec traverse
final class Order
{
#[Assert\Valid] // traverse automatiquement les tableaux
public array $items; // chaque OrderItem est validé
#[Assert\Valid(traverse: false)] // ne pas traverser
public \ArrayObject $history;
}
Quand
Valid cible un tableau ou un Traversable, il itère sur chaque
élément et cascades la validation. Le propertyPath prend la forme
items[0].price — l'index est inclus.
Cascading et groupes
La contrainte Valid hérite des groupes de l'appel de validation parent.
Valider $user avec le groupe 'strict' cascade 'strict' vers
$user->address :
// Address::$zipCode est dans le groupe 'strict'
final class Address
{
#[Assert\Length(min: 5, max: 5, groups: ['strict'])]
public string $zipCode;
}
// Cette contrainte n'est évaluée QUE si on valide avec le groupe 'strict'
$violations = $validator->validate($user, null, 'strict');
// → inclut les violations de address.zipCode
propertyPath est le chemin complet depuis la racine :
address.city, items[2].price, shipping.address.zipCode.
Cela permet d'associer chaque violation au bon champ dans un formulaire imbriqué.
#[Assert\Valid] et se demander pourquoi
l'objet imbriqué n'est jamais validé. Le Validator ne signale aucune erreur dans ce cas —
il ignore simplement les sous-objets sans Valid.
Contraintes composites et conditionnelles
Quatre contraintes changent la logique de combinaison : Sequentially, AtLeastOneOf, Compound, et When.
Ces quatre contraintes modifient la logique de combinaison plutôt que de valider une valeur directement. Elles s'imbriquent dans d'autres contraintes ou changent comment les violations sont collectées.
Sequentially — fail-fast dans une liste
Exécute une liste de contraintes dans l'ordre. Dès qu'une violation est trouvée, les contraintes suivantes ne sont pas évaluées. Évite les messages redondants :
#[Assert\Sequentially([
new Assert\NotBlank(), // 1. vérifier que ce n'est pas vide
new Assert\Length(min: 3), // 2. vérifier la longueur (seulement si non vide)
new Assert\Regex('/^[a-z]+$/'), // 3. vérifier le format (seulement si assez long)
])]
public string $username;
$username est vide, on obtient 3 violations en
même temps (NotBlank + Length + Regex). Avec Sequentially, on n'obtient que la
première — l'utilisateur corrige une étape à la fois.
AtLeastOneOf — au moins une vraie
Valide si au moins une des contraintes de la liste est satisfaite. Utile pour les champs qui acceptent plusieurs formats :
#[Assert\AtLeastOneOf([
new Assert\Email(),
new Assert\Regex('/^\+\d{10,}$/'), // ou un numéro de téléphone
], message: 'Doit être un email ou un téléphone valide.',
includeInternalMessages: false)]
public string $contact;
Par défaut (
true), le message d'erreur liste les raisons d'échec de chaque
contrainte interne. Mettre à false pour n'afficher que le message de
AtLeastOneOf lui-même — plus lisible pour l'utilisateur final.
Compound — contrainte réutilisable composite
Crée une contrainte nommée qui regroupe plusieurs contraintes. Réutilisable partout comme n'importe quelle contrainte built-in :
use Symfony\Component\Validator\Constraints\Compound;
final class StrongPassword extends Compound
{
protected function getConstraints(array $options): array
{
return [
new Assert\NotBlank(),
new Assert\Length(min: 12),
new Assert\Regex('/[A-Z]/'),
new Assert\Regex('/[0-9]/'),
];
}
}
// Utilisation identique à n'importe quelle contrainte
#[StrongPassword]
public string $password;
When — validation conditionnelle
Applique des contraintes seulement si une expression est vraie.
L'option otherwise permet d'appliquer d'autres contraintes dans le cas contraire :
#[Assert\When(
expression: 'this.isCompany',
constraints: [new Assert\NotBlank()],
otherwise: [new Assert\IsNull()],
)]
public ?string $vatNumber;
Variables disponibles dans expression : this (l'objet), value
(la valeur du champ), context (ExecutionContext). Accepte aussi une \Closure
qui reçoit les mêmes arguments.
| Contrainte | Résultat si toutes échouent | Usage typique |
|---|---|---|
Sequentially |
Première violation seulement | Enchaîner des règles progressives |
AtLeastOneOf |
Une violation si aucune ne passe | Champ acceptant plusieurs formats |
Compound |
Toutes les violations | Règle métier réutilisable nommée |
When |
Aucune violation si expression fausse | Champ conditionnel selon l'état de l'objet |
Callback et Expression
Pour la logique métier non couverte par les contraintes standard, Callback et Expression prennent le relais.
Quand aucune contrainte built-in ne couvre ton cas, deux contraintes te donnent accès à
du code PHP arbitraire : Callback pour la logique en méthode de classe,
et Expression pour les règles inline via l'ExpressionLanguage de Symfony.
#[Assert\Callback] — méthode statique sur la classe
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final class Reservation
{
public \DateTimeImmutable $startDate;
public \DateTimeImmutable $endDate;
#[Assert\Callback]
public static function validate(
mixed $object, // l'instance de Reservation
ExecutionContextInterface $context,
mixed $payload
): void {
if ($object->endDate <= $object->startDate) {
$context
->buildViolation('La date de fin doit être après la date de début.')
->atPath('endDate')
->addViolation();
}
}
}
#[Assert\Callback] — méthode d'instance
La méthode peut aussi être d'instance (non statique). Dans ce cas, l'objet est $this
et la signature change :
#[Assert\Callback]
public function validateDates(
ExecutionContextInterface $context,
mixed $payload
): void {
if ($this->endDate <= $this->startDate) {
$context->buildViolation('Date de fin invalide.')->atPath('endDate')->addViolation();
}
}
buildViolation() — construire une violation
$context
->buildViolation('La valeur {{ val }} est interdite.')
->setParameter('{{ val }}', $value) // injecter des variables dans le message
->atPath('fieldName') // chemin relatif à l'objet courant
->setCode(MyConstraint::MY_ERROR) // code UUID pour filtrage programmatique
->setInvalidValue($value) // valeur invalide (pour getInvalidValue())
->addViolation(); // finaliser et ajouter à la liste
#[Assert\Expression] — règle inline
// Valide si l'expression retourne true
#[Assert\Expression(
expression: 'this.endDate > this.startDate',
message: 'La date de fin doit être après la date de début.',
)]
class Reservation { ... }
// negate: true — violé si l'expression est vraie (liste noire)
#[Assert\Expression(
expression: 'value in bannedWords',
negate: true,
values: ['bannedWords' => ['spam', 'admin']],
message: 'Ce mot est interdit.',
)]
public string $username;
- Règle simple en une ligne
- Inline dans l'attribut
- Pas d'accès aux services
- Syntaxe ExpressionLanguage
- Logique complexe multi-lignes
- Méthode dédiée, testable unitairement
- Accès aux services via le contexte
- PHP pur, pas de syntaxe spéciale
Créer une contrainte personnalisée
Quand aucune contrainte built-in ne convient, on en crée une — c'est plus simple qu'il n'y paraît.
Créer une contrainte custom revient à écrire deux classes : la contrainte (les métadonnées) et le validateur (la logique). Symfony se charge du câblage — pas de configuration YAML nécessaire.
Structure — la contrainte
use Symfony\Component\Validator\Constraint;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class FrenchPhoneNumber extends Constraint
{
// Code d'erreur : UUID v4 aléatoire, unique
public const string INVALID_FORMAT_ERROR = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
// Déclaration obligatoire pour que getErrorName() fonctionne
protected const array ERROR_NAMES = [
self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR',
];
public function __construct(
public string $message = 'Le numéro "{{ value }}" n\'est pas un numéro français valide.',
?array $groups = null,
mixed $payload = null,
) {
parent::__construct([], $groups, $payload);
}
}
Structure — le validateur
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class FrenchPhoneNumberValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
// 1. Vérifier que la contrainte est du bon type
if (!$constraint instanceof FrenchPhoneNumber) {
throw new UnexpectedTypeException($constraint, FrenchPhoneNumber::class);
}
// 2. Null et vide = valides (NotBlank s'en charge)
if (null === $value || '' === $value) {
return;
}
// 3. Valider le format
if (!preg_match('/^(\+33|0)[1-9](\d{8})$/', $value)) {
$this->context
->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode(FrenchPhoneNumber::INVALID_FORMAT_ERROR)
->addViolation();
}
}
}
Méthode héritée de
ConstraintValidator. Elle formate la valeur pour l'affichage
dans le message d'erreur : les chaînes sont entourées de guillemets, les tableaux tronqués,
les objets représentés par leur classe. Toujours utiliser formatValue() plutôt que
de caster directement en string.
Contrainte ciblant la classe entière
Pour valider plusieurs propriétés ensemble (ex. startDate < endDate), la contrainte cible
CLASS_CONSTRAINT — le validateur reçoit l'objet complet :
#[\Attribute(\Attribute::TARGET_CLASS)]
final class ValidDateRange extends Constraint
{
public function getTargets(): string
{
return self::CLASS_CONSTRAINT; // reçoit l'objet dans validate()
}
}
FrenchPhoneNumberValidator
car son nom est {ConstraintName}Validator. Aucune configuration ni tag nécessaire
si l'autoconfiguration est activée (c'est le cas par défaut). La classe est taguée
validator.constraint_validator automatiquement.
\Attribute::IS_REPEATABLE dans le
#[\Attribute(...)] de la contrainte et déclare IS_REPEATABLE = true
dans la classe pour que Symfony autorise plusieurs instances de la même contrainte sur
le même élément.
UniqueEntity et validation Doctrine
UniqueEntity délègue la validation à Doctrine — c'est une contrainte au niveau de la base de données.
UniqueEntity est une contrainte de niveau CLASS_CONSTRAINT qui
interroge Doctrine pour vérifier qu'aucune autre entité ne possède déjà la valeur donnée.
Elle vit dans symfony/doctrine-bridge, pas dans symfony/validator.
Configuration de base
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[UniqueEntity(
fields: 'email',
message: 'Cet email est déjà utilisé.',
)]
class User { ... }
Contrainte composite sur plusieurs champs
#[UniqueEntity(
fields: ['username', 'tenant'], // unique ensemble (paire)
message: 'Ce nom est déjà pris dans ce tenant.',
errorPath: 'username', // violation rattachée à username
)]
class User { ... }
Par défaut, la violation est rattachée au premier champ de
fields.
errorPath permet de la rediriger vers un autre champ — utile quand la violation
doit apparaître sur un champ précis dans un formulaire Symfony.
identifierFieldNames — gérer les updates
Lors d'une mise à jour, l'entité garde sa propre valeur. Sans
identifierFieldNames, UniqueEntity trouverait la même entité en base
et lancerait une violation à tort :
#[UniqueEntity(
fields: 'email',
identifierFieldNames: ['id'], // ignorer l'entité avec cet id
)]
class User
{
#[ORM\Id]
private string $id;
private string $email;
}
Symfony appelle
repository->findBy(['email' => $value]) (ou la méthode définie
dans repositoryMethod). Si un résultat est trouvé ET qu'il n'a pas le même
identifiant que l'entité en cours, une violation est créée.
Options avancées
| Option | Défaut | Description |
|---|---|---|
em |
null |
Nom de l'EntityManager (multi-em). null = EntityManager par défaut. |
entityClass |
null |
Classe Doctrine cible si différente de la classe portant la contrainte (ex. DTO). |
repositoryMethod |
'findBy' |
Méthode custom du repository — utile pour les requêtes filtrées (tenant, soft-delete). |
ignoreNull |
true |
Ignorer la contrainte si la valeur est null. |
['field' => $value]
en argument et doit retourner un tableau ou une collection d'entités (comme findBy).
Utile pour un repository qui applique un filtre global (ex. tenant courant, entités non supprimées).
UniqueEntity ne
remplace pas l'index UNIQUE en base — elle le complète au niveau
applicatif pour afficher un message d'erreur lisible. Toujours garder les deux.
Synthèse — le guide de choix
Le guide de choix des contraintes et les pièges du Validator.
Le Validator Symfony est riche — voici le guide de choix et les pièges à éviter pour l'utiliser efficacement.
Guide de choix
| Besoin | Outil |
|---|---|
| Valider un champ texte simple (vide, longueur) | NotBlank + Length |
| Valider un format complexe (regex) | Regex |
| Valider un email ou URL | Email / Url |
| Valider par rapport à d'autres valeurs de l'objet | Expression ou Callback |
| Valider un sous-objet imbriqué | #[Valid] |
| Valider selon le contexte (inscription vs update) | Groupes de validation |
| Plusieurs contraintes en séquence (fail-fast) | Sequentially |
| Validation conditionnelle | When |
| Champ acceptant plusieurs formats | AtLeastOneOf |
| Contrainte réutilisable composée | Compound |
| Logique métier non couverte | Contrainte custom ou Callback |
| Unicité en base | UniqueEntity |
Anti-patterns à éviter
$validator->validate($obj) ne lance aucune exception. Si tu ne vérifies pas
count($violations) > 0, les violations sont silencieusement ignorées et des données
invalides entrent dans ton système.
Sans
#[Valid], les contraintes sur un objet imbriqué (Address,
OrderItem) ne sont jamais évaluées — aucune erreur, aucun avertissement.
L'objet passe comme valide même si ses sous-objets sont invalides.
Expression couvre la plupart des règles cross-field en une ligne
('this.endDate > this.startDate'). Réserve Callback à la logique
complexe qui nécessite du PHP pur ou l'accès à des services.
Sans vérifier le type de
$constraint dans validate(), ton validateur
peut silencieusement s'exécuter avec la mauvaise contrainte en cas de mauvais câblage.
UnexpectedTypeException fait échouer proprement à la compilation.
Récapitulatif des contraintes avancées
| Contrainte | Options clés | À noter |
|---|---|---|
NotBlank |
allowNull, normalizer |
normalizer appliqué avant validation |
Length |
exactly, countUnit |
graphemes pour les emojis |
Email |
mode: html5|strict |
html5 recommandé pour les formulaires |
Regex |
match: false |
Inverser le test pour une liste noire |
Range |
minPropertyPath, maxPropertyPath |
Bornes dynamiques depuis l'objet |
Choice |
callback, multiple, min/max |
callback pour liste dynamique |
Collection |
fields, allowExtraFields |
Valider un tableau associatif complet |
Sequentially |
liste de contraintes | Stoppe à la première violation |
AtLeastOneOf |
includeInternalMessages |
Au moins une doit être vraie |
When |
expression, otherwise |
Variables : this, value, context |
UniqueEntity |
identifierFieldNames, repositoryMethod |
Nécessaire pour les updates |