Я создаю небольшое приложение, используя 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() для объекта "Пользователь", но только если используется компонент безопасности и пользователь вошел в систему?
Я подозреваю, что разница заключается в том, что вы вызываете их в середине события жизненного цикла на уровне сущности (а не в слушателе, например). мог бы означает, что ваша RadioStation обрабатывается до вашей RadioTable, так что изменения в RadioTable затем сохраняются, тогда как User обрабатывается до RadioStation, поэтому после обработки RadioStation (с уже обработанным пользователем) вы должны вручную вернуться к сохранению User. Просто догадка. Отладка / трассировка помогут ответить.




Я решил проблему.
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-запросы.
Было бы полезно увидеть ваши функции
refresh*().