Doctrine 实体监听器的不一致行为

Inconsistent behavior of Doctrine's entity listener

我正在使用 Symfony 4 和 Doctrine 创建一个小应用程序。有用户(用户实体),他们拥有某种称为 radio tables(RadioTable 实体)的内容。 Radio table 包含无线电台(RadioStation 实体)。 RadioStation.radioTableId 与 RadioTable(多对一)相关,RadioTable.ownerId 与 User(多对一)相关。

也许我应该注意到这是我与 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;
}

当添加、删除或修改无线电台时,我需要在适当的 RadioTable 实体中更新 $lastUpdateTime。此外,当创建、删除或更新无线电 table 时,我需要更新无线电 table 所有者(用户 class)的 $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 的新实例。)

我遇到了问题。当我尝试 update/remove/create 广播电台时,RadioStation 侦听器工作正常并且相关的 RadioTable class 已成功更新。但是当我尝试更新 radio table 时,用户 class 被更新但没有被 Doctrine 持久化到数据库中。

我很困惑,因为这些实体侦听器中的代码结构非常相似。

部分找到了问题的原因。很明显,只有所有者可以修改自己的无线电 tables 并且用户必须登录才能修改它们。我正在使用 Symfony 的安全组件来支持登录机制。

当我临时破解控制器代码以禁用安全性并尝试将无线电 table 更新为匿名时,RadioTable 实体侦听器正常工作并且用户实体已成功修改 并持久保存到数据库

为了解决这个问题,我需要手动与 Doctrine 的实体管理器交谈,并以用户实体作为参数调用 flush()(没有参数我正在做无限循环)。此行由 /* hack */ 注释标记。

在这个looong故事之后,我想问一个问题:为什么我必须这样做?为什么我必须为用户对象手动调用 flush(),但前提是使用了安全组件并且用户已登录?

我解决了这个问题。

Doctrine 以指定顺序处理实体。首先,新创建的实体(计划用于 INSERT)具有优先权。接下来,持久化实体(计划更新)按照从数据库中获取的相同顺序进行处理。从实体侦听器内部,我无法预测或执行首选顺序。

当我尝试在 RadioTable 的实体侦听器中更新用户的最后 activity 日期时,在用户实体中所做的更改不会保留。这是因为在很早的阶段,安全组件从数据库加载我的用户对象,然后 然后 Symfony 为控制器准备 RadioTable 对象(例如通过参数转换器)。

为了解决这个问题,我需要告诉 Doctrine 重新计算用户实体变更集。这是我所做的。

我为我的实体侦听器创建了小特征:

<?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 查询。