Skip to Content
Course content

229: Symfony Messenger Component for Async Messaging

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a colleague who had built a sleek new user onboarding flow. Everything looked great until I actually tested it. I clicked "Complete Registration" and the browser just... sat there. For about six seconds, the page hung. I eventually found out that in the controller, he was triggering a welcome email, hitting an external CRM API to create a lead, and generating a starter PDF guide—all synchronously. If the CRM API was slow or the mail server lagged, the user was stuck staring at a white screen, wondering if the site had crashed. He even had a bug where users would click "Submit" three times because of the lag, resulting in three welcome emails and three CRM entries.

This is exactly why the Symfony Messenger component exists. You shouldn't make your user wait for tasks that don't need to happen right now to provide a successful response. By moving these "side effects" into an asynchronous queue, you can tell the user "Success!" in 50 milliseconds, while the heavy lifting happens in the background.

Decoupling the Intent from the Execution

At its core, Messenger uses a "Bus" pattern. Instead of calling a service method directly in your controller, you dispatch a Message. A message is just a simple PHP class—a Data Transfer Object (DTO)—that describes what needs to happen. It doesn't contain any logic; it just holds the data. For example, instead of calling $emailService->sendWelcomeEmail($user), you dispatch a SendWelcomeEmailMessage($userId).

Then, you write a MessageHandler. This is where the actual work lives. The handler is a service that "subscribes" to that specific message type. When the Messenger bus sees a SendWelcomeEmailMessage, it looks for the corresponding handler and executes its __invoke method. This separation is powerful because the controller no longer cares how the email is sent or when it happens; it only cares that the intent to send it has been recorded.

// The Message (DTO)
readonly class SendWelcomeEmailMessage {
    public function __construct(public int $userId) {}
}

// The Handler
class SendWelcomeEmailHandler implements MessageHandlerInterface {
    public function __invoke(SendWelcomeEmailMessage $message) {
        // I'd inject the Mailer here and do the actual sending
        $user = $this->userRepository->find($message->userId);
        $this->mailer->sendWelcome($user);
    }
}

Routing Messages to the Background

By default, Symfony handles messages "synchronously," meaning it just calls the handler immediately. To get that performance boost I mentioned, we need to route the message to a Transport. This is where your queue lives—be it RabbitMQ, Redis, or even a database table via Doctrine.

In your messenger.yaml, you define these transports and then map your messages to them. I usually prefer the Doctrine transport for smaller projects because it doesn't require installing extra software, though I'll move to Redis the second the volume gets high enough to cause database lock contention.

# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async: 'doctrine://default'

        routing:
            'App\Message\SendWelcomeEmailMessage': async

Once this is configured, dispatching the message merely inserts a row into a database table (the "queue"). The request finishes instantly. But now, the work isn't actually being done—it's just sitting there. To process the queue, you need to run a worker process via the CLI. I always recommend using a process manager like Supervisor in production to ensure this command stays running:

php bin/console messenger:consume async

One thing to watch out for: since the handler runs in a separate process, it doesn't share the same memory or state as the original web request. Always pass the ID of an object (like userId) in your message rather than the object itself. This ensures the handler fetches the freshest data from the database when it actually starts processing.




📋 Practical Task

Implement an Asynchronous PDF Invoice Generator

You are working on an e-commerce platform. Currently, when a user completes a purchase, the system generates a PDF invoice and uploads it to S3 before redirecting the user to the "Thank You" page. This is taking 4-7 seconds per order, which is unacceptable.

Your Task: Refactor this process using the Symfony Messenger component to make it asynchronous.

  • Create a GenerateInvoiceMessage DTO that accepts an orderId.
  • Create a GenerateInvoiceHandler that simulates the heavy lifting (use sleep(5) to mimic PDF generation and a dump() or log entry to confirm it finished).
  • Configure messenger.yaml to route this specific message to an async transport using the doctrine://default transport.
  • Dispatch the message from a dummy controller action.
  • Verify the implementation by triggering the controller action and observing that the page loads instantly, and then running php bin/console messenger:consume async to see the "PDF generation" complete in the terminal.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.