有没有办法在父关联实体上强制一个教义事件(如preUpdate)?
例如:我有一个具有一对多orderItem
实体的order
实体。 现在,每当任何orderItems
发生变化时,我想对order
实体甚至其中一个实体orderItem
实体(我需要访问许多其他服务)进行一堆检查和可能的更改。但是,当order
实体之一发生变化时,教义事件不会在orderItem
实体上触发。
注意:这篇文章完全关注preUpdate
事件的特殊情况。可以使用事件管理器手动调度事件。问题在于,如果preUpdate
方法修改了某些内容,则仅触发实体的preUpdate
事件不足以将其新状态保存到数据库中。
有多种方法可以做到这一点,但没有一种是真正简单的。仅考虑preUpdate
事件的情况,我遇到了很多麻烦,无法找到如何以干净的方式执行此操作,因为关联更新根本不是以处理 Doctrine 文档中讨论的此类情况的方式构建的
。无论哪种方式,如果你想这样做,在我找到的解决方案中,有很多建议直接搞砸教义UnitOfWork
。这可能非常强大,但你必须小心你使用的东西,当你使用它时,在下面讨论的某些情况下,Doctrine 可能无法实际调度你想要的事件。
无论如何,我最终实现了一些利用父实体跟踪策略更改的东西。通过这样做,如果父实体preUpdate
事件的一个属性被修改或其中一个"子"被修改,则可以触发该事件的父实体。
工作单元的主要关注点
如果您希望使用UnitOfWork
(可以通过将$args->getEntityManager()->getUnitOfWork()
与任何类型的生命周期事件参数一起使用来检索),则可以使用公共方法scheduleForUpdate(object $entity)
。但是,如果您希望使用此方法,则需要在工作单元内计算提交顺序之前调用它。此外,如果您有一个与您计划更新的实体关联的 preUpdate 事件,则如果您的实体具有空的更改集,它将引发错误(这正是我们正在处理的主实体未被修改但其中一个相关实体被修改的情况)。
因此,在子实体的preUpdate
中调用$unitOfWork->scheduleForUpdate($myParentEntity)
不是文档中解释的选项(强烈建议不要执行对 UnitOfWork API 的调用,因为它不像在刷新操作之外那样工作)。
应该注意的是,$unitOfWork->scheduleExtraUpdate($parentEntity, array $changeset)
可以在该特定上下文中使用,但此方法标记为"内部"。以下解决方案避免使用它,但如果您知道自己正在进入什么,这可能是一个好方法。
可能的解决方案
注意:我没有使用 onFlush 事件测试所需行为的实现,但它通常被认为是最强大的方法。对于此处列出的其他两种可能性,我通过OneToMany关联成功尝试了它们。
在下一节中,当我谈论父实体时,我指的是具有 OneToMany 关联的实体,而子实体指的是具有 ManyToOne 关联的实体(因此,子实体是关联的拥有方)。
1. 使用 onFlush 事件
您可以尝试使用 onFlush 事件来解决此问题,但是,在这种情况下,您必须按照文档中的建议处理 UnitOfWork 内部。在这种情况下,您无法在实体侦听器(在 2.4 中引入)中执行此操作,因为 onFlush 事件不在可能的回调之列。一些基于官方文档给出的示例可以在网络上找到。下面是一个可能的实现:在原则中更新关联实体。
这里的主要缺点是你并没有真正触发你的实体的preUpdate
事件,你只是在其他地方处理你想要的行为。这对我来说似乎有点太重了,所以我寻找其他解决方案。
2. 在子实体的预刷新事件中使用工作单元
实际触发父实体的preUpdate
事件的一种方法是将另一个实体侦听器添加到子实体并使用 UnitOfWork。如前所述,不能简单地在子实体的preUpdate
事件中执行此操作。
为了正确计算提交顺序,我们需要在子实体侦听器的preFlush
事件中调用scheduleForUpdate
和propertyChanged
,如下所示:
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());
}
}
如您所见,我们需要通知 UnitOfWork 属性已更改,以便一切正常。它看起来有点草率,但它可以完成工作。
重要的部分是我们将children
属性(父实体的 OneToMany 关联)标记为已更改,以便父实体的更改集不为空。关于此propertyChanged
调用所涉及的内部的一些重要说明:
- 该方法需要一个持久字段名称(非持久字段将被忽略),任何映射的字段都可以,甚至是关联,这就是使用
children
在这里工作的原因。
连续 - 修改到此调用的更改集在这里没有任何副作用,因为它将在
preUpdate
事件后重新计算。
此方法的主要问题是,即使不需要父实体,也会计划更新。由于没有直接的方法可以判断子实体是否在其preFlush
事件中发生了更改(您可以使用 UnitOfWork,但它的内部会变得有点多余),因此您将在每次管理子实体的刷新时触发父实体的 preUpdate 事件。
此外,通过这个解决方案,Doctrine将开始一个事务并提交,即使没有执行任何查询(例如,如果根本没有修改任何内容,你仍然可以在Symfony Profiler中找到两个连续的条目"START TRANSACTION"和"COMMIT"在Doctrine日志中)。
3. 更改家长的跟踪政策并明确处理行为
由于我一直在弄乱 UnitOfWork 的内部结构,所以我偶然发现了propertyChanged
方法(在以前的解决方案中使用),并注意到它是接口PropertyChangedListener
的一部分。碰巧这链接到一个记录在案的主题:跟踪策略。默认情况下,您可以只让 Doctrine 检测更改,但您也可以更改此策略并手动管理所有内容,如文档中的此处所述。
在阅读了这篇文章之后,我最终提出了以下解决方案,可以干净地处理所需的行为,成本是您必须在实体中做一些额外的工作。
因此,为了获得我想要的,我的父实体遵循NOTIFY
跟踪策略,子实体在其属性之一被修改时通知父实体。如官方文档中所述,您必须实现NotifyPropertyChanged
接口,然后将属性更改通知侦听器(如果UnitOfWork
检测到其中一个托管实体实现了该接口,则会自动将自身添加到侦听器中)。之后,如果添加了注释@ChangeTrackingPolicy,则在提交时,Doctrine 将依赖于通过propertyChanged
调用构建的更改集,而不是自动检测。
以下是对基本父实体执行此操作的方法:
namespace AppBundleEntity;
use DoctrineCommonNotifyPropertyChanged;
use DoctrineCommonPropertyChangedListener;
/**
* ... other annotations ...
* @ORMEntityListeners({"AppBundleListenerParentListener"})
* @ORMChangeTrackingPolicy("NOTIFY")
*/
class Parent implements NotifyPropertyChanged
{
// Add the implementation satisfying the NotifyPropertyChanged interface
use AppBundleDoctrineTraitsNotifyPropertyChangedTrait;
/* ... other properties ... */
/**
* @ORMColumn(name="basic_property", type="string")
*/
private $basicProperty;
/**
* @ORMOneToMany(targetEntity="AppBundleEntityChild", mappedBy="parent", cascade={"persist", "remove"})
*/
private $children;
/**
* @ORMColumn(name="other_field", type="string")
*/
private $otherField;
public function __construct()
{
$this->children = new DoctrineCommonCollectionsArrayCollection();
}
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 ... */
}
特征取自文档中给出的代码:
namespace AppBundleDoctrineTraits;
use DoctrineCommonPropertyChangedListener;
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);
}
}
}
}
以及具有关联拥有方的以下子实体:
namespace AppBundleEntity;
class Child
{
/* .. other properties .. */
/**
* @ORMManyToOne(targetEntity="AppBundleEntityParent", inversedBy="children")
*/
private $parentEntity;
/**
* @ORMColumn(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 ... */
}
就这样,您拥有一切功能。如果修改了子实体,则显式调用notifyChildChanged
,然后通知UnitOfWork
父实体的字段已更改children
从而干净地触发更新过程和preUpdate
事件(如果指定了)。
与解决方案 #2 不同,仅当某些内容发生更改时才会触发事件,您可以精确控制为什么应将其标记为已更改。例如,如果仅更改了一组特定的属性,则可以将子项标记为已更改,并忽略其他更改,因为您可以完全控制最终通知UnitOfWork
的其他内容。
注意:
- 显然,使用 NOTIFY 跟踪策略时,
preFlush
事件不会在父实体侦听器中触发(在 computeChangeSet 中触发的 preFlush 事件,对于使用此策略的实体根本不调用该事件)。 - 有必要跟踪每个"正常"属性,以便在正常属性发生更改时触发更新。无需修改所有二传手即可执行此操作的一种解决方案是使用魔术调用,如下所示。
- 在更改集中设置
children
条目是安全的,因为在创建更新查询时,该条目将被简单地忽略,因为父实体不是关联的拥有方。(即它没有任何外键)
使用魔术调用轻松处理通知
在我的应用程序中,我添加了以下特征
namespace AppBundleUtilsTraits;
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]);
}
}
然后我可以在我的实体中使用。下面是我的 Tag 实体的示例,当修改其别名之一时,需要调用其preUpdate
事件:
/**
* @ORMTable(name="tag")
* @ORMEntityListeners({"AppBundleListenerTagTagListener"})
* @ORMChangeTrackingPolicy("NOTIFY")
*/
class Tag implements NotifyPropertyChanged
{
use AppBundleDoctrineTraitsNotifyPropertyChangedTrait;
use AppBundleUtilsTraitsMagicSettersTrait;
/* ... 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(AppBundleEntityTagTagAlias $alias)
{
$this->notifyAliasChanged();
$this->aliases[] = $alias;
$alias->setTag($this);
return $this;
}
public function removeAlias(AppBundleEntityTagTagAlias $alias)
{
$this->notifyAliasChanged();
$this->aliases->removeElement($alias);
}
}
然后,我可以在名为 TagAlias 的"子"实体中重用相同的特征:
class TagAlias
{
use AppBundleUtilsTraitsMagicSettersTrait;
/* ... 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 ... */
}
注意:如果您选择这样做,则在表单尝试冻结实体时可能会遇到错误,因为默认情况下禁用魔术调用。只需将以下内容添加到您的services.yml
即可启用魔术调用。(摘自本次讨论)
property_accessor:
class: %property_accessor.class%
arguments: [true]
更实用的方法是对父实体进行版本控制。一个简单的示例是在修改子实体集合时更新的时间戳(例如 updated_at)。这假定您通过其父实体更新所有子实体。