Использование событийной модели в Doctrine 2 + Symfony 3

    Давайте представим ситуацию: у вас есть заказ в интернет магазине (Entity). Заказ имеет некий статус. При смене статуса заказа необходимо провести кучу сопутствующих действий, например:


    • сохранить в заказе дату последнего изменения
    • записать в историю по заказу информацию о смене статуса
    • отослать письмо / sms клиенту
    • вызвать метод API службы доставки / платежной системы / партнера и т.д.

    Возникает вопрос как все это правильно организовать с точки зрения программного кода.
    Все ниже описанное справедливо для Doctrine 2 и Symfony > 3.1


    Если вы не знакомы с событийной моделью Doctrine, то сначала рекомендую ознакомиться с документацией.


    Приведу пример простейшего кода для Entity заказа:


    Код Entity заказа
    /**
     * Order
     *
     * @ORM\Table(name="order")
     */
    class Order
    {
        /**
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
    
        /**
         * @ORM\Column(name="fio", type="string", length=100)
         */
        private $fio;
    
        /**
         * @ORM\Column(name="last_update_date", type="datetime")
         */
        private $lastUpdateDate;
    
        /**
         * @ORM\Column(name="create_date", type="datetime")
         */
        private $createDate;
    
        /**
         * @ORM\Column(name="status_id", type="integer")
         */
        private $statusId;
    
        // дальше getter/setter методы
    }

    Начнем с самого простого — нам нужно, чтобы при создании заказа, в поле create_date была записана дата создания, а при любом изменении заказа, в поле last_update_date, дата последнего изменения.


    Самое простое — это явно добавить параметры в том месте, где заказ создается и обновляется (в контроллере или специальном сервисе).


    $order = new Order();
    $order->setCreateDate(new \DateTime());
    $order->setLastUpdateDate(new \DateTime());
    // ....
    $em->persist($order);
    $em->flush();

    Минусы такого подхода очевидны — если заказ создается, а тем более, обновляется в нескольких местах — нужно будет в каждом месте повторять эту логику. К счастью Doctrine содержит в себе обработку событий (LifecycleEvents).


    Добавляем в описание Entity конструкцию, которая говорит Doctrine, что Entity содержит в себе некие события, которые нужно обработать:


    /**
    * @ORM\HasLifecycleCallbacks()
    */

    и создаем методы, которые будут "реагировать" на эти события. В нашем случае будут два метода:


    /**
    *  @ORM\PrePersist
    */
    public function setCreateDate()
    {
         $this->createDate = new \DateTime();
    }
    /**
    *  @ORM\PreFlush
    */
    public function setLastUpdateDate()
    {
         $this->lastUpdateDate = new \DateTime();
    }

    @ORM\PrePersist и @ORM\PreFlush говорят Doctrine выполнить соответствующие методы соответственно при создании Entity и при каждом ее обновлении. Теперь нет нужды отдельно устанавливать эти даты. Полный список возможных событий можно посмотреть здесь


    Текущий вид Entity заказа
    /**
     * Order
     *
     * @ORM\Table(name="order")
     * @ORM\HasLifecycleCallbacks()
     */
    class Order
    {
        /**
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
    
        /**
         * @ORM\Column(name="fio", type="string", length=100)
         */
        private $fio;
    
        /**
         * @ORM\Column(name="last_update_date", type="datetime")
         */
        private $lastUpdateDate;
    
        /**
         * @ORM\Column(name="create_date", type="datetime")
         */
        private $createDate;
    
        /**
         * @ORM\Column(name="status_id", type="integer")
         */
        private $statusId;
    
        // дальше getter/setter методы
    
        /**
         *  @ORM\PrePersist
         */
        public function setCreateDate()
        {
            $this->createDate = new \DateTime();
        }
        /**
         *  @ORM\PreFlush
         */
        public function setLastUpdateDate()
        {
            $this->lastUpdateDate = new \DateTime();
        }
    }

    Усложним задачу -теперь нам нужно в историю по заказу записать информацию кто и когда менял статус этого заказа, плюс мы хотим отослать письмо о смене статуса клиенту.


    OrderHistory: Entity записи в истории по заказу
    /**
     * OrderHistory
     *
     * @ORM\Table(name="order_status_history")
     * @ORM\HasLifecycleCallbacks()
     */
    class OrderHistory
    {
        /**
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
    
        /**
         * @ORM\Column(name="order_id", type="integer")
         */
        private $orderId;
    
        /**
         * @ORM\Column(name="manager_id", type="integer")
         */
        private $managerId;
    
        /**
         * @ORM\Column(name="status_id", type="integer")
         */
        private $statusId;
    
        /**
         * @ORM\ManyToOne(targetEntity="OrderStatus")
         * @ORM\JoinColumn(name="status_id", referencedColumnName="id")
         */
        private $status;
    
        /**
         * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Manager")
         * @ORM\JoinColumn(name="manager_id", referencedColumnName="id")
         */
        private $manager;
    
        /**
         * @ORM\ManyToOne(targetEntity="Order", inversedBy="orderHistory")
         * @ORM\JoinColumn(name="order_id", referencedColumnName="id")
         */
        private $order;    
    
        // дальше getter/setter методы
    
        /**
         * @ORM\Column(name="create_date", type="datetime")
         */
        private $createDate;
    
        /**
         *  @ORM\PrePersist
         */
        public function setCreateDate()
        {
            $this->createDate = new \DateTime();
        }
    }

    Можно все это делать "вручную" в том месте кода, где статус меняется, но хотелось бы чтобы все происходило "автоматически" без привязки к месту операции по изменению статуса.


    Для этого в Doctrine есть EntityListeners — класс, который отслеживает изменения; место, где можно держать всю логику обработки событий.


    Есть два варианта: либо мы добавляем обработчик событий на уровне описания Entity:


    /**
    * @ORM\EntityListeners({"AppBundle\EntityListeners\OrderListener"})
    */

    И создаем класс Listener-а


    class OrderHistoryListener
    {
        public function postUpdate(Order $order, LifecycleEventArgs $event)
        {
             // some code
        }
    }

    Первый параметр — ссылка на объект, в котором произошли события. Второй — это объект события (о нем мы поговорим ниже).


    Либо,


    • у нас много логики, которая реагирует на события, мы хотим разнести ее по разным классам
    • EntityListener должен реагировать не только на события конкретного класса (например одинаковое письмо отсылаем по событиям нескольких видов Entity)

    можно зарегистрировать обработчики через стандартные сервисы Symfony:


    services:
      order.history.status.listener:
            class: AppBundle\EntityListeners\OrderListener
            tags:
                - { name: doctrine.event_listener, event: preUpdate, method: preUpdate }
                - { name: doctrine.event_listener, event: prePersist, method: prePersist }

    Параметр event определяет событие, на которое будет вызван данный сервис, method — определяет конкретный метод, внутри сервиса. Т.е. сервис может быть один, но обрабатывать разные события для разных Entity.


    В этом случае Listener будет реагировать на события вообще любого Entity и внутри класса нужно будет проверять тип объекта.


    class OrderHistoryListener
    {
        public function preUpdate(PreUpdateEventArgs $event)
        {
            if ($event->getEntity() instanceof Order) {
    
            }
        }
    }

    EntityListener может содержать различные методы (handlers), в зависимости от того, на какое событие мы хотим получить реакцию.


    Объект $event уже содержит в себе ссылки на EntityManager и на UnitOfWork. Соответственно уже есть все, чтобы работать с объектами Doctrine. Вы можете вытаскивать необходимые объекты, обновлять и удалять их.


    Сложности начинаются, когда вы хотите сделать что-то, не связанное с базой, например отправить письмо. Для этого в EntityListener нужно внедрить зависимости на внешние сервисы.


    В первом случае мы создаем запись вида, которая внедрит зависимости в EntityListener


    services:
        app.doctrine.listener.order:
            class: AppBundle\EntityListeners\OrderListener
            public: false
            arguments: ["@mailer", "@security.token_storage"]
            tags:
                - { name: "doctrine.orm.entity_listener" }

    Во втором, просто добавляем строку с зависимостями


    services:
      order.history.status.listener:
            class: AppBundle\EntityListeners\OrderListener
            arguments: ["@mailer", "@security.token_storage"]
            tags:
                - { name: doctrine.event_listener, event: preUpdate, method: preUpdate }
                - { name: doctrine.event_listener, event: prePersist, method: prePersist }

    Дальше все как с обычным Symfony-сервисом.


    Внутри Listener можно получить проверку на то, изменилось ли поле, а также получить текущее и предыдущее значения.


    if ($event->hasChangedField('status_id')) {
        $oldValue = $event->getOldValue('status_id');
        $newValue = $event->getNewValue('status_id');
    }

    Окончательный вид Entity заказа
    /**
     * Order
     *
     * @ORM\Table(name="order")
     * @ORM\EntityListeners({"AppBundle\EntityListeners\OrderListener"})
     * @ORM\HasLifecycleCallbacks()
     */
    class Order
    {
        /**
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
    
        /**
         * @ORM\Column(name="fio", type="string", length=100)
         */
        private $fio;
    
        /**
         * @ORM\Column(name="last_update_date", type="datetime")
         */
        private $lastUpdateDate;
    
        /**
         * @ORM\Column(name="create_date", type="datetime")
         */
        private $createDate;
    
        /**
         * @ORM\Column(name="status_id", type="integer")
         */
        private $statusId;
    
        // дальше getter/setter методы
    
        /**
         *  @ORM\PrePersist
         */
        public function setCreateDate()
        {
            $this->createDate = new \DateTime();
        }
        /**
         *  @ORM\PreFlush
         */
        public function setLastUpdateDate()
        {
            $this->lastUpdateDate = new \DateTime();
        }
    }

    Код OrderListener
    class OrderListener {
    
        private
            $_securityContext = null,
            $_mailer = null;
    
        public function __construct(\SwiftMailer $mailer, TokenStorage $securityContext)
        {
            $this->_mailer = $mailer;
            $this->_securityContext = $securityContext;
        }
    
        public function postUpdate(Order $order, LifecycleEventArgs $event)
        {
            $em = $event->getEntityManager();
    
            if ($event->hasChangedField('status_id')) {
    
                $status = $em->getRepository('AppBundle:OrderStatus')->find($event->getNewValue('status_id'));
    
                $history = new OrderHistory();
                $history->setManager($this->_securityContext->getToken()->getUser());
                $history->setStatus($status);
                $history->setOrder($order);
    
                $em->persist($history);
                $em->flush();
    
                // код для отправки письма с помощью SwiftMailer
    
            }
        }
    }
    Поделиться публикацией

    Комментарии 51

      0
      Интересно) Надо будет на Yii2 попробовать такое разделение и автоматизацию…
        0
        На Yii2 это можно решить поведениями в любом наследнике yii\base\Component. Практически один в один стандартные события как в Lifecycle Events. Что Вы имели в виду под разделением?
          –1

          Та достался мне тут один проект на поддержку, где модель служит только для запросов к базе данных… Вся бизнес-логика в контроллерах и вьюхах(
          Теперь переписываю, боясь что-то сломать.
          Просто понравилась реализация истории заказа, я наверное не додумался бы до такого)

            0
            На самом деле самая тривиальная проблема мейнтейнить рабочий проект, в котором вот такое как Вы сказали. Самим часто достаётся подобное. Вы написали про разделение, в Yii2 поведениями решается всё, от трансформации атрибутов модели до запусков бизнес логики. Поэтому у нас в компании действует правило, мы не пишем бизнес логику в поведениях. Та часть процессов, запуск которых связан с изменением AR мы явно вызываем в переопределённых методах insert(), update(), delete().
            public function insert($runValidation = true, $attributes = null)
            {
                Yii::$app->MoexOperation->clearing($this);
                return parent::insert($runValidation, $attributes);
            }
            

            Ничего поэтичного зато выделено. А вы как разделяете?
              –1
              Я сделал trait, который берёт из свойств $beforeSave и $afterSave массив функций(с ключом приоритета), и вызывает их в аналогичных методах с приоритетом
        0
        А что если при событии необходимо изменять (и сильно) другие сущности? Есть какой-то путь для этого?
          0
          Если я правильно понял вопрос, то для Вашего случая подходят EntityListener (описаны в статье). Внутри LifecycleEventArgs, который передается в качестве события во внутрь EntityListener, есть ссылка на EntityManager, через который, в свою очередь, через getRepository() Вы можете получить доступ к любому объекту, а также к его изменению.
            +1
            Делать это на событиях доктрины — очень плохая практика. docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#implementing-event-listeners — здесь подробно описаны ограничения- что можно и чего нельзя делать в каждом конкретном событии, но даже после досконального изучения можно словить очень нетривиальные ошибки.
              0
              В том-то и беда, что практика уже была :) Столкнулись со всеми прелестями после обновления доктрины, поэтому и нужны пути обхода.
                0
                Мы пришли к использованию своих событий через dispatcher, это даёт больше контроля и снимает привязку к доктрине.
            +4

            Don't use Lifecycle Callbacks for Business Logic/Events by @ocramius

              +1
              Никто и не использует Lifecycle Callbacks для бизнес логики. Вряд ли установку текущей даты внутри Entity можно отнести к бизнес-логике.
                +1
                А к чему же отнести, как не к бизнес-логике? Она, самая. Информация о изменении заказов нужна менеджеру, нужна бизнес-аналитику.
                  0
                  Ну, между прочим, в презентации выше сказано что установка даты изменения — это ок.
                    0

                    Но формирование аудита или лога изменений — уже не ок.

                      0
                      Вы говорите про LifecycleCallbacks (в котором бизнес логике быть не должно) или про внешние EntityListener / DoctrineEventListener, в которых формирование аудита или формирование лога изменений (особенно если этот лог не связан с БД в которой идут изменения, например лог пишется в файл) вполне ОК?

                      Вот посмотрите пример на самом doctrine
                      docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#preupdate

                      public function preUpdate(PreUpdateEventArgs $eventArgs)
                          {
                              if ($eventArgs->getEntity() instanceof Account) {
                                  if ($eventArgs->hasChangedField('creditCard')) {
                                      $this->validateCreditCard($eventArgs->getNewValue('creditCard'));
                                  }
                              }
                          }
                      


                      Валидация номера кредитной карты — это вполне себе бизнес логика.
                        0
                        вполне ОК?

                        Для очень простых кейсов — возможно. Но подвязываться не события лишенные контекста — это крайне неудобно и будет вызывать проблемы при внесении изменений в бизнес логику.


                        Вот посмотрите пример на самом doctrine

                        Увы документация к доктрине далеко не соответствует их же лучшим практикам. Ребята вроде Окромиуса это довольно часто говорят и это один из основных пунктов к "переписать" для 3-ей версии.


                        Валидация номера кредитной карты — это вполне себе бизнес логика.

                        если вы посмотрите doctrine best practice то вы увидите что этот пример не имеет смысла. так как у вас никогда не должно быть невалидного состояния в сущностях. Подобная валидация должна происходить еще на подходе к сущности (в крайнем случае в методе который меняет стэйт). Поменять стэйт а потом разбираться что пошло не так — не очень корректный вариант ведения дел.

                        0
                        Да, не ок. Ну и наверно в контексте заказа дата изменений тоже относится к бизнес логики…
                    +2
                    Сохранение OrderHistory это бизнес-логика. Да даже сохранение текущей даты, это бизнес-логика.
                  +1
                  Лучше использовать доменные ивенты, без привязки к доктрине. Пример реализации от Fesor github.com/fesor/domain-events
                    0

                    Отдельно распишу преимущества:


                    • доменные ивенты проще в отладке
                    • доменные ивенты по хорошему должны запускаться когда все хорошо, то есть мы можем скажем по postFlush их обработать и мы точно знаем что транзакция была успешна
                    • доменные ивенты позволяют переходить на более сложные вещи вроде саг. Когда одна логическая транзакция должна либо инициировать другую либо, в случае если что-то пощло не так, компенсировать предыдущие транзакции. Пример. Первая транзакция создала заказ со статусом pending, это привело к доменному ивенту OrderSubmitted например. По этому ивенту кто-то запускает новую логическую транзакцию например для проведения оплаты. Если оплата не удалась, выкидываем событие PaymentFailed, по которому мы меняем статус заказа на failed.

                    То есть в целом штука очень гибкая и позволяет нам очень снизить связанность и по настоящему начать выделять изолированные контексты приложения. А так же за пару минут менять тригеры операций, что может дать очень существенный профит в ситуациях когда бизнес еще не определился что когда делается.

                      0
                      Столкнулся с такой задачей:
                      Происходят несколько событий, которые должны привести к единоразовому выполнению subscriber.
                      Пример: вы создали пользователя, дальше меняете ему дату рождения, дальше вы меняете его прописку (привожу примеры атомарных операций, где логично на каждую операцию иметь отдельный метод. то есть не весь апдейт производить в одном методе).
                      Дальше у вас задача: на все изменения данных клиента отсылать ему СМС

                      $user = new User($email,$password); // создаем eventUserCreated
                      $user->changeShippingAddress($address) // создаем eventAddressChanged
                      $user->changeLegal($lega) //создаем eventLegalChanged.
                      


                      Любой из eventUserCreated, eventAddressChanged, eventLegalChanged должен вызывать отправку смс.
                      Как вы предлагаете решать задачу, когда СМС должна уходить только одна (после последнего изменения)?

                      UPD. Это для AR, где save() может вызываться не один раз.
                        0

                        Для того что бы более-менее корректно вам что-то посоветовать мне нужно лучше понимать вашу бизнес логику. Потому несколько уточняющих вопросов:


                        • Пользователь может существовать с пустыми значениями для даты рождения, адресов прописки и т.д.? Или это обязательная информация для регистрации?
                        • Эти данные меняются по отдельности или вместе? Или иногда по отдельности и иногда вместе? Приведите примеры.
                        • Имеет ли смысл событие AddressChanged если это по сути инициализация значения?
                          0
                          Пользователя я привел в качестве примера, чтобы не расписывать тут бизнес логику на лист А4.
                          Но продолжим с пользователем:
                          Пользователю для регистрации в магазине достаточно заполнить email.
                          Дальше он может в ЛК указать ФИО, дату рождения. Если он захочет заказать посылку — он может указать адрес, если адрес не указан — с ним свяжется менеджер и заполнит его данные в админке.
                          То есть существование отдельных методов на указание адреса, даты рождения отдельно от регистрации оправданно и существует.

                          В то же время пользователь на полной форме регистрации может ввести эти данные скопом, да еще и фотку загрузить и родственников указать. (Можно дальше копать в реализацию пользователя, но это же только пример того, что несколько атомарных операций могут вызывать отправку СМС).

                          Допустим, такой вариант кода:
                          <?php
                          
                          class User
                          {
                              public function changeInfo($userInfo)
                              {
                                  ....
                                  $this->rememberThat(new UserChangeInfo($this));
                              }
                          
                              public function changeAddress($adress)
                              {
                                  ....
                                  $this->rememberThat(new UserChangeAdress($adress));
                              }
                          
                              public function changeUserAll($userInfo, $adress)
                              {
                                  $this->changeInfo($userInfo)
                                  $this->changeAddress($adress);
                              }
                          }
                          


                          Наш subscriber подписан на UserChangeInfo, UserChangeAdress.
                          Пока в коде проблема — при вызове changeUserAll метод subscriber будет вызван 2 раза.
                            0
                            Я бы напилил на все эти ивенты один хендлер, который все эти ивенты собирает во внутреннюю коллекцию, а по кернел терминейт продьюсит новый ивент со всей изменившейся информацией.
                              0

                              kernel.terminate не вызывается в консоле. Конкретно в этом случае это не важно, но если, к примеру, данные будут обновляться по крону из какой-нибудь API, то как тогда?

                                0
                                То тогда вам все равно нужен некий пулл который соберет все изменения за N времени и только тогда спродьюсит ивент на отправку нотификации.
                                  0

                                  если что, кроме kernel.terminate есть еще console.terminate (пруф)

                                    0

                                    подобное использование консольных команд не особо эффективно (для того что бы это работало нужно на один вызов команды делать одну операцию, что означает много короткоживущих процессов).


                                    Вообще подвязывать подобную логику на такие вот события — наверное не ок. Хотя вариант, скажем, агрегации ивентов по времени (объеденять на уровне консюмера все ивенты от одного и того же агрегата, что-то типа throttle/debounce) тоже не шибко хорошая идея, хотя с хорошим брокером сообщений и хитрой маршрутизацией какой может и норм.

                                0
                                А как выглядит у вас событие, например, UserChangeInfo? И где вызывается changeUserAll?
                                  0
                                  Ну допустим new UserChangeInfo($user, array $infoOld, array $infoNew).
                                  changeUserAll — вызываю в контроллере настроек пользователя.
                                  changeAddress, changeInfo — могу вызвать и в админке менеджеров, и в контроллере настроек пользователя.
                                  +1

                                  Я в это случае создаю CQRS команду на отправку SMS уведомления на каждое событие и кладу ее в очередь уникальных команд и по крону разбираю очередь.
                                  Это даёт нам и сохранение уникальности уведомлений и позволяет не тормозить клиент на отправку SMS и контролировать нагрузку на сервер отправки SMS.

                                    0
                                    Понятно, смс не подходящий пример так как можно отправлять через очередь.
                                    Давайте на примере заказов:
                                    Пользователь создал заказ:
                                    1) Нужно проверить проверить была ли ссылка на оплату реферальной, если была — пользователю «рефералу» зачислить бонусные баллы.
                                    2) При первом создании заказа начислить пользователю бонусные баллы.
                                    3) После оплаты заказа начислить пользователю бонусные баллы, если он оплатил товар акционной категории.

                                    Пользователь может оплатить спустя время (после подтверждения наличия товара на складе), а может оплатить сразу же (например покупка электронной книги).

                                    Так у нас будет два события:
                                    — заказ создан
                                    — заказ оплачен
                                    После каждого из них мы должны пересчитать бонусные баллы.
                                    И есть три метода:
                                    — создать заказ
                                    — оплатить заказ
                                    — создать заказ и оплатить его (внутри последовательно вызываем «создать заказ» и «оплатить заказ»)

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

                                    Сам я как раз сделал «уникальные команды», но в рантайме, да еще и с приоритетами (сначала обрабатываем события непосредственно оплаты, потом события бонусной программы, потом можем и СМС послать)

                                    Хочется услышать об этом мнения, может быть ссылки на известные реализации и статьи.
                                      0
                                      После каждого из них мы должны пересчитать бонусные баллы.

                                      мы должны пересчитать единыжды, так? после пересчета создать ивент "бонусы посчитаны" и при срабатывании другого ивента уже не делать этого.


                                      ну то есть, я может быть плохо понял задачу


                                      1) Нужно проверить проверить была ли ссылка на оплату реферальной, если была — пользователю «рефералу» зачислить бонусные баллы.

                                      на оплату или на товар? Ну то есть, вопрос весь в том каков жизненный цикл всего этого дела. А так подобный флоу описывается часто в контексте паттерна сага.

                                        0

                                        Согласен с Fesor. Очень трудно говорить не зная особенностей бизнеса.
                                        Например мне не понятно почему вы не можете пересчитывать бонусные баллы реферала дважды?
                                        Они пересчитываются по разным событиям и количество начисляемых баллов может, а скорей всего и будет, различаться. Также хорошо бы сохранить в истории начисления баллов за что их начисляли каждый раз. То есть опять же нужно сохранять их раздельно.


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

                              0

                              Добавлю и свою либу
                              https://github.com/gpslab/domain-event
                              бандл для интеграции с Symfony
                              https://github.com/gpslab/domain-event-bundle


                              Кроме стандартного агрегирования событий ещё позволяет обрабатывать события в самой сущности, имеет функционал для реализации слушателей, подписчиков, очередей и middleware

                                0
                                позволяет обрабатывать события в самой сущности

                                Это для воспроизведения событий? ну мол event sourcing?

                                  0

                                  Да. И не только. Например для соблюдения SRP, часть кода можно вынести в обработчик событий, но чтоб не размазывать бизнес логику по проекту, её можно сохранить в сущности.
                                  Например, тот же пример от Fantyk можно решить через обработку событий в сущности заказа.

                                    0

                                    Пересчет бонусов в сущности заказа?

                                      0

                                      Скорей: начисление бонусов рефералу за создание заказа.
                                      Пересчет делается в сущности реферала, а вот инициировать процедуру начисления бонусов можно через заказ.

                                        +1

                                        udidahan.com/2009/06/29/dont-create-aggregate-roots/ — вот тут описан схожий пример, к слову.


                                        инициировать процедуру начисления бонусов можно через заказ.

                                        У меня подобное подвешено на события от заказа, в частности когда заказ становится оплаченным.

                                          +1
                                          У меня подобное подвешено на события от заказа, в частности когда заказ становится оплаченным.

                                          Если событие обрабатывается в сущности заказа, как в моей библиотеке, то все гуд. Я это и предлагал.
                                          А вот если вы начисляете бонусы за оплаченный заказ в отдельном сервисе слушателе, то это не есть хорошо. Это пример размазывания бизнес логики по проекту, что часто встречается при использовании событийно-ориентированного подхода.

                                            0

                                            фактически ивент тригирит следующую "команду", начислить бонусы, которая относится уже к другому контексту. Так что вроде бы все хорошо.

                                              0

                                              Соглашусь. Если какое-то действие в сущности одного контекста должно стригерить действие в сущности другого контекста, то конечно логично делать это через доменные события.

                              +2
                              Symfony — это же event-based framework, кастомные ивенты для бизнес логики приложения вам в помощь: symfony.com/doc/current/event_dispatcher.html
                              Ну максимум в доктриновский листенер можно положить время изменения записи, но отправлку письма — это уже переборп.
                                –1
                                Вы также внутри EntityListener можете использовать обычные Event через EventDispatcher. Т.е. не реализовывать логику отсылки письма внутри Listener, а вынести ее во вне.
                                В статье подход упрощенный и акцент сделан не на конкретную реализацию, а на общий принцип.
                                +1
                                Помимо вышеупомянутых минусов есть еще проблема производительности. Событие будет вызываться при изменении/создании любой сущности, и для каждой вы будете делать instanceof.
                                  0

                                  и это будет весьма неплохой источник тупых багов.

                                    0
                                    Сейчас же есть entity_listener'ы они привязываются к конкретной сущности
                                    0
                                    Статья показывает, как делать нельзя.
                                    1) Lifecycle callbacks очень ресурсоемкие. По своему опыту могу сказать, что для highload проектов их вообще нельзя использовать.
                                    2) Испольование lifecycle callbacks для бизнес логики плохой тон.

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

                                      Пару лет назад я тоже так думал когда обжегся с примитивным использованием событий. Спустя какое-то время я пришел к тому, что "замена dispatch на вызов метода" приводит к усложнению управления зависимостями, усложнению тестирования. В ситуациях когда у вас не маленький проект и нужна глубокая кастомизация, это будет приводить либо к проблемам с масштабированием логики, либо к большому количеству медиаторов которые будут работать очень похожим на ивенты образом.


                                      Словом, я бы не стал так категорично говорить о том что "ивенты не нужны". Главный вопрос — как вы с ними работаете. Мне к примеру ни к чему отслеживать "цепочки событий". Мне важно найти причину и следствие, и это как раз таки весьма просто становится сделать. И еще проще — переместить причину (тригер операции какой-то) в виду изменений в требованиях.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое