Symfony Messenger : files de messages, workers et architecture asynchrone en 2026

Guide complet Symfony Messenger couvrant l'architecture du bus de messages, la configuration des transports, la gestion des workers, le middleware de deduplication, les strategies de retry et le transport AMQP streaming dans Symfony 7.3+.

Symfony Messenger architecture asynchrone files de messages et workers

Symfony Messenger constitue la brique fondamentale du traitement asynchrone dans l'ecosysteme PHP moderne. Le composant orchestre l'envoi, le routage et la consommation de messages via un bus structure, des transports configurables et des workers supervises. Avec les apports de Symfony 7.3 et la preparation de Symfony 8.0 -- middleware de deduplication, transport AMQP streaming, keepalive Doctrine -- Messenger se positionne desormais face aux systemes de files dediees comme Laravel Horizon ou Sidekiq en termes de couverture fonctionnelle.

Nouveautes Symfony 7.3+ pour Messenger

Symfony 7.3 introduit le DeduplicateMiddleware pour la deduplication automatique des messages, le keepalive sur le transport Doctrine pour empecher la redistribution des taches longues, et l'attribut #[AsMessage] pour le routage declaratif des transports. Ces trois ajouts transforment la gestion des files de messages en production.

Architecture de Symfony Messenger : bus, transport et worker

Le composant Messenger separe trois responsabilites distinctes : le dispatching (le bus), la livraison (le transport) et le traitement (le worker). Un message est un objet PHP simple. Un handler est une classe invocable. Le bus relie les deux, traverse la pile de middlewares et serialise optionnellement le message vers un transport pour un traitement asynchrone.

src/Message/InvoiceGenerated.phpphp
namespace App\Message;

final readonly class InvoiceGenerated
{
    public function __construct(
        public int $orderId,
        public string $customerEmail,
    ) {}
}

Ce message ne transporte que des donnees scalaires, pas des entites Doctrine. Passer des identifiants plutot que des objets evite les problemes de serialisation et maintient les messages legers. Le handler recupere les donnees fraiches depuis la base de donnees au moment du traitement.

src/MessageHandler/InvoiceGeneratedHandler.phpphp
namespace App\MessageHandler;

use App\Message\InvoiceGenerated;
use App\Service\InvoiceService;
use App\Service\MailerService;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final readonly class InvoiceGeneratedHandler
{
    public function __construct(
        private InvoiceService $invoiceService,
        private MailerService $mailerService,
    ) {}

    public function __invoke(InvoiceGenerated $message): void
    {
        $pdf = $this->invoiceService->generatePdf($message->orderId);
        $this->mailerService->sendInvoice(
            $message->customerEmail,
            $pdf,
        );
    }
}

Le dispatch du message depuis un controleur ou un service se resume a une seule ligne :

src/Controller/OrderController.phpphp
$this->bus->dispatch(new InvoiceGenerated(
    orderId: $order->getId(),
    customerEmail: $order->getCustomer()->getEmail(),
));

Le bus decide de l'execution synchrone ou asynchrone en fonction de la configuration de routage des transports. Si aucun transport n'est associe au message, le handler s'execute immediatement dans le processus courant. Des qu'un transport est configure, le message est serialise et depose dans la file d'attente correspondante.

Configuration des transports et backends de file

Messenger supporte Doctrine DBAL, Redis, Amazon SQS, Beanstalkd, AMQP (RabbitMQ) ainsi que le nouveau transport AMQP streaming introduit en 2025. Chaque transport se configure via une chaine DSN.

yaml
# config/packages/messenger.yaml
framework:
    messenger:
        failure_transport: failed

        transports:
            async_priority_high:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: high
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 3
                    max_delay: 60000

            async_priority_low:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: low
                retry_strategy:
                    max_retries: 5
                    delay: 5000
                    multiplier: 2

            failed:
                dsn: 'doctrine://default?queue_name=failed'

        routing:
            'App\Message\InvoiceGenerated': async_priority_high
            'App\Message\CleanupTempFiles': async_priority_low

La separation des transports par priorite garantit que les taches sensibles au temps (generation de factures) sont traitees avant les taches de maintenance (nettoyage de fichiers temporaires). Chaque transport dispose de sa propre strategie de retry, ajustee selon la criticite et l'idempotence de ses messages.

Doctrine vs Redis vs AMQP

Le transport Doctrine ne necessite aucune infrastructure supplementaire mais augmente la charge sur la base de donnees. Redis offre une latence sub-milliseconde. AMQP (RabbitMQ) fournit un routage avance, des dead-letter exchanges et le nouveau transport streaming pour les scenarios a haut debit. Le choix depend de l'infrastructure existante et des exigences de performance.

Gestion des workers avec Supervisor

Les workers consomment les messages depuis les transports. En production, la commande messenger:consume s'execute sous un gestionnaire de processus comme Supervisor ou systemd.

bash
# Consume high-priority messages first, then low-priority
php bin/console messenger:consume async_priority_high async_priority_low \
    --memory-limit=128M \
    --time-limit=3600 \
    --limit=500

