Symfony 8.2 ajoute deux contraintes qui remplacent du code de validation maison : EntityExists vérifie qu'un identifiant correspond à une entité Doctrine, et Audio contrôle les caractéristiques réelles d'un fichier audio. Présentées le 8 octobre 2026 sur le blog officiel (PR #63483 d'Evgen Yurchenko, #66143 de Valmont Pehaut-Pietri et Nicolas Grekas), elles arrivent avec Symfony 8.2, attendu en novembre 2026. Le billet présente aussi une contrainte Cron, déjà traitée dans notre article sur le Scheduler et la contrainte Cron : je n'y reviens que brièvement.
Tu as sûrement écrit ce validator au moins trois fois. Un DTO reçoit un userId ou un productSku, et tu fais la même chose à chaque fois : find(), test de null, violation à la main. Pour un upload audio, c'est pire : tu fais confiance au MIME type et à la taille, alors qu'un fichier de quatre secondes en 8 kHz mono passe très bien ces deux contrôles.
EntityExists : vérifier qu’un identifiant pointe vers une entité Doctrine
EntityExists vit dans le bridge Doctrine, pas dans le composant Validator. Elle signale une entité manquante comme une violation de validation sur la propriété, agrégée et traduite comme les autres, au lieu d'une exception métier dans le handler. Exemple du billet :
use App\Entity\Product;
use App\Entity\User;
use Symfony\Bridge\Doctrine\Validator\Constraints\EntityExists;
use Symfony\Component\Validator\Constraints as Assert;
final class AssignOrderCommand
{
#[Assert\NotBlank]
#[EntityExists(entityClass: User::class)]
public string $userId;
// look up the value by some field other than the entity identifier
#[EntityExists(entityClass: Product::class, identifierField: 'sku')]
public ?string $productSku = null;
}- Par défaut, la recherche se fait sur l’identifiant de l’entité ;
identifierFieldchange le champ, commeskuici. repositoryMethodappelle ta propre méthode de repository ; tout résultat truthy (entité, compteur,true) compte comme « existe ». Pratique pour exprimer « existe et n’est pas archivée ».identifierFieldetrepositoryMethodsont mutuellement exclusifs.emdésigne l’object manager quand il ne se déduit pas de la classe.
EntityExists accepte null et la chaîne vide : il faut donc l'associer à NotBlank quand la valeur est obligatoire. Sans cela, un champ omis passe la validation en silence. La PR #63483 précise deux comportements utiles : la recherche passe par findOneBy(), et l'entité chargée reste dans l'identity map de l'EntityManager, ce qui sert de préchargement pour ton handler ; et une valeur qui ne se convertit pas dans le type du champ (un UUID malformé) produit une violation dédiée au lieu d'une exception Doctrine en 500. Ces DTO validés s'articulent naturellement avec les formulaires définis par attributs de Symfony 8.2.
EntityExists ou UniqueEntity : lequel choisir ?
La question a été posée sous le billet : « je veux garantir l'unicité d'un e-mail, mais laisser l'utilisateur garder le sien ». Nicolas Grekas répond que c'est un contrôle d'unicité, pas d'existence, et que UniqueEntity le couvre déjà. Sur un DTO, il faut lui dire quelle propriété porte l'identifiant :
#[UniqueEntity(fields: 'email', entityClass: User::class, identifierFieldNames: ['id'])]
final class UpdateUserEmail
{
public int $id;
public string $email;
}Règle de décision : « cette référence doit pointer vers quelque chose qui existe », c'est EntityExists ; « cette valeur ne doit pas déjà exister ailleurs », c'est UniqueEntity. Dans les deux cas, ce sont des lectures en base, pas des garanties d'intégrité.
Les pièges d’EntityExists en production
- Une requête par validation. Quatre références dans un DTO, c’est quatre requêtes avant même ton handler. Pour un contrôle pur, un
repositoryMethodqui compte évite l’hydratation. - N+1 sur les collections. Combinée à
Assert\All, une liste de 200 identifiants donne 200 requêtes. Je n’ai pas trouvé de mode groupé dans la contrainte : pour les listes volumineuses, valide en un seulIN (...)dans le handler. - Course entre validation et écriture. L’existence au moment de la validation ne garantit rien une milliseconde plus tard ; la clé étrangère reste le filet.
- Existence n’est pas autorisation. Qu’un
userIdexiste ne dit pas que l’appelant a le droit d’agir dessus. Un retour « existe / n’existe pas » exposé tel quel peut même servir d’oracle d’énumération. Pour des ressources scopées par client, utilise unrepositoryMethodqui filtre sur le tenant.
Audio : valider durée, bitrate et codec d’un fichier
Votre dette technique s'accumule ?
Audit complet en 10 jours. Recommandations priorisées et actionnables.
Symfony 7.4 avait introduit la contrainte Video ; 8.2 apporte son pendant audio. Elle étend File, donc maxSize et mimeTypes restent disponibles, mais elle regarde les caractéristiques réelles du média plutôt que l'étiquette. Exemple du billet :
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
class PodcastEpisode
{
#[Assert\Audio(
maxSize: '200M',
// durations are in seconds, bitrates in bits per second and sample rates in Hz
minDuration: 60,
maxDuration: 3 * 3_600,
minBitrate: 96_000,
allowedSampleRates: [44_100, 48_000],
maxChannels: 2,
allowedCodecs: ['mp3', 'aac'],
allowedContainers: ['mp3', 'mp4'],
)]
public ?File $audioFile = null;
}Attention aux unités : les durées sont en secondes, les bitrates en bits par seconde (96_000 pour 96 kbit/s), les fréquences d'échantillonnage en hertz. mimeTypes vaut audio/* par défaut, mais contrairement à Video, codecs et conteneurs ne sont pas restreints tant que tu ne renseignes pas allowedCodecs et allowedContainers. La PR explique ce choix : l'espace des noms de codecs audio est trop fragmenté, avec une douzaine de variantes PCM pour le seul WAV. Les valeurs attendues sont les noms renvoyés par ffprobe : vérifie-les sur tes fichiers réels avant de figer ta liste.
Audio en production : ffprobe, conteneurs et coût
Dès qu'un contrôle spécifique à l'audio est configuré, la contrainte passe par le composant Process et le binaire ffprobe de FFmpeg. Dans le code de la branche 8.2, son absence lève une LogicException au moment de la validation : un 500 au premier upload, pas une erreur de formulaire. Les images PHP allégées n'embarquent pas FFmpeg ; ajoute-le, et fais échouer le build s'il manque. Exemple illustratif, à adapter à ta distribution :
# Illustratif : ffprobe est fourni avec le paquet ffmpeg
FROM php:8.4-fpm-alpine AS app
RUN apk add --no-cache ffmpeg \
&& ffprobe -version | head -n1
USER www-data- Les vérifications héritées de
Filepassent d’abord : un fichier trop gros est rejeté sans lancer ffprobe. GardemaxSizestrict. - Chaque validation lance un processus externe, de façon synchrone. Pour de gros fichiers, valide en asynchrone avec Messenger plutôt que dans la requête HTTP.
- Un fichier illisible ou sans flux audio produit une violation dédiée ; plusieurs flux audio dans un même conteneur sont rejetés.
- Une validation de caractéristiques ne remplace ni l’antivirus ni l’isolation du stockage : un fichier validé reste un fichier utilisateur.
Et la contrainte Cron ?
La troisième contrainte du billet, Cron (PR #64538), rejette des expressions qu'une simple regex laisse passer, comme 60 * * * * ou */0 * * * *, avec un mode pour les formes hachées du Scheduler. Usage, contexte et pièges sont détaillés dans notre article dédié au Scheduler.
Comment utiliser ces contraintes dès la 8.2
- Repère tes
Assert\Callbacket validators maison qui fontfind()puis violation, et remplace-les parEntityExists. - Ajoute
NotBlankpartout où l’ancienne logique traitaitnullcomme invalide, sinon la sémantique change. - Garde tes contrôles d’unicité (
UniqueEntity), tes contrôles de droits et tes validations groupées enIN (...). - Installe ffprobe dans toutes les images qui valident des uploads, et fige tes listes de codecs sur un échantillon réel (MP3, M4A, WAV, OGG).
- Vérifie que ta version de DoctrineBundle enregistre bien le validator d’
EntityExists; la PR mentionnait une PR compagnon, dont je n’ai pas vérifié la sortie.
Ces deux contraintes retirent du code sans en ajouter à maintenir, ce qui est le bon critère. Si tu veux pousser la validation plus loin sur le même DTO, notre article sur les contraintes Validator avec closures inline montre ce qui est possible.
Ces contraintes ne protègent que les endroits où l'on pense à les poser. Le jour où un identifiant ou un fichier arrive sans validation, ou avec une validation sans contrôle d'accès, personne ne le voit avant l'incident. C'est ce qu'un Bear Scan passe en revue sur une base de code existante : en dix jours, des recommandations priorisées, validation des entrées et failles d'accès comprises.

Écrit par
Flavien Métivier
Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.
Mon parcours