/validation-constraints — Contraintes Symfony sur entité/DTO/classe
Tu poses ou révises les contraintes de validation d'une classe PHP (entité Doctrine, DTO API, value object). Tu utilises les attributs PHP 8 #[Assert\*], tu choisis la bonne cible (propriété, getter, classe), et tu ne duplique pas ce que Doctrine déduit déjà du mapping.
Détection préalable (obligatoire)
- Lire
composer.json. - Vérifier
symfony/validator.- Présent → OK.
- Absent →
composer require symfony/validatoravant de continuer.
- Si
symfony/formprésent → rappeler que les forms valident automatiquement ladata_classaprèshandleRequest. Pas de call manuel au validator dans le contrôleur (cf./symfony:form-handle). - Si
sylius/syliusprésent → les contraintes custom doivent déclarergroups: ['Default', 'sylius']pour être prises par les forms Sylius (cf./symfony:validation-groups).
Règles fondamentales
- Attributs PHP, pas YAML/XML : la codebase est en attributs. YAML/XML n'est justifié que si l'entité vient d'un bundle tiers non-modifiable (auquel cas →
ExtendsValidationFor, cf./symfony:validation-groups). - Validation auto-mappée Doctrine déjà en place :
nullable: false→NotNull,unique: true→UniqueEntity(à confirmer manuellement, cf. plus bas),length: N→Length(max: N). Ne pas redéclarer ces contraintes sauf surcharge métier (message custom,groups). - Une contrainte = une intention métier.
#[Assert\NotBlank]sur un champ déjànullable: falsen'apporte rien à la persistance, mais clarifie le contrat form/API (le validator accepte''pourNotNull, refuse pourNotBlank). - Messages ICU :
message: 'product.name.blank'et traduction danstranslations/validators.<locale>.yaml. Pas de messages en clair dans le code, sauf prototype jetable. - Ne jamais mettre la logique métier dans un
Callbacksi un service peut s'en charger (viaExpression+ExpressionLanguage, ou une contrainte custom — cf./symfony:validation-groups). - Cibles par défaut : la propriété. Getter uniquement si la valeur est calculée (
isPasswordSafe()). Classe uniquement si la contrainte dépend de plusieurs propriétés.
Catalogue des contraintes (les plus utilisées)
| Famille | Contraintes | Note |
|---|---|---|
| Base | NotBlank, NotNull, IsNull, Blank, IsTrue, IsFalse, Type |
NotBlank refuse '' et null ; NotNull accepte '' |
| Chaîne | Length, Regex, Email, Url, Uuid, Ulid, Ip, Hostname, Json |
Email(mode: 'strict') recommandé |
| Comparaison | EqualTo, NotEqualTo, IdenticalTo, GreaterThan, LessThan, Range |
propertyPath: 'startDate' pour comparer à un autre champ |
| Nombre | Positive, PositiveOrZero, Negative, NegativeOrZero, DivisibleBy |
— |
| Date | Date, DateTime, Time, Timezone |
Sur string ; sur \DateTimeInterface ces contraintes sont inutiles |
| Choix | Choice, Country, Language, Locale, Currency |
Choice(callback: 'getStatuses') pour liste dynamique |
| Fichier | File, Image |
maxSize: '2M', mimeTypes: ['application/pdf'] |
| Finance | Iban, Bic, Isbn, Issn, CardScheme, Luhn |
— |
| Sécurité | PasswordStrength, NotCompromisedPassword, UserPassword |
NotCompromisedPassword appelle l'API HIBP (réseau requis) |
| Collection | Count, Unique, All, Collection |
All(new Assert\Email()) valide chaque élément |
| Objet | Valid, Cascade, Traverse |
Valid pour un sous-objet, Cascade pour tous les sous-objets |
| Doctrine | UniqueEntity |
Attribut de classe, pas de propriété |
| Custom | Callback, Expression, When, Compound, AtLeastOneOf, Sequentially |
Détaillés dans /symfony:validation-groups |
Cibles
Propriété (cas par défaut)
use Symfony\Component\Validator\Constraints as Assert;
class Author
{
#[Assert\NotBlank(message: 'author.name.blank')]
#[Assert\Length(min: 2, max: 100)]
private string $name;
#[Assert\NotBlank]
#[Assert\Email(mode: 'strict')]
private string $email;
#[Assert\Range(min: 0, max: 120)]
private int $age;
#[Assert\Choice(choices: ['draft', 'published', 'archived'])]
private string $status = 'draft';
}
Attention propriété typée non initialisée :
private string $name;sans valeur par défaut lance unErrorà la lecture avant affectation. Initialiser (= '') ou rendre nullable si le validator doit gérer le « vide ».
Getter (valeur calculée)
Nom de méthode commençant par get, is, has. La contrainte porte sur la valeur retournée.
class Author
{
private string $firstName;
private string $password;
#[Assert\IsTrue(message: 'password.not.safe')]
public function isPasswordSafe(): bool
{
return $this->firstName !== $this->password;
}
}
Classe (invariant multi-propriétés)
UniqueEntity (Doctrine)
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[ORM\Entity]
#[UniqueEntity(fields: ['email'], message: 'user.email.already_used')]
#[UniqueEntity(fields: ['tenant', 'slug'], errorPath: 'slug')] // unicité composite
class User
{
#[ORM\Column]
private string $email;
}
Requiert symfony/doctrine-bridge. errorPath dirige l'erreur sur un champ form précis.
Callback de classe
use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[Assert\Callback('validate')]
class Reservation
{
private \DateTimeImmutable $start;
private \DateTimeImmutable $end;
public function validate(ExecutionContextInterface $context): void
{
if ($this->end <= $this->start) {
$context->buildViolation('reservation.end_before_start')
->atPath('end')
->addViolation();
}
}
}
Préférer Expression pour les règles courtes :
#[Assert\Expression(
expression: 'this.end > this.start',
message: 'reservation.end_before_start',
)]
class Reservation { /* ... */ }
Objets imbriqués
Valid et Cascade ne font pas la même chose :
#[Assert\Valid]sur une propriété → valide ce sous-objet précis (recommandé, explicite).#[Assert\Cascade]sur la classe → valide récursivement tous les sous-objets typés. Pratique pour un DTO à plusieurs niveaux mais opaque, à utiliser sciemment.
class Order
{
#[Assert\Valid]
private Address $billingAddress;
#[Assert\Valid]
#[Assert\Count(min: 1, message: 'order.no_lines')]
private Collection $lines; // chaque OrderLine est validée
}
Sans Valid, les sous-objets ne sont pas parcourus — défaut silencieux fréquent.
Héritage
- Les contraintes d'une classe mère s'appliquent sur les enfants (fusion, non surcharge).
- Pour surcharger la règle d'un champ parent : retirer la contrainte parent ou la placer dans un groupe (
groups: ['parent']) et utiliser un groupe différent côté enfant. Détails :/symfony:validation-groups. - Ne pas redéclarer la propriété dans l'enfant juste pour remettre des attributs — PHP le refuse si la propriété mère est
private, et casse le contrat de substitution siprotected.
Validation auto-mappée (Doctrine)
Activée par défaut via framework.validation.auto_mapping. Règles déduites :
| Mapping Doctrine | Contrainte inférée |
|---|---|
nullable: false |
NotNull |
length: N |
Length(max: N) |
unique: true (sur colonne simple) |
UniqueEntity |
type: 'integer' / 'float' |
Type |
Pour désactiver l'auto-mapping sur une classe ou une propriété :
#[Assert\DisableAutoMapping]
class Legacy { /* ... */ }
Ou configurer des exclusions dans config/packages/validator.yaml :
framework:
validation:
auto_mapping:
'App\Entity': []
Déroulement
1 — Cadrer
- Classe cible : entité, DTO, value object.
- Règles métier à exprimer.
- Règles déjà couvertes par le mapping Doctrine (ne pas doublonner).
- Groupes ? → si oui, cf.
/symfony:validation-groups.
2 — Poser les contraintes
Ordre typique sur une propriété : présence (NotBlank/NotNull) → format (Length/Regex/Email) → valeur (Range/Choice). Les contraintes sont évaluées dans l'ordre de déclaration, toutes exécutées par défaut. Pour court-circuiter au premier échec : #[Assert\Sequentially([...])].
3 — Vérifier le mapping
symfony console debug:validator 'App\Entity\Author'
# ou pour un dossier entier
symfony console debug:validator src/Entity
Affiche la liste des contraintes appliquées, leurs groupes, leurs options. À utiliser systématiquement après un changement non trivial.
4 — Tester
Les forms appellent le validator automatiquement, les tests functionals sur /form-handle couvrent. Pour un test unitaire ciblé :
use Symfony\Component\Validator\Validation;
public function testNameTooShort(): void
{
$validator = Validation::createValidatorBuilder()
->enableAttributeMapping()
->getValidator();
$author = new Author();
$author->setName('a');
$violations = $validator->validate($author);
self::assertCount(1, $violations);
self::assertSame('name', $violations[0]->getPropertyPath());
}
Pour tester une contrainte seule (hors classe) → /symfony:validation-use.
5 — Clôture
Afficher :
- Classe(s) modifiée(s).
- Contraintes ajoutées / retirées.
- Ce qui reste : traductions dans
translations/validators.*.yaml, tests, groupes (→/symfony:validation-groups), usage hors form (→/symfony:validation-use).
Delta Sylius
- Contraintes custom sur une resource Sylius : ajouter
groups: ['Default', 'sylius'], sinon les forms Sylius (qui valident sur['Default', 'sylius']) les ignorent. - Les validations Sylius vendor sont fournies en XML (
Resources/config/validation.xmldu bundle). Pour les surcharger,ExtendsValidationForou override de la resource — cf./symfony:validation-groups.
Argument optionnel
/symfony:validation-constraints Product — ajoute/révise les contraintes sur src/Entity/Product.php.
/symfony:validation-constraints src/Dto/CreateOrderRequest.php — audit d'un DTO (présence, format, auto-mapping désactivé si DTO pur).
/symfony:validation-constraints sans argument — demande la classe et les règles à poser.