Несогласованное поведение слушателя сущностей Doctrine

Я создаю небольшое приложение, используя Symfony 4 и Doctrine. Есть пользователи (сущности пользователей), и они владеют неким контентом, называемым радиотаблицами (сущность RadioTable). Радиотаблицы содержат радиостанции (объект RadioStation). RadioStation.radioTableId связан с RadioTable (многие к одному), а RadioTable.ownerId связан с пользователем (многие к одному).

Может, стоит заметить, что это мой первый проект с SF.

Сущности настраиваются с помощью аннотаций следующим образом:

<?php 

namespace App\Entity;

/**
 * @ORM\Entity(repositoryClass = "App\Repository\UserRepository")
 */
class User implements UserInterface, \Serializable, EncoderAwareInterface
{
    /**
     * @ORM\OneToMany(targetEntity = "App\Entity\RadioTable", mappedBy = "owner", orphanRemoval=true)
     */
    private $radioTables;

    /**
     * @ORM\Column(type = "date")
     */
    private $lastActivityDate;
}

// -----------------

namespace App\Entity;

/**
 * @ORM\Entity(repositoryClass = "App\Repository\RadioTableRepository")
 * @ORM\EntityListeners({"App\EventListener\RadioTableListener"})
 */
class RadioTable
{
    /**
     * @ORM\ManyToOne(targetEntity = "App\Entity\User", inversedBy = "radioTables")
     * @ORM\JoinColumn(nullable=false, onDelete = "cascade")
     */
    private $owner;

    /**
     * @ORM\Column(type = "datetime")
     */
    private $lastUpdateTime;
}

// -----------------

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass = "App\Repository\RadioStationRepository")
 * @ORM\EntityListeners({"App\EventListener\RadioStationListener"})
 */
class RadioStation
{
    /**
     * @ORM\ManyToOne(targetEntity = "App\Entity\RadioTable")
     * @ORM\JoinColumn(nullable=false, onDelete = "cascade")
     */
    private $radioTable;
}

Мне нужно обновить $lastUpdateTime в соответствующем объекте RadioTable при добавлении, удалении или изменении радиостанций. Кроме того, мне нужно обновить $lastActivityDate владельца таблицы радио (класс пользователя), когда таблица радио создается, удаляется или обновляется. Я пытаюсь добиться этого с помощью слушателей сущностей:

<?php

namespace App\EventListener;

class RadioStationListener
{
    /**
     * @PreFlush
     * @PreRemove
     */
    public function refreshLastUpdateTimeOfRadioTable(RadioStation $radioStation)
    {
        $radioStation->getRadioTable()->refreshLastUpdateTime();
    }
}

// -----------------------------

namespace App\EventListener;

class RadioTableListener
{
    /**
     * @PreFlush
     * @PreRemove
     */
    public function refreshLastActivityDateOfUser(RadioTable $radioTable, PreFlushEventArgs $args)
    {
        $radioTable->getOwner()->refreshLastActivityDate();

        /* hack */
        $args->getEntityManager()->flush($radioTable->getOwner());
        /* hack */
    }
}

(В методах refresh*() я просто создаю новый экземпляр \DateTime для правильного поля сущности.)

Я столкнулся с проблемой. Когда я пытался обновить / удалить / создать радиостанции, прослушиватель RadioStation работал правильно, и соответствующий класс RadioTable был успешно обновлен. Но когда я попытался обновить таблицу радио, класс User был обновлен, но Doctrine не сохранил его в базе данных.

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

Частично я нашел причину проблемы. Очевидно, что только владелец может изменять свои собственные таблицы радио, и пользователь должен войти в систему, чтобы изменить их. Я использую компонент безопасности от Symfony для поддержки механизма входа в систему.

Когда я временно взломал код контроллера, чтобы отключить безопасность, и попытался обновить таблицу радио как анонимную, прослушиватель объекта RadioTable работал правильно, а объект User был успешно изменен и сохраняется в базе данных.

Чтобы решить эту проблему, мне нужно вручную поговорить с диспетчером сущностей Doctrine и вызвать flush() с сущностью User в качестве аргумента (без аргументов я выполняю бесконечный цикл). Эта строка отмечена комментарием /* hack */.

