Ce qui se passe
Certains contrôleurs ne doivent jamais s'exécuter deux fois en même temps :
importer un catalogue, débiter une commande, vendre les dernières places d'une
conférence. Le composant Lock le permet depuis longtemps, mais il fallait injecter
la fabrique de verrous, construire la clé, acquérir le verrou, traiter le cas
« déjà pris » et le libérer. Symfony 8.2 remplace cette mécanique par un seul
attribut, #[Lock], disponible dès que le composant Lock est installé.
Comment ça marche
Le comportement par défaut : une seule requête à la fois par clé ; la requête concurrente reçoit un 409 Conflict.
read: trueacquiert un verrou partagé : plusieurs téléchargements du catalogue en parallèle, mais les imports sont refusés tant qu'un téléchargement tient le verrou, et inversement.- Clés : des marqueurs
{nom}, avec la syntaxe des routes, sont remplacés par l'attribut de requête du même nom. Ils fonctionnent désormais aussi dans les clés de#[RateLimit]. - Libération : à la fin du contrôleur ; pour une
StreamedResponse, une fois le contenu envoyé. - Ordre : sauf si la clé dépend des arguments du contrôleur, le verrou est pris avant leur résolution. Une requête rejetée ne charge pas l'entité Doctrine ; une requête qui attend la charge après l'attente, donc avec des données à jour.
Pour renouveler ou libérer le verrou soi-même, on ajoute un argument
LockInterface (ou SharedLockInterface) : Symfony injecte le verrou acquis par
l'attribut. Rendu nullable, l'argument vaut null quand il n'y en a pas, par
exemple dans une sous-requête qui réutilise le verrou de la requête principale, ou
quand l'option methods exclut la méthode HTTP courante.
use Symfony\Component\Lock\LockInterface;
#[Lock]
public function import(LockInterface $lock): Response
{
// … traitement long
$lock->refresh(); // repousse l'expiration
// …
}Exemple reconstitué d'après la description du billet : pour le nom exact des paramètres de l'attribut, se reporter à la documentation de Symfony 8.2.
Les trois pièges
- TTL de 30 secondes par défaut, sans renouvellement automatique. Si le verrou expire avant la fin du contrôleur, une autre requête peut l'acquérir.
blockingn'a aucun délai propre : une requête en attente patiente aussi longtemps que le verrou est tenu.- Le verrou est pris avant
#[IsGranted], quel que soit l'ordre des attributs (idem pour#[RateLimit]). Un utilisateur sans droit peut recevoir un 409 au lieu d'un 403, ou attendre le verrou avant d'être refusé.
Que faire
Repérer les contrôleurs d'import, de paiement et de stock, poser l'attribut, et injecter le verrou dès qu'un traitement peut dépasser 30 secondes pour le renouveler.
Source : New in Symfony 8.2: Locking Controllers, blog Symfony, 16 octobre 2026.