Les trois flags de limitation empechent les fuites memoire et assurent un redemarrage periodique des workers. --memory-limit arrete le worker lorsqu'il depasse 128 Mo de consommation. --time-limit impose une duree de vie maximale d'une heure. --limit fixe un nombre maximal de messages traites par cycle. Supervisor redemarre le processus apres chaque arret.

ini
; /etc/supervisor/conf.d/messenger-worker.conf
[program:messenger-consume]
command=php /var/www/app/bin/console messenger:consume async_priority_high async_priority_low --memory-limit=128M --time-limit=3600
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
startsecs=0
stopwaitsecs=30
stdout_logfile=/var/log/messenger-worker.log
stderr_logfile=/var/log/messenger-worker-error.log

Le parametre numprocs=2 lance deux workers en parallele, doublant le debit de traitement. Ce nombre s'ajuste en fonction des coeurs CPU disponibles et du temps de traitement moyen par message. Pour les taches IO-bound (appels API, envois d'emails), un ratio de 2 a 4 workers par coeur offre un bon equilibre.

Pipeline de middlewares et CQRS avec bus multiples

Les middlewares encapsulent chaque dispatch de message, ajoutant des preoccupations transversales. La pile de middlewares integree gere la validation, les transactions Doctrine et le routage.

yaml
# config/packages/messenger.yaml
framework:
    messenger:
        default_bus: command.bus
        buses:
            command.bus:
                middleware:
                    - validation
                    - doctrine_transaction
            query.bus:
                middleware:
                    - validation
            event.bus:
                default_middleware:
                    allow_no_handlers: true
                middleware:
                    - validation

Cette configuration implemente le pattern CQRS (Command Query Responsibility Segregation). Les commandes modifient l'etat a l'interieur d'une transaction Doctrine, garantissant la coherence des ecritures. Les queries sont en lecture seule et ne declenchent pas de transaction. Les events autorisent l'absence de handlers via allow_no_handlers: true, activant un pattern pub/sub ou des listeners peuvent etre ajoutes independamment sans modifier le code existant.

La separation en bus distincts apporte un avantage supplementaire lors des entretiens techniques : elle demontre la maitrise de l'architecture hexagonale et de la segregation des responsabilites, deux sujets recurrents dans les postes backend senior.

Prêt à réussir tes entretiens Symfony ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Middleware de deduplication dans Symfony 7.3

Les messages dupliques gaspillent des ressources et peuvent provoquer des effets de bord -- double facturation d'un client, envoi multiple du meme email. Symfony 7.3 introduit le DeduplicateMiddleware pour ignorer automatiquement les messages identiques deja presents dans la file.

src/Message/SendWelcomeEmail.phpphp
namespace App\Message;

use Symfony\Component\Messenger\Stamp\DeduplicateStamp;

final readonly class SendWelcomeEmail
{
    public function __construct(
        public int $userId,
    ) {}
}
php
// Dispatching with deduplication
use Symfony\Component\Messenger\Stamp\DeduplicateStamp;

$this->bus->dispatch(
    new SendWelcomeEmail(userId: 42),
    [new DeduplicateStamp(id: 'welcome-email-42')],
);

Le DeduplicateStamp prend un identifiant de ressource de verrou. Si un message portant le meme identifiant est deja en attente de traitement, le nouveau dispatch est silencieusement ignore. Ce mecanisme repose sur le composant Lock avec un store serialisable (Redis, Memcached ou base de donnees). En entretien, la question porte souvent sur la distinction entre deduplication a l'emission (ce stamp) et idempotence a la reception (verification dans le handler).

Strategies de retry et transport d'echec

Lorsqu'un handler leve une exception, Messenger relance le message selon la strategie de retry du transport. Une fois les tentatives epuisees, le message est transfere vers le failure transport.

src/MessageHandler/PaymentHandler.phpphp
namespace App\MessageHandler;

use App\Message\ProcessPayment;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;

#[AsMessageHandler]
final class PaymentHandler
{
    public function __invoke(ProcessPayment $message): void
    {
        try {
            $this->gateway->charge($message->amount, $message->token);
        } catch (GatewayTimeoutException $e) {
            // Recoverable: retry with backoff
            throw new RecoverableMessageHandlingException(
                'Payment gateway timeout, retrying',
                previous: $e,
            );
        } catch (InvalidCardException $e) {
            // Unrecoverable: send to failure transport immediately
            throw new UnrecoverableMessageHandlingException(
                'Invalid card, no retry',
                previous: $e,
            );
        }
    }
}

RecoverableMessageHandlingException declenche la strategie de retry avec backoff exponentiel. UnrecoverableMessageHandlingException contourne completement les retries et envoie le message directement au failure transport. Cette distinction evite de gaspiller des tentatives sur des messages definitivement invalides -- une carte bancaire rejetee ne deviendra pas valide apres trois tentatives supplementaires.

