Sytuacja kobiet w IT w 2024 roku
11.01.20215 min
Kim Wüstkamp

Kim WüstkampCreator and Developer of Killer Shell - Kubernetes CKS|CKA|CKAD SimulatorWuestkamp.com

Dlaczego warto używać Symfony Messenger

Dowiedz się, dlaczego komponent Messenger z Symfony jest tak przydatny przy złożonych aplikacjach i sprawdź, jak go właściwie używać.

Dlaczego warto używać Symfony Messenger

Dowiesz się tutaj, jak i dlaczego powinno się używać komponentu z Symfony4 o nazwie Messenger. 

TL;DR

  1. Pomyśl o „Messages” jak o akcjach, które Twoja aplikacja musi wykonać, na przykład, „stwórz nową rezerwację” albo „wyślij użytkownikowi mailowe powiadomienie”. Sprawi to, że w Twojej apce będzie więcej abstrakcji i będzie ją też można łatwiej utrzymywać.
  2. Domyślnie, „Messages” są wykonywane synchronicznie, a więc aplikacja czeka, aż się zakończą. Jeśli niektóre z wiadomości będą zajmowały zbyt dużo czasu, to można je zmienić na asynchroniczne i takie, które są obsługiwane przez workerów. 

Krok 0: proste API do rezerwacji bez komponentu Messenger

Proste żądanie->Kontroler->Odpowiedź


To API może wyświetlać i tworzyć rezerwacje. Jeśli użytkownik utworzy rezerwację, to zostanie on przekierowany do listingu. Nasz BookingController wygląda następująco:

<?php
...
class BookingController extends AbstractController
{
    /**
     * @Route("/bookings", name="booking_list")
     */
    public function index(BookingRepository $bookingRepository)
    {
        $data = [];
        foreach ($bookingRepository->findAll() as $booking) {
            $data[$booking->getId()] = $booking->getName();
        }
        return $this->json($data);
    }
    /**
     * @Route("/bookings/create/{name}", name="booking_create")
     */
    public function create(BookingRepository $bookingRepository, $name)
    {
        $booking = new Booking($name);
        $bookingRepository->save($booking);
        return $this->redirectToRoute('booking_list');
    }
}


Możesz też zobaczyć kod na żywo u siebie, o ile ściągniesz to repozytorium:

git clone [email protected]:wuestkamp/symfony-messaging-queuing-example.git

cd symfony-messaging-queuing-example
git checkout step1 # in branch step2 this is all done already
composer install
bin/console doctrine:schema:create
bin/console doctrine:fixtures:load -n
bin/console server:run


Jeśli postępujesz zgodnie z instrukcjami, to wklej dowolny adres URL, który server:run zwraca w przeglądarce, i otwórz go:


Powyżej widać 3 rezerwacje z naszych data fixtures. Ale to nie wszystko, co może zrobić nasze API! Może ono również tworzyć rezerwacje. W tym celu wywołujemy adres URL, który tworzy nową rezerwację i przekierowuje ją z powrotem do /bookings.

Wywołanie URL-a do tworzenia bookingu


które przekierowuje nas do /bookings

Krok 1: użyj komponentu Messenger synchronicznie

Użyj CreateBookingMessage, aby oddzielić logikę tworzenia


Aby to zaimplementować, musimy zainstalować komponent Messenger:

composer require messenger


Następnie tworzymy dwa nowe katalogi:


Wewnątrz wiadomości utwórz plik CreateBookingMessage.php. Jest to klasa przechowująca dowolne informacje. W Twoim przypadku będzie to ciąg znaków $name, ponieważ możemy z niego utworzyć nowy obiekt Booking.

<?php

namespace App\Message;

class CreateBookingMessage
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}


Wewnątrz MessageHandler utwórz plik CreateBookingMessageHandler.php, który jest wywoływany za każdym razem, gdy uruchamiamy CreateBookingMessage za pomocą komponentu Messenger.

<?php

namespace App\MessageHandler;

use App\Entity\Booking;
use App\Message\CreateBookingMessage;
use App\Repository\BookingRepository;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;

class CreateBookingMessageHandler implements MessageHandlerInterface
{
    private $bookingRepository;

    public function __construct(BookingRepository $bookingRepository)
    {
        $this->bookingRepository = $bookingRepository;
    }

    public function __invoke(CreateBookingMessage $bookingMessage)
    {
        $booking = new Booking($bookingMessage->getName());
        $this->bookingRepository->save($booking);
    }
}


Wewnątrz metody __invoke() tworzymy nową rezerwację za pomocą BookingRepository. Użyjmy teraz CreateBookingMessage w naszej metodzie BookingController::create i dajmy jej taki oto wygląd:

<?php
...
/**
 * @Route("/bookings/create/{name}", name="booking_create")
 */
public function create(MessageBusInterface $messageBus, $name)
{
    $messageBus->dispatch(new CreateBookingMessage($name)); // this is still synchronously!
    return $this->redirectToRoute('booking_list');
}


Jak widać, nie używamy już BookingRepository. Teraz utwórzmy ponownie kolejną rezerwację, wywołując /bookings/create/great-adventure.

Zostaliśmy bezpośrednio przekierowani do /bookings.

Wszystko działa tak jak poprzednio i nadal jest wykonywane synchronicznie, mimo że wywołaliśmy $messageBus->dispatch(), co brzmi nieco asynchronicznie. Po co więc to wszystko? Chyba po prostu skomplikowaliśmy sprawę... no niby tak, ale nie do końca.

Jak skrócić czas wykonywania rzeczy?

Wyobraźmy sobie, że stworzenie rezerwacji zajmuje trochę czasu - wymaga wysłania zapytań do innych usług i kilkukrotnego wysyłania żądań, które wracają po nie wiadomo jak długim czasie.

Zasymulujmy teraz taki scenariusz, zmieniając wywołanie CreateBookingMessageHandler: __ na:

<?php
...
public function __invoke(CreateBookingMessage $bookingMessage)
{
    sleep(5); // I know! impressive action.
    $booking = new Booking($bookingMessage->getName());
    $this->bookingRepository->save($booking);
}


Tworzenie nowej rezerwacji zajmuje teraz 5 sekund.

Tworzymy kolejną rezerwację, wywołując /bookings/create/holy-cow. Działa, ale trwa zbyt długo! Dobry inżynier oprogramowania wie, że prosta akcja może stać się w przyszłości bardziej złożona.

Na szczęście od samego początku korzystaliśmy z Symfony Messenger! Teraz to tylko kwestia dorzucenia konfiguracji w yaml i asynchronicznej obsługi wiadomości.

Krok 2: użyj komponentu Messenger w kolejce asynchronicznej


Tworzenie rezerwacji trwa zdecydowanie za długo. Nikt nie będzie przecież czekał 5 sekund. Będziemy więc obsługiwać tę złożoną czynność asynchronicznie w tle. W tym celu należy zmienić domyślny messenger.yaml na:

framework:
    messenger:
        transports:
            async: "%env(MESSENGER_TRANSPORT_DSN)%"
        routing:
            'App\Message\CreateBookingMessage': async


Możemy to ustawić tylko dla określonych wiadomości! Musimy również zdefiniować MESSENGER_TRANSPORT_DSN w pliku .env:

MESSENGER_TRANSPORT_DSN=doctrine://default



Teraz za każdym razem, gdy zostanie uruchomione CreateBookingMessage, to będzie ono obsługiwane asynchronicznie przez tabelę Doctrine. W tym celu musimy stworzyć nowy schemat bazy danych:

bin/console doctrine:schema:drop --force
bin/console doctrine:schema:create
bin/console doctrine:fixtures:load -n


Musimy również uruchomić workera działającego w tle:

bin/console messenger:consume -vv


Teraz tworzymy nową rezerwację, czyli /bookings/create/holiday-hopper.

Następuje natychmiastowe przekierowanie, a nigdzie nie można znaleźć nowej rezerwacji


Zostaliśmy natychmiast przekierowani do /bookings, ale brakuje naszej rezerwacji. Poczekaj kilka sekund i odśwież:


Po kilku sekundach oczekiwania i odświeżenia widzimy naszą nową rezerwację


Sprawdź też dane wyjściowe procesu workera:


W logach workera widzimy, jak obsługiwane jest CreateBookingMessage


Zamiast nie pokazywać niczego, lepiej byłoby rzecz jasna utworzyć rezerwację jako „oczekującą”, pokazując ją użytkownikowi od razu. Wtedy nasz asynchroniczny worker pracowałby nad rezerwacją i aktualizował jej status.

Aby to osiągnąć, sensowne byłoby podanie identyfikatora rezerwacji wraz z wiadomością i ponowne zapytanie obiektu rezerwacji w handlerze.

Czego się nauczyliśmy?

Symfony Messenger może się przydać przy planowaniu nowej aplikacji czy rozszerzenia.

Pomyśl o akcjach dostępnych w aplikacji jak o komunikatach i twórz programy do ich obsługi. Jeśli pewne części aplikacji są lub stają się bardziej złożone, przełącz je tak, aby były obsługiwane asynchronicznie przez workera.

Sprawdź dokumentację Symfony Messenger, aby dowiedzieć się więcej. W naszym przykładzie wykorzystaliśmy doctrine transport, ale w miarę wzrostu złożoności korzystanie z innych transportów, takich jak AMQP i Redis, też jest całkiem proste.


Oryginał tekstu w języku angielskim możesz przeczytać tutaj.

<p>Loading...</p>