Nowoczesna architektura aplikacji wymusiła na deweloperach zmianę sposobu myślenia o komunikacji pomiędzy poszczególnymi komponentami systemów informatycznych. Kiedyś sprawa była prostsza - większość systemów tworzona była jako monolityczne struktury połączone ze sobą siecią powiązań logiki biznesowej. Utrzymanie takich zależności w PHP projekt było ogromnym wyzwaniem dla Programiści PHPa także rosnąca popularność rozwiązań SaaS i ogromny wzrost popularności chmura Usługi spowodowały, że dziś coraz częściej słyszymy o mikrousługach i modułowości aplikacji.
W jaki sposób, tworząc niezależne mikrousługi, możemy sprawić, by wymieniały one między sobą informacje?
Ten artykuł jest pierwszym z serii postów na temat Komunikacja mikrousług w Symfony i obejmuje najpopularniejszy sposób - komunikację AMQP przy użyciu RabbitMQ.
Cel
Aby utworzyć dwie niezależne aplikacje i osiągnąć komunikację między nimi przy użyciu tylko magistrali komunikatów.
Koncepcja
Mamy dwie wyimaginowane, niezależne aplikacje:
* app1: który wysyła powiadomienia e-mail i SMS do pracowników
* app2który pozwala zarządzać pracą pracowników i przydzielać im zadania.
Chcemy stworzyć nowoczesny i prosty system, dzięki któremu przydzielanie pracy pracownikowi w app2 wyśle powiadomienie do klienta przy użyciu app1. Wbrew pozorom jest to bardzo proste!
Przygotowanie
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:
* działający komputer,
* zainstalowany Docker + Środowisko Docker Compose
* i lokalnie skonfigurowany Symfony CLI i trochę wolnego czasu.
Środowisko uruchomieniowe
Wykorzystamy możliwości Dockera jako narzędzia do wirtualizacji i konteneryzacji aplikacji. Zacznijmy od utworzenia drzewa katalogów, szkieletu dla dwóch Aplikacje Symfonyi opisać infrastrukturę naszych środowisk przy użyciu docker-compose.yml plik.
cd ~
mkdir microservices-in-symfony
cd microservices-in-symfony
symfony new app1
symfony new app2
touch docker-compose.yml
Stworzyliśmy dwa katalogi dla dwóch oddzielnych aplikacji Symfony i utworzyliśmy pusty katalog docker-compose.yml aby uruchomić nasze środowisko.
Dodajmy następujące sekcje do docker-compose.yml file:
version: '3.8'
usługi:
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=hasło
Źródło kod dostępne bezpośrednio: thecodest-co/microservices-in-symfony/blob/main/docker-compose.yml
Ale zaraz, co tu się stało? Dla osób niezaznajomionych z Dockerem powyższy plik konfiguracyjny może wydawać się enigmatyczny, jednak jego cel jest bardzo prosty. Korzystając z Docker Compose budujemy trzy "usługi":
- app1: który jest kontenerem dla pierwszej aplikacji Symfony
- app2: który jest kontenerem dla drugiej aplikacji Symfony
- rabbitmq: obraz aplikacji RabbitMQ jako warstwa pośrednicząca komunikacji
Do prawidłowego działania nadal potrzebujemy Plik Docker które są źródłem do tworzenia obrazów. Więc stwórzmy je:
touch app1/Dockerfile
touch app2/Dockerfile
Oba pliki mają dokładnie taką samą strukturę i wyglądają następująco:
Z 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"
Kod źródłowy dostępny bezpośrednio: /thecodest-co/microservices-in-symfony/blob/main/app1/Dockerfile
Powyższy plik jest używany przez Docker Compose do zbudowania kontenera z obrazu PHP 8.1 z zainstalowanym Composerem i rozszerzeniem AMQP. Dodatkowo uruchamia intepreter PHP w trybie append, aby kontener działał w tle.
Drzewo katalogów i plików powinno teraz wyglądać następująco:
.
├── app1
│ └── Dockerfile
| (...) # Struktura aplikacji Symfony
├── app2
│ └── Dockerfile
| (...) # Struktura aplikacji Symfony
└── docker-compose.yml
Pierwszy mikroserwis Symfony
Zacznijmy od app1 i pierwszej aplikacji.
W naszym przykładzie jest to aplikacja, która nasłuchuje i pobiera wiadomości z kolejki wysyłanej przez app2 zgodnie z opisem w wymaganiach:
przypisanie zadania do pracownika w
app2wyśle powiadomienie do klienta
Zacznijmy od dodania wymaganych bibliotek. AMQP jest natywnie obsługiwany dla symfony/messenger rozszerzenie. Dodatkowo zainstalujemy monolog/monolog do śledzenia dzienników systemowych w celu łatwiejszej analizy zachowania aplikacji.
cd app1/
symfony composer req amqp ampq-messenger monolog
Po instalacji dodano dodatkowy plik w sekcji config/packages/messenger.yaml. Jest to plik konfiguracyjny dla komponentu Symfony Messenger i nie potrzebujemy go pełna konfiguracja.
Zastąp go poniższym plikiem YAML:
ramy:
messenger:
# Usuń ten komentarz (i nieudany transport poniżej), aby wysyłać nieudane wiadomości do tego transportu w celu późniejszej obsługi.
# 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
typ: bezpośredni
default_publish_routing_key: from_external
queues:
messages:
binding_keys: [from_external]
Kod źródłowy dostępny bezpośrednio: thecodest-co/microservices-in-symfony/blob/main/app1/config/packages/messenger.yaml
Symfony Messenger służy do synchronicznej i asynchronicznej komunikacji w aplikacjach Symfony. Obsługuje wiele różnych transportyczyli źródła prawdy warstwy transportowej. W naszym przykładzie używamy rozszerzenia AMQP, które obsługuje system kolejek zdarzeń RabbitMQ.
Powyższa konfiguracja definiuje nowy transport o nazwie external_messagesktóry odwołuje się do MESSENGER_TRANSPORT_DSN i definiuje bezpośrednie nasłuchiwanie na wiadomości w Message Bus. W tym momencie należy również edytować app1/.env i dodać odpowiedni adres transportowy.
“`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;
}
}
Kod źródłowy dostępny bezpośrednio: /thecodest-co/microservices-in-symfony/blob/main/app1/src/Message/StatusUpdate.php
Nadal potrzebujemy klasy, która zaimplementuje logikę biznesową, gdy nasza mikrousługa otrzyma powyższe zdarzenie z kolejki. Stwórzmy więc klasę Obsługa komunikatów w app1/Handler/StatusUpdateHandler.php ścieżka:

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);
// reszta logiki biznesowej, tj. wysyłanie wiadomości e-mail do użytkownika
// $this->emailService->email()
}
}
Kod źródłowy dostępny bezpośrednio: /thecodest-co/microservices-in-symfony/blob/main/app1/src/Handler/StatusUpdateHandler.php
PHP znacznie ułatwiają sprawę i oznaczają, że w tym konkretnym przypadku nie musimy martwić się o autowiring czy deklarację usługi. Nasza mikrousługa do obsługi zdarzeń domenowych jest gotowa, czas zabrać się za drugą aplikację.
Drugi mikroserwis Symfony
Przyjrzymy się app2 i drugi katalog Aplikacja Symfony. Naszą ideą jest wysyłanie wiadomości do kolejki, gdy pracownik otrzyma zadanie w systemie. Wykonajmy więc szybką konfigurację AMQP i uruchommy naszą drugą mikrousługę, aby rozpocząć publikację StatusUpdate zdarzeń do magistrali komunikatów.
Instalacja bibliotek przebiega dokładnie tak samo, jak w przypadku pierwszej aplikacji.
cd ..
cd app2/
symfony composer req amqp ampq-messenger monolog
Upewnijmy się, że app2/.env zawiera prawidłowy wpis DSN dla RabbitMQ:
(...)
MESSENGER_TRANSPORT_DSN=amqp://user:password@rabbitmq:5672/%2f/messages
(...)
Pozostaje tylko skonfigurować Symfony Messenger w sekcji app2/config/packages/messenger.yaml file:
ramy:
messenger:
# Usuń ten komentarz (i nieudany transport poniżej), aby wysyłać nieudane wiadomości do tego transportu w celu późniejszej obsługi.
# failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
# Przekieruj wiadomości do transportów
'AppMessageStatusUpdate': async
Jak widać, tym razem definicja transportu wskazuje bezpośrednio na asynchroniczny i definiuje routing w postaci wysyłania naszych StatusUpdate do skonfigurowanej sieci DSN. Jest to jedyny obszar konfiguracji, pozostaje tylko stworzyć logikę i warstwę implementacji kolejki AMQP. W tym celu utworzymy bliźniaczą StatusUpdateHandler i StatusUpdate zajęcia w 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);
## logika biznesowa, tj. wysyłanie wewnętrznych powiadomień lub kolejkowanie innych systemów
}
}

{
public function __construct(protected string $status){}
public function getStatus(): string
{
return $this->status;
}
}
Kod źródłowy dostępny bezpośrednio: /thecodest-co/microservices-in-symfony/blob/main/app2/src/Message/StatusUpdate.php
Na koniec wszystko, co należy zrobić, to stworzyć sposób wysyłania wiadomości do magistrali komunikatów. Stworzymy prostą aplikację Polecenie Symfony za to:

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 = "Pracownik X przypisany do Y";
$this->messageBus->dispatch(
message: new StatusUpdate($status)
);
return Command::SUCCESS;
}
}
Dzięki Wstrzykiwanie zależności możemy użyć instancji MessageBusInterface w naszym poleceniu i wysłać StatusUpdate wiadomość za pośrednictwem dispatch() do naszej kolejki. Dodatkowo używamy tutaj również atrybutów PHP.
To wszystko - pozostaje tylko uruchomić nasze środowisko Docker Compose i sprawdzić, jak zachowują się nasze aplikacje.
Uruchamianie środowiska i testowanie
Dzięki Docker Compose kontenery z naszymi dwiema aplikacjami zostaną zbudowane i uruchomione jako oddzielne instancje, a jedyną warstwą pośredniczącą będzie rabbitmq i nasza implementacja magistrali komunikatów.
Z katalogu głównego projektu uruchommy następujące polecenia:
cd ../ # upewnij się, że jesteś w katalogu głównym
docker-compose up --build -d
To polecenie może zająć trochę czasu, ponieważ buduje dwa oddzielne kontenery z PHP 8.1 + AMQP i pobiera obraz RabbitMQ. Bądź cierpliwy. Po zbudowaniu obrazów możesz uruchomić naszą komendę z poziomu app2 i wysłać kilka wiadomości do kolejki.
docker exec -it app2 php bin/console app:send
Można to robić dowolną liczbę razy. Tak długo, jak nie ma konsument wiadomości nie będą przetwarzane. Jak tylko uruchomisz aplikację app1 i konsumować wszystkie wiadomości, które będą wyświetlane na ekranie.
docker exec -it app1 php bin/console messenger:consume -vv external_messages

Kompletny kod źródłowy wraz z plikiem README można znaleźć w naszym publicznym repozytorium The Codest Github
Podsumowanie
Symfony ze swoimi bibliotekami i narzędziami pozwala na szybkie i wydajne podejście do tworzenia nowoczesnych aplikacji. aplikacje internetowe. Za pomocą kilku komend i kilku linijek kodu jesteśmy w stanie stworzyć nowoczesny system komunikacji pomiędzy aplikacjami. Symfony, podobnie jak PHPjest idealny dla tworzenie aplikacji internetowych a dzięki swojemu ekosystemowi i łatwości wdrożenia osiąga jedne z najlepszych wskaźników czasu wprowadzenia na rynek.
Jednak szybko nie zawsze znaczy dobrze - w powyższym przykładzie przedstawiliśmy najprostszy i najszybszy sposób komunikacji. Co bardziej dociekliwi z pewnością zauważą, że brakuje odłączenia zdarzeń domenowych poza warstwę aplikacji - w obecnej wersji są one dublowane, nie ma też pełnego wsparcia dla Kopertamiędzy innymi nie ma Znaczki. Tych i innych zapraszam do lektury części II, w której poruszymy temat ujednolicania struktury domenowej aplikacji Symfony w środowisku mikrousług oraz omówimy drugą popularną metodę komunikacji mikrousług - tym razem synchroniczną, opartą o REST API.