bash
# Inspect and manage failed messages
php bin/console messenger:failed:show
php bin/console messenger:failed:show 20 --transport=failed

# Retry specific messages
php bin/console messenger:failed:retry 20 30

# Filter and remove by class (Symfony 7.3+)
php bin/console messenger:failed:remove --class-filter="App\Message\CleanupTempFiles"

La commande messenger:failed:show permet d'inspecter les messages echoues avec leur stack trace et le nombre de tentatives effectuees. La commande messenger:failed:retry permet de rejouer des messages specifiques apres correction du probleme sous-jacent. Le filtre par classe, ajoute dans Symfony 7.3, facilite le nettoyage des messages obsoletes sans affecter les autres.

Handlers idempotents

Les retries impliquent qu'un handler peut s'executer plusieurs fois pour le meme message. Chaque handler doit etre concu de maniere idempotente : verifier si le travail a deja ete effectue avant de le repeter. Les contraintes d'unicite en base de donnees et les flags de statut empechent le double traitement.

Keepalive Doctrine et messages a execution longue

Les handlers de messages a execution longue risquent de voir leurs messages redistribues lorsque le timeout de visibilite du transport expire. Symfony 7.2 a introduit le keepalive pour Redis, SQS et Beanstalkd. Symfony 7.3 etend cette fonctionnalite au transport Doctrine.

bash
# Enable keepalive to prevent redelivery during long processing
php bin/console messenger:consume async --keepalive

Le flag --keepalive met periodiquement a jour le timestamp delivered_at dans la table du transport Doctrine, signalant que le worker traite toujours activement le message. Sans ce flag, un message dont le traitement depasse le timeout du transport (5 minutes par defaut) est recupere par un autre worker, provoquant un traitement en double. Ce scenario est particulierement critique pour les taches de generation de rapports ou d'export de donnees volumineuses.

Attribut #[AsMessage] pour le routage declaratif

Symfony 7.2 a introduit l'attribut #[AsMessage], deplacant le routage des transports du YAML vers la classe de message elle-meme.

src/Message/GenerateReport.phpphp
namespace App\Message;

use Symfony\Component\Messenger\Attribute\AsMessage;

#[AsMessage(transport: 'async_priority_low')]
final readonly class GenerateReport
{
    public function __construct(
        public int $reportId,
        public string $format = 'pdf',
    ) {}
}

Cette approche elimine la section routing dans messenger.yaml pour ce message. Le transport est declare a la source, rendant le code auto-documente. Les deux approches (routage YAML et attribut) coexistent, l'attribut prenant la precedence. En entretien, cette fonctionnalite illustre la tendance de Symfony vers la configuration declarative via les attributs PHP 8, un mouvement initie avec #[AsController] et #[AsMessageHandler].

Transport AMQP streaming pour les files a haut debit

Le transport AMQP traditionnel utilise le polling (get()) pour recuperer les messages, generant une charge inutile sur RabbitMQ. Le transport AMQP streaming publie en 2025 adopte un modele push (consume()), reduisant la latence et l'utilisation des ressources.

yaml
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            streaming:
                dsn: 'amqp-lib://guest:guest@localhost:5672/%2f/messages'
                options:
                    exchange:
                        name: app_events
                        type: topic
                    queues:
                        order_events:
                            binding_keys: ['order.*']

Les differences cles par rapport au transport AMQP classique : aucune extension C requise (utilise php-amqplib), livraison en streaming via des connexions TCP persistantes, et support natif des topic exchanges avec routage par binding keys. Ce transport gere plusieurs milliers de messages par seconde avec une consommation CPU minimale. Pour les architectures evenementielles a fort volume, le transport streaming associe a des binding keys granulaires permet de distribuer les messages vers des consumers specialises sans multiplier les exchanges.

Conclusion

  • Passer des identifiants scalaires dans les messages, pas des entites Doctrine ; recuperer les donnees fraiches dans le handler pour eviter les problemes de serialisation et les etats obsoletes
  • Separer les transports par priorite et criticite ; chaque transport dispose de sa propre strategie de retry et de son pool de workers dedie
  • Utiliser RecoverableMessageHandlingException et UnrecoverableMessageHandlingException pour controler explicitement le comportement de retry
  • Activer --keepalive sur les workers du transport Doctrine pour empecher la redistribution des messages a execution longue
  • Appliquer DeduplicateStamp sur les messages ou le traitement en double provoque des effets de bord (paiements, emails, notifications)
  • Implementer CQRS avec des bus multiples : bus de commandes pour les mutations avec transactions Doctrine, bus de queries pour les lectures, bus d'events pour le pub/sub
  • Utiliser Supervisor ou systemd en production avec les flags --memory-limit, --time-limit et --limit pour la gestion du cycle de vie des workers
  • Envisager le transport AMQP streaming pour les scenarios a haut debit necessitant une latence sub-milliseconde

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#symfony
#messenger
#php
#async
#message queue
#workers

Partager

Articles similaires