PHP 8.2: Wat is er nieuw?
De nieuwe versie van PHP staat voor de deur. Wat zijn de nieuwe implementaties waar je vanaf moet weten? Lees dit artikel om erachter te komen!
Read the first part of our PHP series devoted to microservices communication in Symfony framework and the most popular way – AMQP communication using RabbitMQ.
Modern application architecture has forced developers to change the way of thinking about the communication between different components of IT systems. Once the matter was simpler – most systems were created as monolithic structures connected with each other by a network of business logic connections. Maintaining such dependencies in a PHP project was a huge challenge for PHP ontwikkelaars, and the growing popularity of SaaS solutions and the huge increase in the popularity of cloud services caused that today we hear more and more about microservices and application modularity.
Just how, by creating independent microservices, can we make them exchange information with each other?
This article is the first in a series of posts on microservices communication in Symfony framework and it covers the most popular way – AMQP communication using RabbitMQ.
To create two independent applications and achieve communication between them using only Message Bus.
We have two, imaginary, independent applications:
* app1
: which sends E-Mail and SMS notifications to employees
* app2
: which allows you to manage employee’s work and assign them tasks.
We want to create a modern and simple system whereby the assignment of work to an employee in app2
will send a notification to the customer using app1
. Despite appearances, this is very simple!
For the purpose of this article, we will use the latest Symfony(version 6.1 at the time of writing) and the latest version of PHP (8.1). In a few very simple steps we will create a working local Docker environment with two microservices. All you need is:
* a working computer,
* installed Docker + Docker Compose environment
* and a locally configured Symfony CLI and some free time.
We will use Docker’s capabilities as an application virtualization and containerization tool. Let’s start by creating a directory tree, a framework for two Symfony applications, and describe the infrastructure of our environments using the docker-compose.yml
bestand.
cd ~
mkdir microservices-in-symfony
cd microservices-in-symfony
symfony new app1
symfony new app2
touch docker-compose.yml
We have created two directories for two separate Symfony applications and created an empty docker-compose.yml
file to launch our environment.
Let’s add the following sections to the docker-compose.yml
file:
version: '3.8'
services:
app1:
containername: app1
build: app1/.
restart: on-failure
envfile: app1/.env
environment:
APPNAME: app1
tty: true
stdinopen: true
app2:
containername: app2
build: app2/.
restart: on-failure
envfile: app2/.env
environment:
APPNAME: app2
tty: true
stdinopen: true
rabbitmq:
containername: rabbitmq
image: rabbitmq:management
ports:
- 15672:15672
- 5672:5672
environment:
- RABBITMQDEFAULTUSER=user
- RABBITMQDEFAULT_PASS=password
Bron code available directly: thecodest-co/microservices-in-symfony/blob/main/docker-compose.yml
But wait, what happened here? For those unfamiliar with Docker, the above configuration file may seem enigmatic, however its purpose is very simple. Using Docker Compose we are building three “services”:
For proper operation, we still need Dockerfile
files which are the source to build the images. So let’s create them:
touch app1/Dockerfile
touch app2/Dockerfile
Both files have exactly the same structure and look as follows:
FROM php:8.1
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
COPY . /app/
WORKDIR /app/
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions && sync &&
install-php-extensions amqp
RUN apt-get update
&& apt-get install -y libzip-dev wget --no-install-recommends
&& apt-get clean
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN docker-php-ext-install zip;
CMD bash -c "cd /app && composer install && php -a"
Source code available directly: /thecodest-co/microservices-in-symfony/blob/main/app1/Dockerfile
The above file is used by Docker Compose to build a container from a PHP 8.1 image with Composer and the AMQP extension installed. Additionally, it runs the PHP intepreter in append mode to keep the container running in the background.
Your directory and file tree should now look as follows:
.
├── app1
│ └── Dockerfile
| (...) # Symfony App Structure
├── app2
│ └── Dockerfile
| (...) # Symfony App Structure
└── docker-compose.yml
Let’s start with the app1
directory and the first application.
In our example, it is an application that listens and consumes messages from the queue sent by app2
as described in the requirements:
assigning a job to a worker in
app2
will send a notification to the client
Let’s start by adding the required libraries. AMQP is natively supported for the symfony/messenger
extension. We will additionally install monolog/monolog
to keep track of system logs for easier application behavior analysis.
cd app1/
symfony composer req amqp ampq-messenger monolog
After the installation, an additional file was added under config/packages/messenger.yaml
. It is a configuration file for the Symfony Messenger component and we don’t need its full configuration.
Replace it with the YAML file below:
framework:
messenger:
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
# failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
external_messages:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
auto_setup: false
exchange:
name: messages
type: direct
default_publish_routing_key: from_external
queues:
messages:
binding_keys: [from_external]
Source code available directly: thecodest-co/microservices-in-symfony/blob/main/app1/config/packages/messenger.yaml
Symfony Messenger is used for synchronous and asynchronous communication in Symfony applications. It supports a variety of transports, or sources of truth of the transport layer. In our example, we use the AMQP extension that supports the RabbitMQ event queue system.
The above configuration defines a new transport named external_messages
, which references the MESSENGER_TRANSPORT_DSN
environment variable and defines direct listening on the messages
channel in Message Bus. At this point, also edit the app1/.env
file and add the appropriate transport address.
“`env
(…)
MESSENGERTRANSPORTDSN=amqp://user:password@rabbitmq:5672/%2f/messages
(…)
“
After preparing the application framework and configuring the libraries, it is time to implement the business logic. We know that our application must respond to the assignment of a job to a worker. We also know that assigning a job in theapp2system changes the status of the job. So let's create a model that mimics the status change and save it in theapp1/Message/StatusUpdate.php` path:
{
public function __construct(protected string $status){}
public function getStatus(): string
{
return $this->status;
}
}
Source code available directly: /thecodest-co/microservices-in-symfony/blob/main/app1/src/Message/StatusUpdate.php
We still need a class that will implement the business logic when our microservice receives the above event from the queue. So let’s create a Message Handler in de app1/Handler/StatusUpdateHandler.php
path:
use PsrLogLoggerInterface;
use SymfonyComponentMessengerAttributeAsMessageHandler;
[AsMessageHandler]
class StatusUpdateHandler
{
public function __construct(
protected LoggerInterface $logger,
) {}
public function __invoke(StatusUpdate $statusUpdate): void
{
$statusDescription = $statusUpdate->getStatus();
$this->logger->warning('APP1: {STATUS_UPDATE} - '.$statusDescription);
// the rest of business logic, i.e. sending email to user
// $this->emailService->email()
}
}
Source code available directly: /thecodest-co/microservices-in-symfony/blob/main/app1/src/Handler/StatusUpdateHandler.php
PHP attributes make things much easier and mean that in this particular case we don’t have to worry about autowiring or service declaration. Our microservice for handling domain events is ready, it’s time to get down to the second application.
We will take a look at the app2
directory and the second Symfony application. Our idea is to send a message to the queue when a worker is assigned a task in the system. So let’s do a quick configuration of AMQP and get our second microservice to start publishing StatusUpdate
events to the Message Bus.
Installing the libraries is exactly the same as for the first application.
cd ..
cd app2/
symfony composer req amqp ampq-messenger monolog
Let’s make sure that the app2/.env
file contains a valid DSN entry for RabbitMQ:
(...)
MESSENGER_TRANSPORT_DSN=amqp://user:password@rabbitmq:5672/%2f/messages
(...)
All that remains is to configure Symfony Messenger in the app2/config/packages/messenger.yaml
file:
framework:
messenger:
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
# failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
# Route your messages to the transports
'AppMessageStatusUpdate': async
As you can see, this time the transport definition points directly to async
and defines routing in the form of sending our StatusUpdate
message to the configured DSN. This is the only area of configuration, all that is left is to create the logic and implementation layer of the AMQP queue. For this we will create the twin StatusUpdateHandler
en StatusUpdate
classes in app2
.
use PsrLogLoggerInterface;
use SymfonyComponentMessengerAttributeAsMessageHandler;
[AsMessageHandler]
class StatusUpdateHandler
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function __invoke(StatusUpdate $statusUpdate): void
{
$statusDescription = $statusUpdate->getStatus();
$this->logger->warning('APP2: {STATUS_UPDATE} - '.$statusDescription);
## business logic, i.e. sending internal notification or queueing some other systems
}
}
{
public function __construct(protected string $status){}
public function getStatus(): string
{
return $this->status;
}
}
Source code available directly: /thecodest-co/microservices-in-symfony/blob/main/app2/src/Message/StatusUpdate.php
Finally, all that has to be done is to create a way to send a message to the Message Bus. We will create a simple Symfony Command for this:
use SymfonyComponentConsoleAttributeAsCommand;
use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;
use SymfonyComponentMessengerMessageBusInterface;
[AsCommand(
name: "app:send"
)]
class SendStatusCommand extends Command
{
public function construct(private readonly MessageBusInterface $messageBus, string $name = null)
{
parent::construct($name);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$status = "Worker X assigned to Y";
$this->messageBus->dispatch(
message: new StatusUpdate($status)
);
return Command::SUCCESS;
}
}
Met dank aan Dependency Injection we can use an instance of MessageBusInterface
in our Command and send a StatusUpdate
message via the dispatch()
method to our queue. Additionally, we also use PHP Attributes here.
That’s it – all that’s left is to run our Docker Compose environment and see how our applications behave.
With Docker Compose the containers with our two applications will be built and run as separate instances, the only middleware layer will be the rabbitmq
container and our Message Bus implementation.
From the root directory of the project, let’s run the following commands:
cd ../ # make sure you're in main directory
docker-compose up --build -d
This command can take some time, as it builts two separate containers with PHP 8.1 + AMQP and pulls RabbitMQ image. Be patient. After images are built you can fire our Command from app2
and send some messages on a queue.
docker exec -it app2 php bin/console app:send
You can do it as many times as you can. As long as there’s no consumer your messages will not be processed. As soon as you fire up the app1
and consume all the messages they’ll show on your screen.
docker exec -it app1 php bin/console messenger:consume -vv external_messages
The complete source code along with the README can be found in our public repository The Codest Github
Symfony with its libraries and tools allows for a fast and efficient approach to developing modern webtoepassingen. With a few commands and a few lines of code we are able to create a modern communication system between applications. Symfony, like PHP, is ideal for developing web applications and thanks to its ecosystem and ease of implementation this ecosystem achieves some of the best time-to-market indicators.
However, fast does not always mean good – in the example above we presented the simplest and fastest way of communication. What the more inquisitive will certainly notice that there is a lack of disconnection of domain events outside the application layer – in the current version they are duplicated, and there is no full support for Envelope
, among others there is no Stamps
. For those and others, I invite you to read Part II, where we’ll cover the topic of unifying the domain structure of Symfony applications in a microservices environment, and discuss the second popular microservices communication method – this time synchronous, based on the REST API.