После этой долгой истории я хочу задать вопрос: ПОЧЕМУ я должен это делать? ПОЧЕМУ мне нужно вручную вызывать flush() для объекта "Пользователь", но только если используется компонент безопасности и пользователь вошел в систему?

Было бы полезно увидеть ваши функции refresh*().

ehymel 14.09.2018 01:47

Я подозреваю, что разница заключается в том, что вы вызываете их в середине события жизненного цикла на уровне сущности (а не в слушателе, например). мог бы означает, что ваша RadioStation обрабатывается до вашей RadioTable, так что изменения в RadioTable затем сохраняются, тогда как User обрабатывается до RadioStation, поэтому после обработки RadioStation (с уже обработанным пользователем) вы должны вручную вернуться к сохранению User. Просто догадка. Отладка / трассировка помогут ответить.

ehymel 14.09.2018 05:17
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Symfony Station Communiqué - 17 февраля 2023 г
Symfony Station Communiqué - 17 февраля 2023 г
Это коммюнике первоначально появилось на Symfony Station , вашем источнике передовых новостей Symfony, PHP и кибербезопасности.
Управление ответами api для исключений на Symfony с помощью KernelEvents
Управление ответами api для исключений на Symfony с помощью KernelEvents
Много раз при создании api нам нужно возвращать клиентам разные ответы в зависимости от возникшего исключения.
0
2
298
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я решил проблему.

Doctrine обрабатывает сущности в указанном порядке. Во-первых, вновь созданные объекты (запланированные для INSERT) имеют приоритет. Затем сохраненные сущности (запланированные для ОБНОВЛЕНИЯ) обрабатываются в том же порядке, в котором они были извлечены из базы данных. Изнутри прослушивателя сущностей я не могу предсказать или применить предпочтительный порядок.

Когда я пытаюсь обновить дату последней активности пользователя внутри слушателя сущности RadioTable, изменения, внесенные в сущность пользователя, не сохраняются. Это потому, что на очень ранней стадии компонент безопасности загружает мой объект User из БД, а тогда Symfony подготавливает объект RadioTable для контроллера (например, с помощью преобразователя параметров).

Чтобы решить эту проблему, мне нужно сказать Doctrine, чтобы она пересчитала набор изменений сущности User. Вот что я сделал.

Я создал небольшую черту для моих слушателей сущностей:

<?php

namespace App\EventListener\EntityListener;

use Doctrine\Common\EventArgs;

trait EntityListenerTrait
{
    // There is need to manually enforce update of associated entities,
    // for example when User entity is modified inside RadioTable entity event.
    // It's because associations are not tracked consistently inside Doctrine's events.
    private function forceEntityUpdate(object $entity, EventArgs $args): void
    {
        $entityManager = $args->getEntityManager();

        $entityManager->getUnitOfWork()->recomputeSingleEntityChangeSet(
            $entityManager->getClassMetadata(get_class($entity)),
            $entity
        );
    }
}

Внутри слушателей сущностей я делаю следующее:

<?php

namespace App\EventListener\EntityListener;

use App\Entity\RadioTable;
use Doctrine\Common\EventArgs;
use Doctrine\ORM\Mapping\PreFlush;
use Doctrine\ORM\Mapping\PreRemove;

class RadioTableListener
{
    use EntityListenerTrait;

    /**
     * @PreFlush
     * @PreRemove
     */
    public function refreshLastActivityDateOfUser(RadioTable $radioTable, EventArgs $args): void
    {
        $user = $radioTable->getOwner();

        $user->refreshLastActivityDate();
        $this->forceEntityUpdate($user, $args);
    }
}

Есть другое решение. Можно вызвать $entityManager->flush($user), но он работает правильно только для UPDATE, генерирует бесконечный цикл для INSERT. Чтобы избежать бесконечного зацикливания, можно проверить $unitOfWork->isScheduledForInsert($radioTable).

Это решение хуже, потому что оно генерирует дополнительные транзакции и SQL-запросы.

Другие вопросы по теме