min*_*llo 2 php events doctrine-orm
有没有办法在父关联实体上强制执行学说事件(如 preUpdate)?
例如:我有一个order具有一对多orderItem实体的实体。现在,我想在任何更改时对order实体或什至其中一个orderItem实体(我需要访问许多其他服务)进行大量检查和可能的orderItems更改。但是order当实体之一orderItem发生变化时,学说事件不会在实体上触发。
注意:这篇文章完全专注于preUpdate事件的特殊情况。可以使用事件管理器手动调度事件。问题在于,preUpdate如果该preUpdate方法修改了某些内容,那么仅触发实体的事件不足以将其新状态持久化到数据库中。
有多种方法可以做到这一点,但没有一种方法是真正直接的。仅考虑preUpdate事件的情况,我很难找到如何以干净的方式执行此操作,因为关联更新根本不是以处理Doctrine 文档中讨论的此类情况的方式构建的。
无论哪种方式,如果您想这样做,在我找到的解决方案中,有很多建议直接与UnitOfWorkDoctrine 混淆。这可能非常强大,但是您必须小心使用的内容以及何时使用它,因为在下面讨论的某些情况下,Doctrine 可能无法实际调度您想要的事件。
无论如何,我最终实现了一些利用父实体跟踪策略更改的东西。通过这样做,父实体preUpdate事件可以在其属性之一被修改或其“子”之一被修改时被触发。
如果您希望使用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 关联的实体(因此,子实体是拥有方的实体)协会)。
您可以尝试使用 onFlush 事件解决此问题,但是,在这种情况下,您必须按照文档中的建议处理 UnitOfWork 内部结构。在这种情况下,您不能在实体侦听器(在 2.4 中引入)中执行此操作,因为 onFlush 事件不在可能的回调中。可以在网上找到一些基于官方文档给出的示例。这是一个可能的实现:更新学说中的关联实体。
这里的主要缺点是您并没有真正触发preUpdate实体的事件,您只是在其他地方处理您想要的行为。这对我来说似乎有点过于沉重,所以我寻找其他解决方案。
实际触发preUpdate父实体事件的一种方法是向子实体添加另一个实体侦听器并使用 UnitOfWork。如前所述,您不能在preUpdate子实体的情况下简单地执行此操作。
为了正确计算提交顺序,我们需要在子实体侦听器的事件中调用scheduleForUpdate和propertyChanged,preFlush如下所示:
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调用所涉及的内部结构的一些重要说明:
children在这里工作的原因。preUpdate事件发生后重新计算。这种方法的主要问题是即使不需要父实体也会安排更新。由于没有直接的方法来判断子实体是否在其preFlush事件中发生了变化(您可以使用 UnitOfWork 但它的内部结构会变得有点多余),您将在每次刷新时触发父实体的 preUpdate 事件实体被管理。
此外,使用此解决方案,即使没有执行任何查询,Doctrine 也会开始事务并提交(例如,如果根本没有修改任何内容,您仍然会在 Symfony Profiler 中找到两个连续的条目“START TRANSACTION”和“COMMIT”学说日志)。
由于我一直在搞乱 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它将通知父实体的UnitOfWork该children字段已更改,从而干净地触发更新过程和preUpdate事件(如果指定了)。
与解决方案#2 不同,只有在某些内容发生更改时才会触发事件,并且您可以精确控制为什么应该将其标记为已更改。例如,如果仅更改了特定的一组属性,您可以将子项标记为已更改,而忽略其他更改,因为您可以完全控制最终通知给UnitOfWork.
笔记:
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)