Задать вопрос
@ComPUCKter
Web-разработчик

Почему когда подписываюсь на Doctrine.Events::preUpdate, он выполняется бесконечно?

У меня есть сущности Order и Notification. При изменении поля status сущности Order, я хочу создавать новое оповещение для клиента. Для этого я решил создать OrderEventSubscriber, который имплементит Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface и подписаться на Doctrine\ORM\Events::preUpdate.

Когда я создаю в preUpdate новое оповещение и сохраняю в БД через flush, preUpdate начинает вызываться бесконечно, хотя у меня в preUpdate идёт проверка изменённой сущности и если это не Order, я завершаю работу метода.

Код метода preUpdate:
public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getObject();

        if ($entity instanceof Order) {
            $this->logger->info($entity);

            $onlyStatusChanged = count($args->getEntityChangeSet()) === 1 && $args->hasChangedField('status');
            if ($onlyStatusChanged) {
                $this->notificationsCreator->createChangeStatusNotification($entity->getCustomer());
            }
        }
    }


Оповещение создаётся и флашится в createChangeStatusNotification
public function createChangeStatusNotification(User $recipient)
    {
        $notification = new Notification();
        $notification->setAction((new DataMapping())->getKeyByValue('notification_actions', 'order_status_changed'));
        $notification->setRecipient($recipient);
        $notification->setCreatedAt(new DateTimeImmutable('now', new DateTimeZone('Europe/Moscow')));

        $this->em->persist($notification);
        $this->em->flush();
    }
  • Вопрос задан
  • 129 просмотров
Подписаться 1 Простой 1 комментарий
Решения вопроса 1
Frostealth
@Frostealth
Backend Developer
Вызов flush() в процессе обработки preUpdate приводит к бесконечной рекурсии, ибо обработчик события вызывается в процессе выполнения предыдущего вызова flush().
Цепочка вложенных вызовов в вашем случае выглядит следующим образом:

- EntityManager::flush()
- UnitOfWork::commit()
- UnitOfWork::executeUpdates()
- ListenersInvoker::invoke()
- OrderEventSubscriber::preUpdate() - вызывается еще до обновления Order и его удаления из UnitOfWork
- $this->notificationsCreator->createChangeStatusNotification() - создает рекурсию вызовом `flush()`
- EntityManager::flush() - снова пытается обновить данные Order и вызывает обработку событий `preUpdate`
- ...
- EntityManager::flush()
- ...

Вызов методов persist(), remove() и т.п. в процессе обработки событий доктрины может привести к неожиданным результатам. Данные просто могут быть не сохранены, как минимум. Крайне не рекомендую менять какие-либо данные в Entity, EM/UoW при обработке событий доктрины.

В вашем случае можно реализовать свое событие, например OrderStatusChanged. Но лучше использовать более конкретные события вроде OrderCompleted и OrderCanceled.
И воспользоваться symfony/event-dispatcher.
Либо создавать уведомление непосредственно там, где выполняете изменение статуса заказа (контроллер, сервис и т.п., зависит от вашей архитектуры) и вызывается flush().
пример верного вызова

$this->em->flush();
$this->events->dispatch(new OrderCompleted($order->id));

// или
$this->em->flush();
$this->notificationCreator->createChangeStatusNotification($order->getCustomer());



P.S. Рекомендую использовать Guard Clauses для уменьшения вложенности и улучшения чтения кода.
пример

public function preUpdate(PreUpdateEventArgs $args)
{
    $entity = $args->getObject();
    if (!$entity instanceof Order) {
    	return;
    }

    $this->logger->info($entity);
    $onlyStatusChanged = count($args->getEntityChangeSet()) === 1 && $args->hasChangedField('status');
    if ($onlyStatusChanged) {
        $this->notificationsCreator->createChangeStatusNotification($entity->getCustomer());
    }
    // много кода...
}

Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы