Переосмысление Event Listeners

Давайте поговорим об Event Listeners. Вы знаете, что такое Event Listener?

Ну, если вы привыкли работать с Symfony, тогда вы должны знать, для чего они предназначены. Если нет, то не постесняйтесь взглянуть в документацию Symfony. (Или почитайте про паттерн Наблюдатель, прим. перев.)

Этот пост преследует цель начать небольшое обсуждение о том, как Event Listener должен выглядеть, если мы действительно хотим сохранить логику отдельно.

«многабукаф»

  • Если мы поместим нашу бизнес-логику в Event Listener, то не сможем использовать эту логику из других точек. К примеру, отправить email «Заказ создан» по электронной почте вручную нашим Админом, используя простую кнопку.
  • Что мы могли бы сделать, так это поместить ВСЮ логику внутри службы, которая ТОЛЬКО отправляет этот email, обеспечив простой и задокументированный API.
  • Тогда, учитывая, что эта служба будет внедрена с помощью любой реализации Внедрения Зависимостей, мы сможем внедрить его в Event Listener.
  • Так Event Listeners станут только точкой входа для слоя служб.

Используем Event Listeners

Чтобы понять, о чём я говорю, давайте продемонстрирую небольшой пример:

Сценарий: Вы покупаете что-то в интернет магазине, значит, ваша Корзина получила Заказ. Поскольку пользовательский опыт (user experience) важен в этой ситуации, вы захотите послать электронное письмо пользователю с некоторой Информацией о заказе, т. е. вам нужно отправить email Клиенту.

Проблема: Конечно же вы хотите создать эту функцию очень несвязанным образом. Платформа интернет магазина предоставляет вам только событие, что заказ создан. Вы можете получить доступ к самому Заказу и к Клиенту.

Решение: Создать новый Event Listener объект, подписанный на событие order.oncreate, и отправить email.

Рассмотрим маленький пример того, как Event Listener должен выглядеть. Будем идти простым путём, только фокусируясь на отправке электронного письма в несвязанной форме с действием.

class OrderEmailEventListener
{
    /**
     * Мы посылаем email Клиенту, как только заказ создан
     *
     * @param OrderOnCreatedEvent $event Event
     */
    public function sendEmail(OrderOnCreatedEvent $event)
    {
        $order = $event->getOrder();
        $customer = $event->getCustomer();

        /**
         * Отправить email
         */
    }
}

И мы могли бы использовать эту конфигурацию в нашем пакете.

services:

    #
    # Event Listeners
    #
    project.event_listener.order_created_email:
        class: My\Bundle\EventListener\OrderCreatedEmailEventListener
        tags:
            - { name: kernel.event_listener, event: order.oncreate, method: sendEmail }

Таким образом, это можно очень легко реализовать. Я так делаю с начала времён, поскольку это можно было считать хорошей практикой (good practice). Но, в то же время, делая всё больше и больше Event Listeners, мне пришли на ум некоторые вопросы.

Отделим логику от события

Предположим, что наш проект имеет админ-панель. Конечно, мы должны иметь возможность отправить email в любое время, когда это необходимо (к примеру, наш email сервер упал во время обработки заказа, и мы должны переслать сообщение). Возможно ли это с такой реализацией?

Да. Давайте сделаем это, учитывая, что мы внедрили наш EventListener, и он доступен локально!

$event = new OrderCreatedEmailEventListener(
    $order,
    $customer
);
$orderCreatedEmailEventListener->sendEmail($event);

Ну, эта часть кода действительно пошлёт электронное письмо…, но данная реализация действительно ли достаточно верна? Я думаю, что это не так…

Этот Event должен быть отправлен только тогда, когда происходит реальное событие. Нет никакого смысла создавать новый экземпляр OrderEmailEventListener без использования диспетчера событий. Впрочем, это означает и то, что какой-то Заказ должен был быть создан.

Итак, прежде всего, создание нового Event вне очереди — это не очень хорошая практика вообще.

Для решения проблемы, можно было бы поступить так:

class OrderCreatedEmailEventListener
{
    /**
     * Мы отправляем email Клиенту как только заказ будет создан
     *
     * @param OrderOnCreatedEvent $event Event
     */
    public function sendEmail(OrderOnCreatedEvent $event)
    {
        $order = $event->getOrder();
        $customer = $event->getCustomer();

        $this->sendOrderCreatedEmail(
            $order,
            $customer
        );
    }

    /**
     * Мы отправляем email Клиенту как только заказ создан,
     * учитывая заказ и клиента
     *
     * @param OrderInterface    $order    Order
     * @param CustomerInterface $customer Customer
     */
    public function sendOrderCreatedEmail(
        OrderInterface $order,
        CustomerInterface $customer
    ) {
        /**
         * Send the email
         */
    }
}

И тогда, мы могли бы сделать следующее:

$orderCreatedEmailEventListener->sendOrderCreatedEmail(
    $order,
    $customer
);

Намного лучше, не правда ли? Но достаточно ли хорошо? Нет.

Отделим логику от слушателя

Мы используем экземпляр Event Listener, чтобы отправить письмо. Наш анализ может быть точно такой же, как раньше… Должны ли мы использовать Event Listener даже тогда, когда событие не отправляется?

Нет, не должны.

Event Listener — это слушатель события. Слушает он одно событие, и это должна быть всей его работой. Таким образом, нам не потребуется нигде внедрять какого-либо слушателя события. Давайте сделаем некоторый рефакторинг сейчас!

Прежде всего, изолируем нашу бизнес-логику в новую службу. Эта служба будет делать только одну вещь — отправлять электронное письмо.

class OrderCreatedEmailSender
{
    /**
     * Мы отправляем письмо Клиенту как только заказ создан,
     * учитывая заказ и клиента
     *
     * @param OrderInterface    $order    Order
     * @param CustomerInterface $customer Customer
     */
    public function sendEmail(
        OrderInterface $order,
        CustomerInterface $customer
    ) {
        /**
         * Send the email
         */
    }
}

Данная служба имеет только одну миссию: отправить письмо, независимо от того, какое событие запускает службу, независимо от его окружения. Так что, если мы взглянем на то, как реализация Event Listener должна выглядеть сейчас…

class OrderCreatedEmailEventListener
{
    /**
     * @var OrderCreatedEmailSender
     *
     * Созданный Заказом отправитель email 
     */
    private $orderCreatedEmailSender;

    /**
     * Конструктор
     *
     * @param OrderCreatedEmailSender $orderCreatedEmailSender
     */
    public function __construct(OrderCreatedEmailSender $orderCreatedEmailSender)
    {
        $this->orderCreatedEmailSender = $orderCreatedEmailSender;
    }

    /**
     * Мы посылаем email Клиенту, как только заказ создан
     *
     * @param OrderOnCreatedEvent $event Event
     */
    public function sendEmail(OrderOnCreatedEvent $event)
    {
        $order = $event->getOrder();
        $customer = $event->getCustomer();

        $this
            ->orderCreatedEmailSender
            ->sendEmail(
                $order,
                $customer
            );
    }
}

Наконец, мы должны реорганизовать определение нашей службы в конфигурационном файле внедрения зависимостей (DependencyInjection).

services:

    #
    # Business layer
    #
    project.business.order_created_email_sender:
        class: My\Bundle\Business\OrderCreatedEmailSender

    #
    # Event Listeners
    #
    project.event_listener.order_created_email:
        class: My\Bundle\EventListener\OrderCreatedEmailEventListener
        arguments:
            - @project.business.order_created_email_sender
        tags:
            - { name: kernel.event_listener, event: order.oncreate, method: sendEmail }

И это всё. Этот пример очень лёгок и прост, но я уверен, что если вы посмотрите на свой проект, то найдёте много логики в ваших Event Listener. Может быть будет хорошей идеей начать выносить всю эту логику из коробки, обрабатывая этих слушателей как реальные точки входа, как мы это делаем с нашими Командами (Commands), Контроллерами или Twig расширениями.


Примечание

Это перевод статьи «Re-thinking Event Listeners — Because yes».

Комментарии

  1. ... пишет:

    У вас ссылка в уведомлениях не работает. Сайт отличный!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *