Компонент Messenger (Symfony)

Компонент Messenger (Symfony)

Ще в березні 2018 року серед розробників швидко поширилася приємна новина про те, що сімейство компонентів Symfony поповнилося корисним інструментом Messenger. За задумом творців, цей інструмент Symfony повинен був взяти на себе завдання відправки/отримання повідомлень в/з інших додатків, а також управління чергою.

Природно, що поява цікавого інструменту викликає бажання швидше впровадити його в проекти. Однак мало хто може собі це дозволити. Тому ми не стали ризикувати своїм часом, а також стабільністю роботи додатків наших клієнтів, і відклали це питання.

І ось, настав 2020 рік, і кожного разу, коли я переглядаю changelog нових релізів Symfony, я натрапляю на правки Messenger. Очевидно, що робота кипіла майже два роки, компонент здається вже більш стабільним, а значить, прийшов час придивитися до нього ближче.

Я не люблю копіювати приклади з документації, тому вирішив зосередитися на концептуальних речах, вивчити структуру Symfony Messenger, його основні абстракції та завдання, які на них покладені. Вичерпний підручник з Symfony Messenger читайте в наступній статті блогу.

Складність сприйняття

Почну з невеликого ліричного відступу і торкнуся теми складності сприйняття інформації.

Складність вивчення нової теми багато в чому залежить від того, як автор подає матеріал (під матеріалом я маю на увазі: документацію, статті, скринкаст і т.д.). Ви, напевно, чули, або, можливо, зустрічали людей, які є справжніми професіоналами своєї справи, але абсолютно не вміють висловлюватися просто, а іноді їх спроби зробити це тільки погіршують ситуацію.

Передача знань не є повністю формалізованим процесом, тобто не існує спеціального підходу, при якому кожен студент зрозуміє матеріал швидко і правильно. Тому кожен робить це так, як вміє.

Я вирішив написати про це, тому що перше, з чим я зіткнувся, коли почав вивчати Symfony Messenger, була блок-схема, представлена нижче, і незважаючи на те, що схеми повинні спрощувати сприйняття, вона викликала у мене зворотний ефект.

Джерело: https://symfony.com/doc/current/components/messenger.html

Щоб ви не витрачали багато часу, я хотів би дати кілька тезових рекомендацій щодо прочитання цієї схеми:

    • Ваше повідомлення, а це може бути будь-який об'єкт, перетворюється на додаткову обгортку під назвою Envelope. Ви можете побачити це, подивившись на реалізацію методу dispatch класу MessageBus class.
      1. $envelope = Envelope::wrap($message, $stamps);
    • Всередині Bus, Envelope проходить через ланцюжок Middlewares за допомогою яких можна використовувати додаткову логіку (на діаграмі вона не відображена);
    • Stamps - це метадані, які дозволяють фактично керувати повідомленням всередині проміжного програмного Middlewares (на схемі не відображено). Принцип надзвичайно простий: якщо Envelope надсилається з якоюсь Stamp, то, знаючи її, ми можемо виконати додаткову логіку;
    • Пропустивши Envelope через всі Middlewares, Bus використовує вказаний вами Transport і, нарешті, відправляє повідомлення;
    • На діаграмі видно, що Receiver, Bus and Sender мають однаковий колір. На мою думку, це не зовсім правильно. Receiver і Sender - це Transport.

Ви можете переконатися в цьому, трохи вивчивши структуру компонента Symfony Message.

Таким чином, під фразою Bus використовує Transport передбачається, що Sender вказаного Transport буде активований для вашого повідомлення. Аналогічний висновок стосується і Receiver - Кружечками на схемі позначені саме системи 3-ї частини, такі як RabbitMQ, Doctrine, Redis, і т.д.

Буду радий, якщо ці нотатки допоможуть вам швидше розібратися в схемі. Мені це вдалося лише після вивчення кодової бази та прикладів з документації.

Message Bus (шина повідомлень)

Це базова абстракція, в завдання якої входить контроль над процесом відправки повідомлення і його передача обробнику. Незважаючи на те, що завдання, покладені на Bus є ключовими, це досить простий клас з одним методом відправки. Вся справа в тому, що Bus робить це дуже елегантно.

Як ви можете бачити з коду, Bus виконує лише дві ключові функції: загортає повідомлення в Envelope (більш детально про цю абстракцію пізніше) та ітеративно передає повідомлення через Middlewares.

Middlewares це ті самі будівельні блоки, які реалізують основну логіку роботи Symfony Messenger. Варто знати, що Middlewares виконуються в тому порядку, в якому вони зареєстровані в контейнері.

Звичайно, ви також можете реалізувати будь-яку бізнес-логіку у вашому проміжному Middlewares, але майте на увазі, що SendMessageMiddleware і HandleMessageMiddleware завжди виконуються в останню чергу... що загалом логічно :)

Список доступних програм можна отримати, виконавши наступну команду:

    php bin/console debug:container messenger.middleware

На момент написання статті - так:

     [0 ] messenger.middleware.send_message
      [1 ] messenger.middleware.handle_message
      [2 ] messenger.middleware.add_bus_name_stamp_middleware
      [3 ] messenger.middleware.dispatch_after_current_bus
      [4 ] messenger.middleware.validation
      [5 ] messenger.middleware.reject_redelivered_message_middleware
      [6 ] messenger.middleware.failed_message_processing_middleware
      [7 ] messenger.middleware.traceable
      [8 ] messenger.middleware.doctrine_transaction
      [9 ] messenger.middleware.doctrine_ping_connection
      [10] messenger.middleware.doctrine_close_connection

Middleware, Envelope і Stamps

Об'єкт, як варіант передачі повідомлення, обраний не просто так, це чудовий спосіб передати контекст іншим частинам системи.

Об'єкт, як варіант передачі повідомлення, обраний не просто так, це чудовий спосіб передати контекст іншим частинам системи. Handler знайде цей UUID користувача в базі даних і надішле йому листа на поштову адресу.

    class UserRegisteredMessage
    {
       /** @var string */
       private $userId;
     
       /**
        * @param string $userId
        */
       public function __construct(string $userId)
       {
           $this->userId = $userId;
       }
     
       /**
        * @return string
        */
       public function getUserId(): string
       {
           return $this->userId;
       }
    }

Це легко зрозуміти і зрозуміло з EventDispatcher. З чим же тоді пов'язана поява абстракцій Envelope і Stamp?

Я вже згадував, що Bus пропускає повідомлення через проміжну логіку, так звані Middlewares. Ми можемо керувати нашим повідомленням всередині кожного проміжного програмного забезпечення Middleware.

Тепер виникає питання, якщо я хочу, щоб Middleware 1 нічого не робив з повідомленням, а деяка логіка була виконана в Middleware 2, як я можу повідомити Middleware 2 про це? (див. діаграму нижче).

Також можна перевірити екземпляр переданого повідомлення у Middleware 2.

    $envelope instanceof UserRegisteredMessage

Це гарне рішення, але тоді Middleware 2 буде жорстко пов'язано з конкретним повідомленням. Я не виключаю, що такі випадки також можуть бути, але що, якщо ми маємо справу з універсальним Middleware яке повинно обробляти різні повідомлення?

Тут ми приходимо до ідеї метаданих, тобто якогось допоміжного набору даних, який повинен надсилатися разом з повідомленням. Stamps роблять це. Це маркер, який несе свій власний контекст.

І це є першопричиною появи Stamps. Що стосується Envelope, то тут потрібно відповісти на наступне питання: наскільки коректно передавати метадані разом з контекстом повідомлення? Тобто, зберігати Stamps безпосередньо в об'єкті повідомлення.

Відповідь наступна: це буде дуже грубим порушенням, тому що контекст повідомлення виходить з бізнес-вимог, а метадані - з реалізації.

Звідси випливає друга ідея, що контексти потрібно передавати як окремі об'єкти, а для управління ними потрібна допоміжна абстракція, певний об'єкт управління. Цю функцію було покладено якраз на Envelope. Він виступає обгорткою для нашого листа і додає можливість прив'язувати до повідомлення Stamps.

Давайте закріпимо наше розуміння концепції на простому прикладі. Припустимо, ви хочете перевірити повідомлення, перш ніж відправити його обробнику. Цю проблему можна вирішити за допомогою ValidationMiddleware доступного з коробки. Зверніть увагу на 39-й рядок, валідатор спрацює, якщо до ValidationStamp було прикріплено Envelope

    if ($validationStamp = $envelope->last(ValidationStamp::class)) {
        $groups = $validationStamp->getGroups();
    }

Для цього під час відправки ми передамо об'єкт ValidationStamp з назвою групи валідації. І тоді в разі помилки буде згенеровано ValidationFailedException, яке може бути оброблене тим, хто його викликає.

    $bus->dispatch($message), [
       new ValidationStamp([
           'validation_group',
       ])
    ]);

Transport, Sender і Receiver

Для кращого розуміння, що таке Transport краще уявити його як деяке сховище, для роботи з яким потрібні ще дві абстракції Sender, який реалізує відправку повідомлення в сховище, і Receiver, який забирає це повідомлення з сховища і відправляє його обробнику.

З коробки ми маємо три асинхронні Transports: AmqpExt, RedisExt і Doctrine, а також два синхронних: In Memory та Sync. Причому для Receiver is not required for Sync не потрібна реалізація Messages одразу надсилаються до Handler. Загалом, він робить те ж саме, що і EventDispatcher (ну, ви зрозуміли, чи не так ;)).

Звичайно, у вас є можливість створити свій власний Transport. Для цього необхідно реалізувати два інтерфейси TransportFactory і TransportInterface.

Аспект Transports може зайняти ще одну повноцінну статтю, тому що є багато питань, починаючи від конфігурації та управління чергами і закінчуючи роботою робітників та розгортанням. Але оскільки метою статті є опис основних абстракцій та їх завдань, я обмежуся коротким оглядом цієї теми.

Висновки

Висновок однозначний, Messenger є корисним інструментом для розробки symfony і ми обов'язково будемо використовувати його в майбутніх проектах нашої компанії.

По суті, ми отримуємо покращений EventDispatcher: з маршрутизацією, Middlewares, можливістю керувати повідомленнями через Stamps, а також використовувати різні Transport, в тому числі і асинхронні.

Роблячи такі висновки, я фактично поставив Messenger в один ряд з EventDispatcher, але хочу підкреслити, що усвідомлюю різницю в підходах, які використовують ці два компоненти.

EventDispatcher відправляє, а Listeners / Subscribers обробляють. В той же час, Messenger і надсилає, і контролює обробку. Отже, можна підсумувати, що Messenger може робити все, що робить EventDispatcher і навіть більше, але EventDispatcher не можна сказати те ж саме, оскільки він обмежений у порівнянні з першим інструментом.

Ви маєте право самі вирішувати, що використовувати, але я б віддав перевагу Messenger як більш гнучкій альтернативі.

Тепер давайте подивимося статистику. На момент написання статті на сайті packagist.org було зазначено:

1,8 мільйона інсталяцій і майже 500 зірок.

Щодо проблем, то ми маємо 41/190 (відкритих/закритих) з них:

    • помилки: 13/93;
    • продуктивність: 2/5
    • функціонал: 31/160.

Як бачите, спільнота активно цікавиться і працює над покращенням стабільності компонента, а також додає нові можливості.

І, нарешті, в процесі вивчення матеріалів, доступних в інтернеті, я натрапив на досить цікаву статтю на цю тему. Я не знайомий з автором, але мені дуже імпонує спосіб подачі інформації, тому рекомендую її прочитати

https://vria.eu/delve_into_the_heart_of_the_symfony_messenger/

Ну, а тепер тримайте барвисту шпаргалку :)

https://medium.com/@andreiabohner/symfony-4-4-messenger-cheat-sheet-5ca99cbba4a8

Дякую, що дочитали до кінця. Успіхів вам!