Symfony Messenger is a powerful component that provides a message-driven architecture for building robust, scalable, and efficient applications. It decouples the process of message dispatching from message handling, enabling you to process tasks asynchronously, queue jobs, and scale your application more effectively.
In this blog post, we’ll walk through the core concepts of Symfony Messenger, how it works, and a detailed workflow that demonstrates how messages move through the system.
What is Symfony Messenger?
Sponsored
Symfony Messenger is a message queue and task handling library for Symfony applications. It provides an easy way to send and receive messages asynchronously, either immediately or with a delayed execution, by using a transport system (like a message queue, database, or other systems). It also facilitates background processing, enabling your app to scale by processing tasks outside of the main request-response cycle.
Core Concepts
Before diving into the workflow, let’s familiarize ourselves with the main components of Symfony Messenger:
- Message: A PHP object representing the data to be sent or processed.
- Handler: A PHP class responsible for handling the logic associated with a message.
- Transport: The system used to store and deliver messages (e.g., message queues, databases, etc.).
- Middleware: Layers that modify or intercept the message as it flows through the system.
- Bus: A class responsible for dispatching messages to the appropriate transport and invoking their handlers.
Symfony Messenger Workflow
The following is the typical workflow for how Symfony Messenger operates, from dispatching a message to processing it asynchronously.
Step 1: Define a Message
The first step in using Symfony Messenger is to create a message. A message is simply a PHP object that contains the data you want to pass to a handler.
A message should be a simple DTO (Data Transfer Object) that represents the job or data to be processed. It should have no logic, just data to transport.
Example Message
// src/Message/OrderMessage.php
namespace App\Message;
class OrderMessage
{
private string $orderId;
public function __construct(string $orderId)
{
$this->orderId = $orderId;
}
public function getOrderId(): string
{
return $this->orderId;
}
}
In this example, the OrderMessage
represents the ID of an order that needs to be processed. The message carries the data, but the logic is defined elsewhere (in the handler).
Step 2: Dispatch the Message
Once the message is defined, you can dispatch it to be processed asynchronously. The dispatcher sends the message to a transport, where it will be stored temporarily before being handled.
You can dispatch a message using the
MessageBusInterface
(Bus) service.
Example Dispatching the Message
// src/Controller/OrderController.php
namespace App\Controller;
use App\Message\OrderMessage;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class OrderController extends AbstractController
{
private MessageBusInterface $bus;
public function __construct(MessageBusInterface $bus)
{
$this->bus = $bus;
}
#[Route('/order/place')]
public function placeOrder(string $orderId): Response
{
// Dispatch the order message (to a transport system)
$this->bus->dispatch(new OrderMessage($orderId));
return $this->render('order_placed.html.twig');
}
}
In this example, when an order is placed, a OrderMessage
is dispatched. This message is sent to the transport system (e.g., a queue) and is waiting to be processed asynchronously.
Step 3: Send Message to Transport
After dispatching, the message will be sent to a transport system. The transport is a storage system for the messages (e.g. database). Symfony Messenger supports several transport types, including:
- AMQP (RabbitMQ, Amazon SQS)
- Redis
- Database (Doctrine DBAL)
- In-memory (for testing purposes)
This transport is responsible for temporarily holding the message until it can be processed by a worker.
Example Transport Configuration
# config/packages/messenger.yaml
framework:
messenger:
transports: # define your transport configurations here
async_1:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%' # E.g., amqp://localhost:5672
options:
exchange:
name: app_exchange
async_2: doctrine://default
async_3: ...
routing:
# OrderMessage will be sent to "async_1" transport
'App\Message\OrderMessage': async_1
# SomeOtherMessage will be sent to "async_2" transport
'App\Message\SomeOtherMessage': async_2
In this configuration, OrderMessage
is routed to the async_1
transport. This means that when the OrderMessage
is dispatched, it will be sent to the transport (RabbitMQ
) defined in async_1
If a message doesn’t match any routing rules, it won’t be sent to any transport and will be handled immediately.
Step 4: Worker Picks Up the Message
Once the message is stored in the transport system, a Worker is responsible for pulling the message from the transport and processing it. A worker is a process that continuously runs in the background, fetching messages from the transport system and processing them using the corresponding handler.
How Does the Worker Work?
- The worker checks the transport for any new messages to process.
- The worker retrieves a message and hands it off to the handler for processing.
- After processing the message, if no exception is thrown, the worker acknowledges that the message has been successfully handled, and the message is removed from the transport.
- If an exception is thrown, the worker can retry or move the message to a dead-letter queue.
Step 5: Middleware Intercepts the Message
Before the message reaches its handler, Middlewares (if any) may interfere to modify, log, validate, or intercept the message. A middleware is a layer that sits between the point where the message is dispatched and the handler that processes it.
- Middleware can be used to intercept, modify, or stop a message before it reaches the handler.
- Symfony Messenger provides several built-in middlewares, but you can also create your own custom middleware to suit your application.
Example Logging Middleware
namespace App\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
class LoggingMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$message = $envelope->getMessage();
// do something here (e.g message logging)
// Proceed with the next middleware or handler
return $stack->next()->handle($envelope, $stack);
}
}
This middleware intercepts the message before it reaches the handler, logging its details. You can add multiple middleware layers to perform different tasks (logging, validation, etc.).
Step 6: Process the Message (Handler)
A handler is responsible for processing the message when it’s pulled from the transport (by the worker). Handlers are simple PHP classes that contain the logic to act upon the data in the message.
A handler implements the MessageHandlerInterface
and contains a __invoke
method, which is triggered when a message of a specific type is received.
Example Handler
namespace App\Handler;
use App\Message\OrderMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class OrderMessageHandler
{
public function __invoke(OrderMessage $message)
{
$orderId = $message->getOrderId();
// Process the order here
}
}
In this example, the OrderHandler
processes the order when it is received from the transport. The __invoke
method extracts the order ID from the message and processes the order.
Finally: Acknowledge or Fail
As explained in the worker step, after the message is processed successfully, it is either:
- acknowledged (removed from the transport) or
- failed (if there was an issue)
Symfony Messenger supports retrying messages a configurable number of times before moving them to a “failed” state.
If a message fails, you can configure dead-letter queues or failure handlers to process these messages separately.
Workflow summary
[ Message ] ← Dispatched
↓
[ Transport Queue ]
↓
[ Worker ]
↓
[ Middleware Stack ] ← (optional) interception logic
↓
[ Handler ] ← executes your actual business logic
Conclusion
Symfony Messenger enables you to implement powerful, asynchronous task processing and message-driven architectures with minimal effort. By breaking down your application’s tasks into discrete messages, handling them asynchronously, and using transports, you can achieve a highly scalable and responsive system.