Introduction to RabbitMQ and Symfony

One day I was trying to learn the deep concepts of RabbitMQ, its use cases and why it's different from other message brokers. I've started by reading the cool documentation and then I was eager to try it out in a demo application.

Turns out it wasn't so easy to setup a Symfony application and connect to RabbitMQ. Google displayed different solutions and I also needed StackOverflow to install some additional dependencies.

Hopefully I could condense all the information and display it here in a simple and fun way.

What am I going to build?

Symfony

But why Symfony, right? It's a popular PHP framework and I really liked its software architecture and integration with RabbitMQ.

First, I installed Symfony CLI and create a a traditional web application:

symfony new --full php-symfony-rabbitmq

My application now can be started by running the following command on the new project directory:

symfony serve

Symfony messenger

composer require symfony/messenger

Following the good documentation, I created a simple class to encapsulate the message to be published:

final class SampleMessage
{
    public function __construct(private string $content)
    {
    }

    public function getContent(): string
    {
        return $this->content;
    }
}

And its respective handler:

final class SampleMessangeHandler implements MessageHandlerInterface
{
    public function __invoke(SampleMessage $message)
    {
        // magically invoked when an instance of SampleMessage is dispatched
        print_r('Handler handled the message!');
    }
}

Now to see if everything is working so far, I added a simple endpoint to dispatch a message:

final class SampleController extends AbstractController
{
    #[Route('/sample', name: 'sample')]
    public function sample(MessageBusInterface $bus): Response
    {
        $message = new SampleMessage('content');
        $bus->dispatch($message);

        return new Response(sprintf('Message with content %s was published', $message->getContent()));
    }
}

Thankfully when I hit the endpoint, I can see the output from the handler and the http response from the controller:

curl http://localhost:8000/sample

Handler handled the message!Message with content content was published

Where is RabbitMQ?

Install all dependencies

Docker image

I'll use docker to spawn a RabbitMQ instance, because it's much easier. Just install Docker Compose and then edit the .docker-compose.yml file on the project root directory to add a new service:

version: '3'

services:
  rabbitmq:
    image: rabbitmq:3.9-management
    ports:
      - '5672:5672'
      - '15672:15672'

By running docker-compose up on project root directory, I can see everything is working.

PECL AMQP Extension

The AMQP (Advanced Message Queuing Protocol, the protocol which RabbitMQ uses) extension is needed to be installed using PECL (PHP Extension Community Language). This was a bit tricky, at least on MacOS:

First, installed HomeBrew:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Then installed rabbitmq-c:

brew install rabbitmq-c

Which enabled the installation of amqp extension:

pecl install amqp

When prompted to enter the path to librabbitmq, you need to check which version is installed inside the folder /usr/local/Cellar/rabbitmq-c/. Mine was 0.11.0:

Set the path to librabbitmq install prefix [autodetect] : /usr/local/Cellar/rabbitmq-c/0.11.0

Finally, the last dependency:

composer require symfony/amqp-messenger

What a relief! Now I can go back to proper coding.

Using the asynchronous power

By default the username and password created in the docker image are guest, which is coincidentally the exact line I need to uncomment on .env file to expose the RabbitMQ connection as an environment variable:

###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=doctrine://default
 MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
###< symfony/messenger ###

Now with this new known value, I need to tell the application which messages should be handled by this new transport.

Then on file config/packages/messanger.yaml, I defined a new transport and the message type that will use it:

framework:
    messenger:
        transports:
            # https://symfony.com/doc/current/messenger.html#transport-configuration
            async: '%env(MESSENGER_TRANSPORT_DSN)%'

        routing:
            # Route your messages to the transports
            'App\Message\SampleMessage': async

Now I can hit the previous endpoint again:

curl http://localhost:8000/sample

And in another terminal I can check the message is still being handled:

php bin/console messenger:consume async -vv

This outputs a verbose log message but the important parts are:

[messenger] Received message App\Message\SampleMessage
[messenger] App\Message\SampleMessage was handled successfully (acknowledging to transport).
[messenger] Message App\Message\SampleMessage handled by App\MessageHandler\SampleMessangeHandler

To be sure the application is really using RabbitMQ, I can access the admin on http://localhost:15672 and see this cool chart:

Wrap up

45