Introdução ao RabbitMQ e Symfony
Tags: braziliandevs, php, webdev, architecture
Published: 2022-01-11 17:26:09

Um dia eu estava tentando aprender os conceitos avançados do RabbitMQ, os casos de uso e as diferenças para os outros _message brokers_. Comecei lendo a [boa documentação](https://www.rabbitmq.com/getstarted.html) e então eu estava ansioso para experimentar em uma aplicação real.

Entretanto não foi tão simples de criar uma aplicação [Symfony](https://symfony.com/) e conectar com o RabbitMQ. Os resultados da busca do Google não eram tão precisos e eu ainda precisei da ajuda do StackOverflow para instalar dependências adicionais.

Espero que eu tenha conseguido condensar toda a informação aqui e mostrá-la de uma forma simples e divertida.

## O que eu vou construir?
![Um homem confuso](https://media.giphy.com/media/l2JeblbdfRL0i2qOI/giphy.gif)
Inicialmente eu pensei em criar uma aplicação web para explorar os diferentes padrões do RabbitMQ. Depois de sofrer com a integração do RabbitMQ com framework web popular, eu decidi dar um passo para trás e simplesmente só criar um endpoint que publica uma mensagem, e um consumidor que loga o conteúdo recebido. Então vamos construir!

Código final no github:
{% embed https://github.com/fabiothiroki/php-symfony-rabbitmq %}

## Symfony
Mas por que Symfony? É um framework PHP popular e no final eu gostei de como ficou a integração e a arquitetura com o RabbitMQ.

Primeiramente eu instalei o [Symfony CLI](https://symfony.com/download) e através dele, criei uma aplicação web tradicional:
```bash
symfony new --full php-symfony-rabbitmq
```

Posso rodar minha aplicação através do seguinte comando no diretório do projeto:
```bash
symfony serve
```

## Symfony messenger
![Um carteiro mostrando uma carta](https://media.giphy.com/media/APwmwKVumP56UsAkLd/giphy.gif)
[Symfony Messenger](https://symfony.com/doc/current/messenger.html) é a abstração de mensageria provido por uma dependência separada. Vamos instalá-la!

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

Seguindo a boa documentação, eu criei uma classe simples para encapsular a mensagem a ser publicada:
```php
final class SampleMessage
{
    public function __construct(private string $content)
    {
    }

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

E a sua respectiva classe _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!');
    }
}
```

Agora para verificar que tudo está funcionando até agora, eu adicionei um _endpoint_ simples para enviar uma mensagem:
```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()));
    }
}
```

Ao testar o _endpoint_, verifico que a saída impressa pelo _handler_ e pela resposta http está correta:
```bash
curl http://localhost:8000/sample

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

## Onde está o RabbitMQ?
![Gif de um coelho](https://media.giphy.com/media/w3TwZiezAY1TG/giphy.gif)
Até agora não temos nem um sinal do RabbitMQ, mas não se preocupe porque deixei todo o terreno preparado para introduzi-lo.

## Instalação das dependências

### Imagem docker
Vou utilizar o docker para subir uma instância do RabbitMQ porque é muito fácil dessa forma. Só é preciso instalar o [Docker Compose](https://docs.docker.com/compose/install/) e então editar o arquivo `.docker-compose.yml` no diretório do projeto PHP para adicionar um novo serviço: 
```yaml
version: '3'

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

Ao rodar `docker-compose up` é possível verificar que a instância do RabbitMQ está rodando.

### Extensão PECL AMQP
AMQP (Advanced Message Queuing Protocol), é o protocolo que sustenta o RabbitMQ. A instalação da sua extensão utilizando PECL (PHP Extension Community Language) foi um pouco difícil, pelo menos no MacOS:

Primeiro, instalei o [HomeBrew](https://brew.sh/):
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```

Depois instalei `rabbitmq-c`:
```bash
brew install rabbitmq-c
```

O que por fim permitiu a instalação da extensão:
```bash
pecl install amqp
```

Quando foi pedido para digitar o caminho para `librabbitmq`, você precisa verificar qual é a versão instalada dentro do diretório `/usr/local/Cellar/rabbitmq-c/`. A minha versão era a `0.11.0`:

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

Finalmente a última dependência:
```bash
composer require symfony/amqp-messenger 
```

Que alívio! Agora posso voltar a programar.

## Utilizando o poder da assincronicidade

Por padrão, o usuário e a senha criados na imagem docker são `guest`, o que coincidentemente é a linha exata que eu preciso descomentar no arquivo `.env`, que expõe a conexão ao RabbitMQ como uma variável de ambiente:
```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 ###
```

Agora com essa nova variável, eu preciso dizer a aplicação quais mensagens deveriam ser tratadas por esse transporte.

Então no arquivo `config/packages/messanger.yaml` eu defino um novo `transport` e o tipo de mensagem a ser tratada:
```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
```

Nesse ponto eu posso testar o endpoint novamente:
```bash
curl http://localhost:8000/sample
```

E em outro terminal eu ligo o consumidor:
```bash
php bin/console messenger:consume async -vv
```

Esse comando imprime várias mensagens bem verbosas, mas as partes importantes são:
:
```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
```

E para ter certeza que a aplicação está realmente usando o RabbitMQ, eu posso acessar o admin no endereço http://localhost:15672 e verificar esse gráfico bonito:
![Gráfico mostrando as mensagens enviadas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwbszsz53h8wsh7tjdy0.png)

## Conclusão
![Gif de alguém feliz](https://media.giphy.com/media/xT5LMK5WOVhGdMMAzm/giphy.gif)
Finalmente eu tenho um _setup_ básico do RabbitMQ e Symfony! Agora eu posso dar vida a vários _side projects_ e explorar os padrões de mensageria. Espero que tenha gostado e também aprendido algo!