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?
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:
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
Symfony Messenger is the message bus abstraction provided by a separate lib. Let's install it then!
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?
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 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
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!