Using the Symfony Messenger component

Using the Symfony Messenger component

When developing a large scale application the software often needs to do a lot of different tasks. These tasks can consume a lot of time. Because you want your application to be fast and offer a great user experience you don’t want to process these tasks immediately. This is where the Symfony Messenger component comes into play.

What is the Symfony Messenger component?

The Messenger component is an easy to use component for sending and receiving messages to and from a message bus. The bus consists of different middleware classes. This middleware can perform different actions on the messages and manipulate the metadata of the messages. There are a few concepts we need to cover before diving in too deep.

  • Receiver: The receiver is responsible for receiving the messages from the queue and sending them on the bus.
  • Sender: The sender is at the other end of the bus its responsibility is processing the message and sending the message to ‘something’ like a message broker.
  • Handler: The handler is responsible for processing the message using the business logic of the application.
  • Envelope: Every message is wrapped in an envelope object (just like sending letters in real-life). The envelope object allows for adding extra metadata to the message, which can in turn be used by the middleware.
  • Envelope stamps: Just like real-life letters the envelope can accept stamps. Stamps are objects which store pieces of metadata during the life of the message.
  • Middleware: The responsibility of the middleware is performing logic which is not directly related to the business logic. For example logging or validating the messages.

This seems complex, but the puzzle pieces will fall into place with the examples later in this article.

Using the messenger component

Before we can send messages we first need to create a message. In this article we will use the message bus for sending email notifications to for our application. Let’s create a message class for our user registration email.

namespace App\Messages\Email;

use App\Messages\Interfaces\EmailMessageInterface;

class UserRegistrationEmail implements EmailMessageInterface
{
    /**
     * @param string $userId
     * @param string $subject
     * @param string $template
     * @param array $variables
     */
    public function __construct(private string $userId, private string $subject, private string $template, private array $variables = []) {}

    /**
     * @return string
     */
    public function getUserId(): string
    {
        return $this->userId;
    }
    
    /**
     * @return string
     */
    public function getSubject(): string
    {
        return $this->subject;
    }

    /**
     * @return string
     */
    public function getTemplate(): string
    {
        return $this->template;
    }

    /**
     * @return array
     */
    public function getVariables(): array
    {
        return $this->variables;
    }
}Code language: PHP (php)

This is the UserRegistrationEmail message, which consist of a user, a subject, a template and some variables. Imagine we have an EmailService class which will take these and send the email for us based on the specified twig template.

To make sure our EmailService class is used for sending email messages we need to create a message handler which handles our UserRegistrationEmail message and calls the EmailService for us.

namespace App\Messages\Handler;

use App\Application\Service\EmailService;
use App\Messages\Email\UserRegistrationEmail;
use App\Messages\Interfaces\EmailMessageInterface;
use App\Repository\UserRepository;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

class EmailHandler implements MessageHandlerInterface
{
    /**

     * @param UserRepository $userRepository
     * @param EmailService $emailService
     */
    public function __construct(private UserRepository $userRepository, private EmailService $emailService) {}

    /**
     * @param EmailMessageInterface $message
     */
    public function __invoke(EmailMessageInterface $message)
    {
        $user = $this->userRepository->find($message->getUserId());
        $this->emailService->send($user, $message->getSubject(), $message->getTemplate(), $message->getVariables());
    }
}Code language: PHP (php)

This message handler is pretty straight forward. It’s a simple class with an __invoke method. This method receives the message from the message bus and passes it on to the EmailService. Because Symfony uses autowiring this message handler is automatically configured. Due to the type hinting of the parameter of the __invoke method the message bus knows to invoke this handler for EmailMessageInterface messages.

Sending messages

Now that we’ve created a message class and a handler we can start sending the messages to the bus. We can use the messenger.default_bus for this. We can inject this bus into our service. Let’s make this happen!

namespace App\Application\Service;

use App\Entity\User;
use App\Messages\Email\UserRegistrationEmail;
use Symfony\Component\Messenger\MessageBusInterface;

class UserService
{
    /**
     * @param MessageBusInterface $messageBus
     */
    public function __construct(private MessageBusInterface $messageBus) {}

    /**
     * @param string $email
     * @param string $password
     *
     * @return User
     */
    public function registerUser(string $email, string $password): User
    {
        // create a new user here (removed for brevity)

        // Dispatch the email message to the message bus
        $this->messageBus->dispatch(new UserRegistrationEmail(
            $user->getId(),
            'Activate your account',
            'new-user',
            ['activation-token' => $activationToken]
        ));
    }
}Code language: PHP (php)

We’ve injected the message bus into our UserService using the MessageBusInterface. Because of this our service now has access to the message bus. In our registerUser method we want to make sure the user gets an account activation message. To make this happen we dispatch an instance of our UserRegistrationEmail class using the dispatch function of the injected MessageBusInterface. Because of this the message is sent onto the bus.

Routing messages

In our previous example of sending the account activation email this email is sent immediately. For account activation mails this is what we want. But other messages might have less priority and don’t need to be sent immediately. We can send these messages to a queue first, which will be processed by one or more workers asynchronously.

Let’s create our message class for this example.

namespace App\Messages\Email;

use App\Entity\User;
use App\Messages\Interfaces\EmailMessageInterface;

class NewMessageNotificationEmail implements EmailMessageInterface
{
    /**
     * @param User $user
     * @param string $subject
     * @param string $template
     * @param array $variables
     */
    public function __construct(private User $user, private string $subject, private string $template, private array $variables = []) {}

    // Getter functions removed for brevity
}Code language: PHP (php)

This message class defines a NewMessageNotificationEmail class. Messages of this class can be sent with a lower priority because no immediate user interaction is needed, unlike the account activation email.

Now we need to add some configuration to make sure the component knows this message needs to be queued first.

framework:
    messenger:
        transports:
            lowPriority: "%env(MESSENGER_TRANSPORT_DSN)%"

        routing:
            'App\Messages\Email\NewMessageNotificationEmail': lowPriorityCode language: JavaScript (javascript)

This piece of YAML configures a new transport for the messenger component called lowPriority. The routing part of the configuration is where the magic happens. Here we tell the messenger component which transport needs to be used for different messages. You can specify multiple transports and send messages to multiple transport by defining the transports for messages as an array.

Now that the messages are sent to the transport we need a way to consume the messages. The Symfony Messenger component provides a command for this purpose: bin/console messenger:consume lowPriority This command will process the lowPriority queue and consume the messages. You can create a cronjob or use a different method to call this command.

If a message doesn’t match any routing rules it will be processed immediately like the UserRegistrationEmail message in our example.

Configuring messenger transports

There are different transports we can use for our messenger component. By default AMQP, doctrine and redis are supported by the component. You can specify these in your environment by using the .env file or by using environment variables.

It is also possible to create your own custom transports, for example when you want to use Google Pub/Sub system.

Conclusion

These are the basics on how to use the Symfony Messenger component. There are a lot more options and possibilities with this versatile component. Which we’ll cover in a different article.

Leave a Reply

Your email address will not be published. Required fields are marked *