Using Symfony Messenger to Manage Message Queues in Symfony

mainick

Maico Orazio

Posted on May 6, 2024

Using Symfony Messenger to Manage Message Queues in Symfony

Symfony, the famous PHP framework, offers a wide range of powerful tools and components for developers of all levels. One of these tools is Symfony Messenger, a bundle that greatly simplifies message handling within Symfony applications.

What is Symfony Messenger?

Symfony Messenger is a bundle that provides everything you need to consume messages from a message queue. This approach is extremely useful for improving the performance of our applications by allowing us to separate heavy tasks and handle them through a separate worker.

Advantages of Message Queuing

Message queuing allows you to:

  • Improve application performance.

  • Handle heavy tasks asynchronously.

  • Maintain a fast response time for users.

If you are not familiar with message queuing, I recommend delving into the topic to fully understand its benefits.

Use Case

To better understand the usefulness of Symfony Messenger, let's consider a common use case: managing orders and shipments in an application.

Let's imagine having to perform several tasks every time an order is updated, such as:

  • Updating the order status on the website.

  • Sending a confirmation email to the user.

  • Perhaps, sending a notification via SMS.

  • Executing other internal scripts.

All these tasks require calls to different services, which can take a lot of time, and our goal is to have the shortest response time possible.

By using Symfony Messenger, we could delegate these tasks to a separate worker, significantly improving the overall performance of the application.

Introduction and Installation

To start using Symfony Messenger, we need to first install the package via Composer:

composer require symfony/messenger
Enter fullscreen mode Exit fullscreen mode

This package provides everything needed to create and manage messages and message handlers within our application.

Messages and Handlers

The core of Symfony Messenger revolves around messages and message handlers. A message represents the data to be processed, while a message handler contains the logic to process that message.

For example, suppose we need to send a registration confirmation email to a user who has just registered on our site. We will create a message called SendRegistrationEmailMessage which will contain the user ID.

<?php

namespace App\Message;

class SendRegistrationEmailMessage
{
    public function __construct(
        private readonly int $userId
    ) {}

    public function getUserId(): int
    {
        return $this->userId;
    }
}
Enter fullscreen mode Exit fullscreen mode

Instead of executing everything at the exact moment the registration request is sent, we will queue a message containing the user ID. This way, our controller no longer needs to handle an exception, and we could send a "OK" (200) response much faster.

Next, we will create a message handler called SendRegistrationEmailMessageHandler that will retrieve the user from the ID and send the confirmation email.

<?php

namespace App\MessageHandler;

use App\Entity\User;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class SendRegistrationEmailMessageHandler
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager
    ) {}

    public function __invoke(SendRegistrationEmailMessage $message): void
    {
        $user = $this->entityManager->getRepository(User::class)->find($message->getUserId());

        if (!$user instanceOf User) {
            // Handle entity not found case
        }

        // Send the email
    }
}
Enter fullscreen mode Exit fullscreen mode

Symfony Messenger simplifies this process, allowing us to focus on business logic without worrying about implementation details.

Let's see how.

Symfony Messenger comes with a PHP attribute #[AsMessageHandler], so that Symfony considers this service as a message handler to route the correct messages to.

The type of the message to consume is declared in the __invoke method.
This means that for each message, we should create a message handler, with the correct type in the __invoke method, and the routing will be handled automatically by Symfony.

Message Transport and Routing

Symfony Messenger supports various types of transports:

  • AMQP services (like RabbitMQ).

  • Doctrine (storing messages in an SQL table).

  • Cache services (like Redis).

First of all, if we intend to use an AMQP protocol, we need to install the package:

> composer require symfony/amqp-messenger
Enter fullscreen mode Exit fullscreen mode

If we intend to use Doctrine, we need to install the following:

> composer require symfony/doctrine-messenger
Enter fullscreen mode Exit fullscreen mode

Finally, if we want to use Redis as our transport, this is the package we need to install:

> composere require symfony/redis-messenger
Enter fullscreen mode Exit fullscreen mode

If you want to learn more about other transports, you can take a look at the relevant documentation.

We can easily configure the desired transport in the messenger.yaml configuration file, which we will find in config/packages along with all the other configuration files of installed packages.

The transport key contains all the configuration regarding message handling (consumption).

The failure-transport key contains the name of the transport to be used in case of issues (exception thrown during handling).
If something goes wrong here, the stack trace and exception details will be saved in the message and can be retrieved later to be properly handled.

We can also configure the consumer to perform x retries before sending an error message to the "failed" queue and manage different priorities for each queue.

Finally, the routing part explains how to route a specific instance of the message to the transport.

Here is an example of how our configuration file for the above case might look:

framework:
    messenger:
        transports:
            registration_email:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                failure_transport: registration_email_failed
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 2
                    max_delay: 0
                options:
                    exchange:
                        name: registration_email
                    queues:
                        registration_email: ~

            registration_email_failed:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    exchange:
                        name: registration_email_failed
                    queues:
                        registration_email_failed: ~

        routing:
            'App\Message\SendRegistrationEmailMessage': registration_email
Enter fullscreen mode Exit fullscreen mode

Dispatching the Message

Dispatching a message is as simple as calling the MessageBus service provided by Symfony Messenger and passing the message to dispatch. The bus will take care of the rest, routing the message to the appropriate handler for processing.

Here's an example:

<?php

namespace App\Service;

use App\Message\SendEMailRegistrationEmailMessage;

class MyExampleService
{
    public function __construct(
        private readonly MessageBusInterface $messageBus
    ) {}

    public function doSomething(): void
    {
        // Any logic...

        $message = new SendEMailRegistrationEmailMessage($userId);

        # This will dispatch the message to the correct transport
        $this->messageBus->dispatch($message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Architecture

The architecture of Symfony Messenger is well-structured and involves the use of a publisher (controller, service, command, etc.) that sends a message to the bus. If the bus is synchronous, the message is consumed directly by a handler. If the bus is asynchronous, the message is sent via a transport to a queuing system, where it is processed by a separate worker.

If the transport is asynchronous, the message must be serialized in order to be interpreted and consumed correctly by the worker. Symfony natively supports two serialization modes:

By default, PHP's native serialization is used, which represents the class itself of the message.

If another application consumes the same message queue, it may not have this class, so it will be impossible to deserialize the message. For this reason, we will use a more traditional exchange format. Usually JSON, but we could use XML, Protobuf, or any other interoperable serialization language.

Conclusion

Symfony Messenger is a powerful tool for message handling within Symfony applications. With its support for various transport types and its simple architecture, it allows us to improve application performance and provide a better user experience.

I hope this article has provided you with a comprehensive overview of Symfony Messenger and inspired you to use it in your future Symfony applications! If you want to receive updates on future articles, feel free to follow my account on Medium!

Good work! 👨‍💻

💖 💪 🙅 🚩
mainick
Maico Orazio

Posted on May 6, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related