Introduction to RabbitMQ and Symfony
Tags: php, architecture, tutorial, webdev
Published: 2022-01-02 14:15:56

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](https://www.rabbitmq.com/getstarted.html) and then I was eager to try it out in a demo application.

Turns out it wasn't so easy to setup a [Symfony](https://symfony.com/) 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?
![A man confused](https://media.giphy.com/media/l2JeblbdfRL0i2qOI/giphy.gif)
Initially I thought of creating a web application to explore the different patterns of using RabbitMQ. After struggling with the setup of RabbitMQ using a popular web framework, I decided to take a step back and simply create an endpoint that publishes a message, and a consumer that logs the content received. So let's build it!

Final code on Github:
{% embed https://github.com/fabiothiroki/php-symfony-rabbitmq %}

## 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](https://symfony.com/download) and create a a traditional web application:
```bash
symfony new --full php-symfony-rabbitmq
```

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

## Symfony messenger
![A mailman showing a letter](https://media.giphy.com/media/APwmwKVumP56UsAkLd/giphy.gif)
[Symfony Messenger](https://symfony.com/doc/current/messenger.html) is the message bus abstraction provided by a separate lib. Let's install it then!

```bash
composer require symfony/messenger
```

Following the good documentation, I created a simple class to encapsulate the message to be published:
```php
final class SampleMessage
{
    public function __construct(private string $content)
    {
    }

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

And its respective handler:
```php
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:

```php
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:
```bash
curl http://localhost:8000/sample

Handler handled the message!Message with content content was published
```

## Where is RabbitMQ?
![Gif of a Rabbit](https://media.giphy.com/media/w3TwZiezAY1TG/giphy.gif)
Yeah so far there's not even a trace of RabbitMQ, but don't worry because I've prepared the terrain.

## Install all dependencies

### Docker image
I'll use docker to spawn a RabbitMQ instance, because it's much easier. Just install [Docker Compose](https://docs.docker.com/compose/install/) and then edit the `.docker-compose.yml` file on the project root directory to add a new service:
```yaml
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](https://brew.sh/):
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```

Then installed `rabbitmq-c`:
```bash
brew install rabbitmq-c
```

Which enabled the installation of amqp extension:
```bash
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`:
```bash
Set the path to librabbitmq install prefix [autodetect] : /usr/local/Cellar/rabbitmq-c/0.11.0
```

Finally, the last dependency:
```bash
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:
```bash
###> 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:
```yaml
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:
```bash
curl http://localhost:8000/sample
```

And in another terminal I can check the message is still being handled:
```bash
php bin/console messenger:consume async -vv
```

This outputs a verbose log message but the important parts are:
```bash
[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:

![Chart showing the message rate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwbszsz53h8wsh7tjdy0.png)

## Wrap up
![Gif of someone happy](https://media.giphy.com/media/xT5LMK5WOVhGdMMAzm/giphy.gif)
Finally I have a basic setup of RabbitMQ and Symfony! Now I can give life to a bunch of side projects and explore message queueing patterns. Hope you enjoyed and learned something in the way!