Ce qui se passe
Deux défauts classiques de la messagerie asynchrone trouvent une réponse native
dans Symfony 8.2. D'une part, écrire en base et publier sur un broker (AMQP,
Amazon SQS…) ne forme pas une opération atomique : une transaction annulée
laisse un message déjà envoyé, un broker indisponible laisse des données
enregistrées sans leur message. D'autre part, les brokers refusent les messages
au-delà d'une certaine taille. Messenger reçoit une option de transport pour
chacun : outbox, contribuée par Nicolas Grekas, et claim_check, par Yanick
Witschi.
L'outbox : le message entre dans la transaction
Jusqu'ici, le contournement consistait à router le message vers le transport Doctrine pour que son insertion rejoigne la transaction — au prix d'une base qui devient la file interrogée par tous les workers. Le motif transactional outbox garde l'écriture transactionnelle mais confie la suite à un worker dédié, le relais, qui relit les messages stockés et les pousse vers le vrai broker. Les consommateurs continuent de lire le broker.
config/packages/messenger.yamlYAMLframework:
messenger:
transports:
orders:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
outbox: db_outbox
db_outbox: 'doctrine://default?queue_name=outbox'php bin/console messenger:consume db_outbox # le relais, sans handler
php bin/console messenger:consume orders # les handlers, comme avantQuatre règles à connaître :
- le transport d'outbox est obligatoirement Doctrine, sur la même connexion que les données de l'application ;
- un délai s'applique pendant l'attente dans l'outbox, le message est ensuite transmis sans délai ;
- un échec de transmission suit la stratégie de retry et le transport d'échec de l'outbox ; un échec de traitement repart directement vers le transport cible ;
- l'ordre de transmission n'est pas garanti.
Le claim check : le gros message reste au vestiaire
Plutôt que de réduire les messages, le motif claim check dépose ceux qui dépassent un seuil dans un stockage à part et n'envoie qu'un ticket. Le réglage est par transport, puisque chaque broker a sa propre limite.
config/packages/messenger.yamlYAMLframework:
cache:
pools:
messenger.claim_check.cache:
adapter: cache.adapter.redis
default_lifetime: 604_800 # 7 jours
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
claim_check:
cache_pool: messenger.claim_check.cache
max_size: 200_000Sous max_size (en octets, corps et en-têtes encodés), rien ne change. Au-delà,
le message va dans le pool et le transport ne porte qu'un identifiant aléatoire
accompagné d'une somme de contrôle, vérifiée à la consommation. N'importe quel
pool PSR-6 convient : Redis, Valkey, Memcached, PDO, Doctrine DBAL.
Le pool fait désormais partie de la livraison, ce qui impose trois précautions :
qu'il soit joignable par tous les producteurs et consommateurs ; qu'il ne soit
pas partagé avec le reste de l'application, car le vider efface les messages en
attente ; que son default_lifetime dépasse la vie complète d'un message,
délais, retries et séjour dans le transport d'échec compris. Un ticket expiré
lève une ClaimCheckNotFoundException, enveloppée dans une
MessageDecodingFailedException, et suit le chemin normal des retries.
Source : New in Symfony 8.2: Messenger Outbox and Claim Check, blog Symfony, 21 septembre 2026.