教义的实体侦听器的行为不一致

时间:2018-09-13 20:15:59

标签: symfony doctrine entity listener symfony4

我正在使用Symfony 4和Doctrine创建一个小型应用程序。有用户(用户实体),并且他们拥有某种称为无线电表(RadioTable实体)的内容。广播表包含广播电台(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。另外,在创建,删除或更新广播表时,我需要更新广播表所有者(用户类)的$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的Security组件来支持登录机制。

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

要解决此问题,我需要与Doctrine的实体管理器进行手动交谈,并以用户实体作为参数调用flush()(没有参数,我就在无休止的循环中)。此行标有/* hack */注释。

在这个漫长的故事之后,我想问一个问题:为什么我必须这样做?为什么我必须为用户对象手动调用flush(),但是只有在使用安全组件并且用户已登录的情况下?

1 个答案:

答案 0 :(得分:0)

我解决了这个问题。

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

当我尝试在RadioTable的实体侦听器中更新User的上次活动日期时,在User实体中所做的更改将不会保留。这是因为在非常早期的阶段,安全组件从DB加载了我的User对象,然后然后 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查询。