父实体上的教义强制事件?

min*_*llo 2 php events doctrine-orm

有没有办法在父关联实体上强制执行学说事件(如 preUpdate)?

例如:我有一个order具有一对多orderItem实体的实体。现在,我想在任何更改时对order实体或什至其中一个orderItem实体(我需要访问许多其他服务)进行大量检查和可能的orderItems更改。但是order当实体之一orderItem发生变化时,学说事件不会在实体上触发。

Ala*_* T. 6

注意:这篇文章完全专注于preUpdate事件的特殊情况。可以使用事件管理器手动调度事件。问题在于,preUpdate如果该preUpdate方法修改了某些内容,那么仅触发实体的事件不足以将其新状态持久化到数据库中。

有多种方法可以做到这一点,但没有一种方法是真正直接的。仅考虑preUpdate事件的情况,我很难找到如何以干净的方式执行此操作,因为关联更新根本不是以处理Doctrine 文档中讨论的此类情况的方式构建的。

无论哪种方式,如果您想这样做,在我找到的解决方案中,有很多建议直接与UnitOfWorkDoctrine 混淆。这可能非常强大,但是您必须小心使用的内容以及何时使用它,因为在下面讨论的某些情况下,Doctrine 可能无法实际调度您想要的事件。

无论如何,我最终实现了一些利用父实体跟踪策略更改的东西。通过这样做,父实体preUpdate事件可以在其属性之一被修改或其“子”之一被修改时被触发。


UnitOfWork 的主要问题

如果您希望使用UnitOfWork(您可以通过$args->getEntityManager()->getUnitOfWork()与生命周期事件的任何类型的参数一起使用来检索),您可以使用公共方法scheduleForUpdate(object $entity)。但是,如果您希望使用此方法,则需要工作单元内计算提交顺序之前调用它。此外,如果你有一个与你计划更新的实体相关联的 preUpdate 事件,如果你的实体有一个空的更改集它会引发错误(这正是我们正在处理的情况,当主实体没有被修改但其相关实体是)。

因此$unitOfWork->scheduleForUpdate($myParentEntity),在preUpdate子实体的a中调用,不是文档中解释的选项(强烈建议不要执行对 UnitOfWork API 的调用,因为它不像在刷新操作之外那样工作)。

应该注意的是,$unitOfWork->scheduleExtraUpdate($parentEntity, array $changeset)可以在该特定上下文中使用,但此方法被标记为“INTERNAL”。以下解决方案避免使用它,但如果您知道自己在做什么,这可能是一个很好的方法。


可能的解决方案

注意:我没有使用 onFlush 事件测试所需行为的实现,但它通常被认为是最强大的方法。对于此处列出的其他两种可能性,我使用 OneToMany 关联成功地尝试了它们。

在下一节中,当我谈论父实体时,我指的是具有 OneToMany 关联的实体,而子实体指的是具有 ManyToOne 关联的实体(因此,子实体是拥有方的实体)协会)。

1. 使用 onFlush 事件

您可以尝试使用 onFlush 事件解决此问题,但是,在这种情况下,您必须按照文档中的建议处理 UnitOfWork 内部结构。在这种情况下,您不能在实体侦听器(在 2.4 中引入)中执行此操作,因为 onFlush 事件不在可能的回调中。可以在网上找到一些基于官方文档给出的示例。这是一个可能的实现:更新学说中的关联实体

这里的主要缺点是您并没有真正触发preUpdate实体的事件,您只是在其他地方处理您想要的行为。这对我来说似乎有点过于沉重,所以我寻找其他解决方案。

2.在子实体的preFlush事件中使用UnitOfWork

实际触发preUpdate父实体事件的一种方法是向子实体添加另一个实体侦听器并使用 UnitOfWork。如前所述,您不能在preUpdate子实体的情况下简单地执行此操作。

为了正确计算提交顺序,我们需要在子实体侦听器的事件中调用scheduleForUpdatepropertyChangedpreFlush如下所示:

class ChildListener
{   
    public function preFlush(Child $child, PreFlushEventArgs $args)
    {
        $uow = $args->getEntityManager()->getUnitOfWork();
        // Add an entry to the change set of the parent so that the PreUpdateEventArgs can be constructed without errors
        $uow->propertyChanged($child->getParent(), 'children', 0, 1);
        // Schedule for update the parent entity so that the preUpdate event can be triggered
        $uow->scheduleForUpdate($child->getParent());
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,我们需要通知 UnitOfWork 某个属性已更改,以便一切正常工作。它看起来有点草率,但它完成了工作。

重要的部分是我们将children属性(父实体的 OneToMany 关联)标记为已更改,以便父实体的更改集不为空。关于此propertyChanged调用所涉及的内部结构的一些重要说明:

  • 该方法需要一个持久字段名称(非持久字段将被忽略),任何映射字段都可以,甚至是关联,这就是 usingchildren在这里工作的原因。
  • 连续修改到此调用的更改集在此处没有任何副作用,因为它将preUpdate事件发生后重新计算

这种方法的主要问题是即使不需要父实体也会安排更新。由于没有直接的方法来判断子实体是否在其preFlush事件中发生了变化(您可以使用 UnitOfWork 但它的内部结构会变得有点多余),您将在每次刷新时触发父实体的 preUpdate 事件实体被管理。

此外,使用此解决方案,即使没有执行任何查询,Doctrine 也会开始事务并提交(例如,如果根本没有修改任何内容,您仍然会在 Symfony Profiler 中找到两个连续的条目“START TRANSACTION”和“COMMIT”学说日志)。

3.更改父级跟踪策略,明确处理行为

由于我一直在搞乱 UnitOfWork 的内部结构,因此我偶然发现了该propertyChanged方法(在之前的解决方案中使用过)并注意到它是 interface 的一部分PropertyChangedListener。碰巧这与一个记录在案的主题有关:跟踪策略。默认情况下,您可以让 Doctrine 检测更改,但您也可以更改此策略并手动管理所有内容,如文档中所述

读完这篇文章后,我最终想出了以下解决方案,可以干净地处理所需的行为,代价是您必须在实体中做一些额外的工作。

因此,为了得到我想要的东西,我的父实体遵循NOTIFY跟踪策略,当他们的属性之一被修改时,子实体会通知父实体。如官方文档中所述,您必须实现该NotifyPropertyChanged接口,然后将属性更改通知给侦听器(UnitOfWork如果检测到某个托管实体实现了该接口,它会自动将自己添加到侦听器中)。之后,如果添加了 @ChangeTrackingPolicy 注释,在提交时,Doctrine 将依赖于通过propertyChanged调用构建的更改集,而不是自动检测。

以下是对基本父实体执行此操作的方法:

namespace AppBundle\Entity;

use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\PropertyChangedListener;

/**
 * ... other annotations ...
 * @ORM\EntityListeners({"AppBundle\Listener\ParentListener"}) 
 * @ORM\ChangeTrackingPolicy("NOTIFY")
 */
class Parent implements NotifyPropertyChanged
{
    // Add the implementation satisfying the NotifyPropertyChanged interface
    use \AppBundle\Doctrine\Traits\NotifyPropertyChangedTrait;

    /* ... other properties ... */

    /**
     * @ORM\Column(name="basic_property", type="string")
     */
    private $basicProperty;

    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\Child", mappedBy="parent", cascade={"persist", "remove"})
     */
    private $children;

    /**
     * @ORM\Column(name="other_field", type="string")
     */
    private $otherField;

    public function __construct()
    {
        $this->children = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function notifyChildChanged()
    {
        $this->onPropertyChanged('children', 0, 1);
    }

    public function setBasicProperty($value)
    {
        if($this->basicProperty != $value)
        {
            $this->onPropertyChanged('basicProperty', $this->basicProperty, $value);
            $this->basicProperty = $value;
        }
    }

    public function addChild(Child $child)
    {
        $this->notifyChildChanged();
        $this->children[] = $child;
        $child->setParent($this);
        return $this;
    }

    public function removeChild(Child $child)
    {
        $this->notifyChildChanged();
        $this->children->removeElement($child);
    }

    /* ... other methods ... */
}
Run Code Online (Sandbox Code Playgroud)

从文档中给出的代码中获取特征:

namespace AppBundle\Doctrine\Traits;

use Doctrine\Common\PropertyChangedListener;

trait NotifyPropertyChangedTrait
{
    private $listeners = [];

    public function addPropertyChangedListener(PropertyChangedListener $listener) 
    {
        $this->listeners[] = $listener;
    }

    /** Notifies listeners of a change. */
    private function onPropertyChanged($propName, $oldValue, $newValue) 
    {
        if ($this->listeners) 
        {
            foreach ($this->listeners as $listener) 
            {
                $listener->propertyChanged($this, $propName, $oldValue, $newValue);
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

以及以下拥有关联方的子实体:

namespace AppBundle\Entity;

class Child
{

    /* .. other properties .. */

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Parent", inversedBy="children")
     */
    private $parentEntity;

    /**
     * @ORM\Column(name="attribute", type="string")
     */
    private $attribute;

    public function setAttribute($attribute)
    {
        // Check if the parentEntity is not null to handle the case where the child entity is created before being attached to its parent
        if($this->attribute != $attribute && $this->parentEntity)
        {
            $this->parentEntity->notifyChildChanged();
            $this->attribute = $attribute;
        }
    }

    /* ... other methods ... */
}
Run Code Online (Sandbox Code Playgroud)

就是这样,你让一切都正常工作。如果您的子实体被修改,您显式调用notifyChildChanged它将通知父实体的UnitOfWorkchildren字段已更改,从而干净地触发更新过程和preUpdate事件(如果指定了)。

与解决方案#2 不同,只有在某些内容发生更改时才会触发事件,并且您可以精确控制为什么应该将其标记为已更改。例如,如果仅更改了特定的一组属性,您可以将子项标记为已更改,而忽略其他更改,因为您可以完全控制最终通知给UnitOfWork.

笔记:

  • 显然,使用 NOTIFY 跟踪策略,preFlush不会在 Parent 实体侦听器中触发事件(在 computeChangeSet中触发的 preFlush 事件根本不会为使用此策略的实体调用)。
  • 如果正常属性发生更改,则有必要跟踪每个“正常”属性以触发更新。一种无需修改所有设置器即可执行此操作的解决方案是使用魔术调用,如下所示。
  • children在更改集中设置条目是安全的,因为在创建更新查询时它会被简单地忽略因为父实体不是关联的拥有方。(即它没有任何外键)

使用魔术调用轻松处理通知

在我的应用程序中,我添加了以下特征

namespace AppBundle\Utils\Traits;

trait MagicSettersTrait
{
    /** Returns an array with the names of properties for which magic setters can be used */
    abstract protected function getMagicSetters();

    /** Override if needed in the class using this trait to perform actions before set operations */
    private function _preSetCallback($property, $newValue) {}
    /** Override if needed in the class using this trait to perform actions after set operations */
    private function _postSetCallback($property, $newValue) {}

    /** Returns true if the method name starts by "set" */
    private function isSetterMethodCall($name)
    {
        return substr($name, 0, 3) == 'set';
    }

    /** Can be overriden by the class using this trait to allow other magic calls */
    public function __call($name, array $args)
    {
        $this->handleSetterMethodCall($name, $args);
    }

    /**
     * @param string $name Name of the method being called
     * @param array  $args Arguments passed to the method
     * @throws BadMethodCallException if the setter is not handled or if the number of arguments is not 1
     */
    private function handleSetterMethodCall($name, array $args)
    {
        $property = lcfirst(substr($name, 3));
        if(!$this->isSetterMethodCall($name) || !in_array($property, $this->getMagicSetters()))
        {
            throw new \BadMethodCallException('Undefined method ' . $name . ' for class ' . get_class($this));
        }

        if(count($args) != 1)
        {
            throw new \BadMethodCallException('Method ' . $name . ' expects 1 argument (' . count($args) . ' given)');;
        }

        $this->_preSetCallback($property, $args[0]);
        $this->$property = $args[0];
        $this->_postSetCallback($property, $args[0]);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我可以在我的实体中使用它。这是我的 Tag 实体的示例,preUpdate当修改其别名之一时,需要调用其事件:

/**
 * @ORM\Table(name="tag")
 * @ORM\EntityListeners({"AppBundle\Listener\Tag\TagListener"}) 
 * @ORM\ChangeTrackingPolicy("NOTIFY")
 */
class Tag implements NotifyPropertyChanged
{
    use \AppBundle\Doctrine\Traits\NotifyPropertyChangedTrait;
    use \AppBundle\Utils\Traits\MagicSettersTrait;

    /* ... attributes ... */

    protected function getMagicSetters() { return ['slug', 'reviewed', 'translations']; }

    /** Called before the actuel set operation in the magic setters */
    public function _preSetCallback($property, $newValue)
    {
        if($this->$property != $newValue)
        {
            $this->onPropertyChanged($property, $this->$property, $newValue);
        }
    }

    public function notifyAliasChanged()
    {
        $this->onPropertyChanged('aliases', 0, 1);
    }

    /* ... methods ... */

    public function addAlias(\AppBundle\Entity\Tag\TagAlias $alias)
    {
        $this->notifyAliasChanged();
        $this->aliases[] = $alias;
        $alias->setTag($this);
        return $this;
    }

    public function removeAlias(\AppBundle\Entity\Tag\TagAlias $alias)
    {
        $this->notifyAliasChanged();
        $this->aliases->removeElement($alias);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,我可以在名为 TagAlias 的“子”实体中重用相同的特征:

class TagAlias
{
    use \AppBundle\Utils\Traits\MagicSettersTrait;

    /* ... attributes ... */

    public function getMagicSetters() { return ['alias', 'main', 'locale']; }

    /** Called before the actuel set operation in the magic setters */
    protected function _preSetCallback($property, $newValue)
    {
        if($this->$property != $newValue && $this->tag)
        {
            $this->tag->notifyAliasChanged();
        }
    }

    /* ... methods ... */
}
Run Code Online (Sandbox Code Playgroud)

注意:如果您选择这样做,当表单尝试对您的实体进行水合时,您可能会遇到错误,因为默认情况下禁用魔法调用。只需将以下内容添加到您的services.yml即可启用魔术调用。(取自本次讨论

property_accessor:
    class: %property_accessor.class%
    arguments: [true]
Run Code Online (Sandbox Code Playgroud)