Introdução ao RabbitMQ e Symfony

Fabio Hiroki - Jan 11 '22 - - Dev Community

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 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 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
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:


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 e através dele, criei uma aplicação web tradicional:

symfony new --full php-symfony-rabbitmq
Enter fullscreen mode Exit fullscreen mode

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

symfony serve
Enter fullscreen mode Exit fullscreen mode

Symfony messenger

Um carteiro mostrando uma carta
Symfony Messenger é a abstração de mensageria provido por uma dependência separada. Vamos instalá-la!

composer require symfony/messenger
Enter fullscreen mode Exit fullscreen mode

Seguindo a boa documentação, eu criei uma classe simples para encapsular a mensagem a ser publicada:

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

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

E a sua respectiva classe 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!');
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora para verificar que tudo está funcionando até agora, eu adicionei um endpoint simples para enviar uma mensagem:

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()));
    }
}
Enter fullscreen mode Exit fullscreen mode

Ao testar o endpoint, verifico que a saída impressa pelo handler e pela resposta http está correta:

curl http://localhost:8000/sample

Handler handled the message!Message with content content was published
Enter fullscreen mode Exit fullscreen mode

Onde está o RabbitMQ?

Gif de um coelho
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 e então editar o arquivo .docker-compose.yml no diretório do projeto PHP para adicionar um novo serviço:

version: '3'

services:
  rabbitmq:
    image: rabbitmq:3.9-management
    ports:
      - '5672:5672'
      - '15672:15672'
Enter fullscreen mode Exit fullscreen mode

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:

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

Depois instalei rabbitmq-c:

brew install rabbitmq-c
Enter fullscreen mode Exit fullscreen mode

O que por fim permitiu a instalação da extensão:

pecl install amqp
Enter fullscreen mode Exit fullscreen mode

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:

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

Finalmente a última dependência:

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

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:

###> 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 ###
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

Nesse ponto eu posso testar o endpoint novamente:

curl http://localhost:8000/sample
Enter fullscreen mode Exit fullscreen mode

E em outro terminal eu ligo o consumidor:

php bin/console messenger:consume async -vv
Enter fullscreen mode Exit fullscreen mode

Esse comando imprime várias mensagens bem verbosas, mas as partes importantes são:
:

[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
Enter fullscreen mode Exit fullscreen mode

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

Conclusão

Gif de alguém feliz
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!

. . . . . . . . . . . . . . . . . . . . .