# Microservices and Messaging with PHP, NodeJS, RabbitMQ and Docker

- Author: Luiz Schons
- Published: 2024-06-29
- URL: https://luizschons.com/posts/microservices-and-messaging-with-php-nodejs-rabbitmq-and-docker/
- Português: https://luizschons.com/pt-br/posts/microservices-e-mensageria-com-php-nodejs-rabbitmq-e-docker.md
- Topics: Microservices, Messaging, PHP, Docker

Microservices development is an architectural approach that structures an application as a set of small, independent services, each running in its own process and communicating through lightweight protocols such as HTTP, WebSockets or AMQP.

In this article, we will build a microservices application with PHP, RabbitMQ and Docker. RabbitMQ is messaging software that implements AMQP (Advanced Message Queuing Protocol), an asynchronous messaging protocol.

### What is RabbitMQ?

Imagine the following scenario: you have a system that needs to send emails to users. Instead of sending the emails directly, you can send a message to a message queue, which will be consumed by a service that sends the emails. That is what RabbitMQ does: it receives messages from producers and delivers them to consumers.

This decouples producing messages from consuming them, which lets you scale each part of the system independently. RabbitMQ also makes sure messages are delivered in the right order and are not lost.

### Application architecture

Our application will have two microservices: a producer and a consumer. The producer sends messages to a message queue, and the consumer reads the messages and prints them to the terminal.

Our first step is to create the `docker-compose.yml` file to define our application's services:

```yaml
services:
  rabbitmq:
    image: "rabbitmq:3-management"
    container_name: "rabbitmq"
    ports:
      - "15672:15672"
      - "5672:5672"

  php-app:
    build: ./php-app
    depends_on:
      - rabbitmq

  node-app:
    build: ./node-app
    depends_on:
      - rabbitmq
```

In this file, we define three services: `rabbitmq`, `php-app` and `node-app`. The `rabbitmq` service is based on the `rabbitmq:3-management` image, which includes the RabbitMQ management UI. The `php-app` and `node-app` services are based on custom images we are going to build.

### Building the producer with PHP

Now let's create the `php-app` directory and a `Dockerfile` inside it:

```bash
mkdir php-app && touch php-app/Dockerfile
```

In the `Dockerfile`, we define the base image and copy the application files:

```dockerfile
FROM php:7.4-cli

RUN apt-get update && apt-get install -y librabbitmq-dev libssl-dev git unzip wget curl

RUN docker-php-ext-install sockets pdo pdo_mysql
RUN pecl install amqp && docker-php-ext-enable amqp

COPY . /usr/src/myapp
WORKDIR /usr/src/myapp

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

RUN composer clear-cache

RUN composer install

CMD ["php", "./publisher.php"]
```

In this file, we use the `php:7.4-cli` base image and install the dependencies needed for RabbitMQ and Composer. Then we copy the application files into `/usr/src/myapp`, install the Composer dependencies and run the `publisher.php` script.

Now let's create the `publisher.php` file at the project root:

```php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$maxRetries = 5;
$retryDelay = 5;

for ($attempt = 0; $attempt try {
        $connection = new AMQPStreamConnection('rabbitmq', 5672, 'guest', 'guest');
        $channel = $connection->channel();
        break;
    } catch (Exception $e) {
        echo "Failed to connect to RabbitMQ. Retrying in $retryDelay seconds...\n";
        sleep($retryDelay);
    }    
}

if (!isset($connection)) {
    echo "Not possible to connect to RabbitMQ. Exiting...\n The application will be restarted by Docker\n";
    exit(1);
}

$channel->queue_declare('hello', false, false, false, false);

$msg = new AMQPMessage('Hello, RabbitMQ! Now is ' . date('Y-m-d H:i:s'));
$channel->basic_publish($msg, '', 'hello');

echo " [x] Sent 'Hello, RabbitMQ!'\n";

$channel->close();
$connection->close();
```

In this file, we open a connection to RabbitMQ, declare a queue called `hello` and send a message to it. Then we close the connection. Notice that we try to connect to RabbitMQ several times, waiting 5 seconds between attempts.

This matters to make sure the application can connect to RabbitMQ even if it is not available right away. It is a common practice in distributed applications, where service availability can vary over time; look up the `circuit breaker` and `retry pattern` patterns to learn more.

To install the RabbitMQ dependencies with Composer, we need to create a `composer.json` file at the project root:

```json
{ "require": { "php-amqplib/php-amqplib": "^3.1", "phpseclib/phpseclib": "^3.0" } }
```

## Building the consumer with Node.js

Now let's create the `node-app` directory and a `Dockerfile` inside it:

```bash
mkdir node-app && touch node-app/Dockerfile
```

In the `Dockerfile`, we define the base image and copy the application files:

```dockerfile
FROM node:14 
WORKDIR /usr/src/app 
COPY package\*.json ./ 
RUN npm install 
COPY . . 
CMD \["node", "subscriber.js"\]
```

In this file, we use the `node:14` base image, copy the application files into `/usr/src/app`, install the Node.js dependencies and run the `subscriber.js` script.

Now let's create the `subscriber.js` file at the project root:

```javascript
const amqp = require('amqplib');

async function connectWithRetry() {
    const maxRetries = 5;
    const retryDelay = 5000;

    for (let attempt = 1; attempt try {
            const connection = await amqp.connect('amqp://rabbitmq');
            return connection;
        } catch (err) {
            console.error(`Not possible to connect to RabbitMQ. Retrying in ${retryDelay}ms. Attempt ${attempt} of ${maxRetries}`);
            await new Promise(resolve => setTimeout(resolve, retryDelay));
        }
    }

    throw new Error(`Failed to connect to RabbitMQ after ${maxRetries} attempts`);
}

async function receiveMessages() {
    try {
        const connection = await connectWithRetry();
        const channel = await connection.createChannel();
        const queue = 'hello';

        await channel.assertQueue(queue, { durable: false });

        console.log(" [*] Waiting for messages in %s.", queue);

        channel.consume(queue, function (msg) {
            console.log(" [x] Received: %s", msg.content.toString());
        }, {
            noAck: true
        });

    } catch (err) {
        console.error(err.message);
        process.exit(1);
    }
}

receiveMessages();
```

In this file, we open a connection to RabbitMQ, declare a queue called `hello` and consume the messages from it.

Then we print the messages to the terminal. Just like in the producer, we try to connect to RabbitMQ several times, waiting 5 seconds between attempts. This makes sure the consumer can connect to RabbitMQ even if it is not available right away.

And to install the RabbitMQ dependencies, we need to create a `package.json` file at the project root:

```json
{ "name": "node-app", "version": "1.0.0", "description": "", "main": "subscriber.js", "dependencies": { "amqplib": "^0.8.0" }, "author": "", "license": "ISC" }
```

## Running the application

Now that we have the RabbitMQ, producer and consumer services, let's run the application with Docker Compose:

```bash
docker compose up --build
```

This will create the service containers and run the application. You will see the messages sent by the producer printed to the terminal by the consumer.

RabbitMQ will also be available on port 15672, where you can open the management UI and see the queues and messages.

With that, we have built a microservices application with PHP, RabbitMQ and Docker. This architectural approach lets you scale each part of the system independently and guarantees that messages are delivered in the right order.

I hope this article was useful and that you can apply these concepts in your projects. If you have any questions or suggestions, leave them in the comments.

Feel free to reach out with any questions.

Repository: [Microservices and Messaging with PHP, RabbitMQ and Docker](https://github.com/sschonss/ms-rabbitmq)
