假设我检索一个实体$e
并使用setters修改其状态:
$e->setFoo('a');
$e->setBar('b');
是否有可能检索已更改的字段数组?
在我的示例中,我想检索foo => a, bar => b
作为结果
PS:是的,我知道我可以修改所有访问器并手动实现此功能,但我正在寻找一些方便的方法
Doctrine'ORM'EntityManager#getUnitOfWork
得到一个Doctrine'ORM'UnitOfWork
.
然后只需通过 Doctrine'ORM'UnitOfWork#computeChangeSets()
触发变更集计算(仅适用于托管实体(。
您也可以使用类似的方法,例如Doctrine'ORM'UnitOfWork#recomputeSingleEntityChangeSet(Doctrine'ORM'ClassMetadata $meta, $entity)
如果您确切地知道要检查的内容,而无需迭代整个对象图。
之后,您可以使用Doctrine'ORM'UnitOfWork#getEntityChangeSet($entity)
检索对对象的所有更改。
把它放在一起:
$entity = $em->find('My'Entity', 1);
$entity->setTitle('Changed Title!');
$uow = $em->getUnitOfWork();
$uow->computeChangeSets(); // do not compute changes if inside a listener
$changeset = $uow->getEntityChangeSet($entity);
注意。如果尝试在 preUpdate 侦听器中获取更新的字段,请不要重新计算更改集,因为它已经完成。只需调用 getEntityChangeSet 即可获取对实体所做的所有更改。
警告:如注释中所述,此解决方案不应在 Doctrine 事件侦听器之外使用。这将打破教义的行为。
检查这个公共(而不是内部(函数:
$this->em->getUnitOfWork()->getOriginalEntityData($entity);
从教义回购:
/**
* Gets the original data of an entity. The original data is the data that was
* present at the time the entity was reconstituted from the database.
*
* @param object $entity
*
* @return array
*/
public function getOriginalEntityData($entity)
您所要做的就是在您的实体中实现一个toArray
或serialize
函数并进行差异。 像这样:
$originalData = $em->getUnitOfWork()->getOriginalEntityData($entity);
$toArrayEntity = $entity->toArray();
$changes = array_diff_assoc($toArrayEntity, $originalData);
对于那些想要使用上述方法检查实体更改的人来说,要大当心标志。
$uow = $em->getUnitOfWork();
$uow->computeChangeSets();
持久例程在内部使用 $uow->computeChangeSets()
方法,使上述解决方案不可用。这也是该方法的注释中写的内容:@internal Don't call from the outside
.使用 $uow->computeChangeSets()
检查对实体的更改后,在方法结束时执行以下代码段(每个托管实体(:
if ($changeSet) {
$this->entityChangeSets[$oid] = $changeSet;
$this->originalEntityData[$oid] = $actualData;
$this->entityUpdates[$oid] = $entity;
}
$actualData
数组保存对实体属性的当前更改。一旦这些被写入$this->originalEntityData[$oid]
,这些尚未持久化的更改就被认为是实体的原始属性。
稍后,当调用 $em->persist($entity)
以保存对实体的更改时,它还涉及方法 $uow->computeChangeSets()
,但现在它将无法找到对实体的更改,因为这些尚未持久化的更改被视为实体的原始属性。
它将返回更改
$entityManager->getUnitOfWork()->getEntityChangeSet($entity)
可以使用通知策略跟踪更改。
首先,实现 NotifyPropertyChanged 接口:
/**
* @Entity
* @ChangeTrackingPolicy("NOTIFY")
*/
class MyEntity implements NotifyPropertyChanged
{
// ...
private $_listeners = array();
public function addPropertyChangedListener(PropertyChangedListener $listener)
{
$this->_listeners[] = $listener;
}
}
然后,只需在更改数据抛出实体的每个方法上调用 _onPropertyChanged,如下所示:
class MyEntity implements NotifyPropertyChanged
{
// ...
protected function _onPropertyChanged($propName, $oldValue, $newValue)
{
if ($this->_listeners) {
foreach ($this->_listeners as $listener) {
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
}
}
}
public function setData($data)
{
if ($data != $this->data) {
$this->_onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
}
那么......当我们想在Doctrine生命周期之外找到一个变更集时该怎么办?正如我在上面对@Ocramius帖子的评论中提到的,也许可以创建一个"只读"方法,它不会扰乱实际的 Doctrine 持久性,但让用户了解已更改的内容。
这是我正在考虑的一个例子...
/**
* Try to get an Entity changeSet without changing the UnitOfWork
*
* @param EntityManager $em
* @param $entity
* @return null|array
*/
public static function diffDoctrineObject(EntityManager $em, $entity) {
$uow = $em->getUnitOfWork();
/*****************************************/
/* Equivalent of $uow->computeChangeSet($this->em->getClassMetadata(get_class($entity)), $entity);
/*****************************************/
$class = $em->getClassMetadata(get_class($entity));
$oid = spl_object_hash($entity);
$entityChangeSets = array();
if ($uow->isReadOnly($entity)) {
return null;
}
if ( ! $class->isInheritanceTypeNone()) {
$class = $em->getClassMetadata(get_class($entity));
}
// These parts are not needed for the changeSet?
// $invoke = $uow->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
//
// if ($invoke !== ListenersInvoker::INVOKE_NONE) {
// $uow->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($em), $invoke);
// }
$actualData = array();
foreach ($class->reflFields as $name => $refProp) {
$value = $refProp->getValue($entity);
if ($class->isCollectionValuedAssociation($name) && $value !== null) {
if ($value instanceof PersistentCollection) {
if ($value->getOwner() === $entity) {
continue;
}
$value = new ArrayCollection($value->getValues());
}
// If $value is not a Collection then use an ArrayCollection.
if ( ! $value instanceof Collection) {
$value = new ArrayCollection($value);
}
$assoc = $class->associationMappings[$name];
// Inject PersistentCollection
$value = new PersistentCollection(
$em, $em->getClassMetadata($assoc['targetEntity']), $value
);
$value->setOwner($entity, $assoc);
$value->setDirty( ! $value->isEmpty());
$class->reflFields[$name]->setValue($entity, $value);
$actualData[$name] = $value;
continue;
}
if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
$actualData[$name] = $value;
}
}
$originalEntityData = $uow->getOriginalEntityData($entity);
if (empty($originalEntityData)) {
// Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
// These result in an INSERT.
$originalEntityData = $actualData;
$changeSet = array();
foreach ($actualData as $propName => $actualValue) {
if ( ! isset($class->associationMappings[$propName])) {
$changeSet[$propName] = array(null, $actualValue);
continue;
}
$assoc = $class->associationMappings[$propName];
if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
$changeSet[$propName] = array(null, $actualValue);
}
}
$entityChangeSets[$oid] = $changeSet; // @todo - remove this?
} else {
// Entity is "fully" MANAGED: it was already fully persisted before
// and we have a copy of the original data
$originalData = $originalEntityData;
$isChangeTrackingNotify = $class->isChangeTrackingNotify();
$changeSet = $isChangeTrackingNotify ? $uow->getEntityChangeSet($entity) : array();
foreach ($actualData as $propName => $actualValue) {
// skip field, its a partially omitted one!
if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
continue;
}
$orgValue = $originalData[$propName];
// skip if value haven't changed
if ($orgValue === $actualValue) {
continue;
}
// if regular field
if ( ! isset($class->associationMappings[$propName])) {
if ($isChangeTrackingNotify) {
continue;
}
$changeSet[$propName] = array($orgValue, $actualValue);
continue;
}
$assoc = $class->associationMappings[$propName];
// Persistent collection was exchanged with the "originally"
// created one. This can only mean it was cloned and replaced
// on another entity.
if ($actualValue instanceof PersistentCollection) {
$owner = $actualValue->getOwner();
if ($owner === null) { // cloned
$actualValue->setOwner($entity, $assoc);
} else if ($owner !== $entity) { // no clone, we have to fix
// @todo - what does this do... can it be removed?
if (!$actualValue->isInitialized()) {
$actualValue->initialize(); // we have to do this otherwise the cols share state
}
$newValue = clone $actualValue;
$newValue->setOwner($entity, $assoc);
$class->reflFields[$propName]->setValue($entity, $newValue);
}
}
if ($orgValue instanceof PersistentCollection) {
// A PersistentCollection was de-referenced, so delete it.
// These parts are not needed for the changeSet?
// $coid = spl_object_hash($orgValue);
//
// if (isset($uow->collectionDeletions[$coid])) {
// continue;
// }
//
// $uow->collectionDeletions[$coid] = $orgValue;
$changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
continue;
}
if ($assoc['type'] & ClassMetadata::TO_ONE) {
if ($assoc['isOwningSide']) {
$changeSet[$propName] = array($orgValue, $actualValue);
}
// These parts are not needed for the changeSet?
// if ($orgValue !== null && $assoc['orphanRemoval']) {
// $uow->scheduleOrphanRemoval($orgValue);
// }
}
}
if ($changeSet) {
$entityChangeSets[$oid] = $changeSet;
// These parts are not needed for the changeSet?
// $originalEntityData = $actualData;
// $uow->entityUpdates[$oid] = $entity;
}
}
// These parts are not needed for the changeSet?
//// Look for changes in associations of the entity
//foreach ($class->associationMappings as $field => $assoc) {
// if (($val = $class->reflFields[$field]->getValue($entity)) !== null) {
// $uow->computeAssociationChanges($assoc, $val);
// if (!isset($entityChangeSets[$oid]) &&
// $assoc['isOwningSide'] &&
// $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
// $val instanceof PersistentCollection &&
// $val->isDirty()) {
// $entityChangeSets[$oid] = array();
// $originalEntityData = $actualData;
// $uow->entityUpdates[$oid] = $entity;
// }
// }
//}
/*********************/
return $entityChangeSets[$oid];
}
它在这里被表述为静态方法,但可以成为 UnitOfWork 中的一个方法......?
我没有跟上 Doctrine 的所有内部结构,所以可能错过了一些有副作用的东西或误解了这种方法的部分功能,但对它的(非常(快速测试似乎给了我期望看到的结果。
我希望这对某人有所帮助!
如果有人仍然对与接受的答案不同的方式感兴趣(它对我不起作用,我发现在我个人看来比这种方式更混乱(。
我安装了 JMS 序列化程序捆绑包,并在每个实体和我认为更改的每个属性上添加了一个 @Group({"changed_entity_group"}(。这样,我可以在旧实体和更新的实体之间进行序列化,之后只需说$oldJson == $updatedJson即可。如果您感兴趣或想要考虑更改的属性,则 JSON 将不同,如果您甚至想注册具体更改的内容,则可以将其转换为数组并搜索差异。
我使用这种方法是因为我主要对一堆实体的一些属性感兴趣,而不是完全对实体感兴趣。这很有用的一个例子是,如果你有一个@PrePersist @PreUpdate并且你有一个last_update日期,它将始终更新,因此你将始终得到实体是使用工作单元和类似的东西更新的。
希望这种方法对任何人都有帮助。
在我的情况下,我想在实体中获取关系的旧值,所以我使用基于这个
在 Doctrine 事件侦听器中使用UnitOfWork
和computeChangeSets
可能是首选方法。
但是:如果要在此侦听器中保留和刷新新实体,则可能会遇到很多麻烦。看起来,唯一合适的倾听者会onFlush
自己的一系列问题。
因此,我建议进行简单但轻量级的比较,只需注入EntityManagerInterface
即可在控制器甚至服务中使用(灵感来自上面帖子中的@Mohamed Ramrami(:
$uow = $entityManager->getUnitOfWork();
$originalEntityData = $uow->getOriginalEntityData($blog);
// for nested entities, as suggested in the docs
$defaultContext = [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $object->getId();
},
];
$normalizer = new Serializer([new DateTimeNormalizer(), new ObjectNormalizer(null, null, null, null, null, null, $defaultContext)]);
$yourEntityNormalized = $normalizer->normalize();
$originalNormalized = $normalizer->normalize($originalEntityData);
$changed = [];
foreach ($originalNormalized as $item=>$value) {
if(array_key_exists($item, $yourEntityNormalized)) {
if($value !== $yourEntityNormalized[$item]) {
$changed[] = $item;
}
}
}
注意:它确实可以正确比较字符串、日期时间、布尔值、整数和浮点数,但在对象上失败(由于循环引用问题(。可以更深入地比较这些对象,但对于例如文本更改检测,这已经足够了,而且比处理事件侦听器简单得多。
更多信息:
- 序列化程序和规范化程序
- 处理循环引用
就我而言,为了将数据从远程WS
同步到本地DB
我使用这种方式来比较两个实体(检查 il 旧实体是否与编辑的实体不同(。
我对称克隆持久化的实体以具有两个未持久化的对象:
<?php
$entity = $repository->find($id);// original entity exists
if (null === $entity) {
$entity = new $className();// local entity not exists, create new one
}
$oldEntity = clone $entity;// make a detached "backup" of the entity before it's changed
// make some changes to the entity...
$entity->setX('Y');
// now compare entities properties/values
$entityCloned = clone $entity;// clone entity for detached (not persisted) entity comparaison
if ( ! $em->contains( $entity ) || $entityCloned != $oldEntity) {// do not compare strictly!
$em->persist( $entity );
$em->flush();
}
unset($entityCloned, $oldEntity, $entity);
另一种可能性,而不是直接比较对象:
<?php
// here again we need to clone the entity ($entityCloned)
$entity_diff = array_keys(
array_diff_key(
get_object_vars( $entityCloned ),
get_object_vars( $oldEntity )
)
);
if(count($entity_diff) > 0){
// persist & flush
}
它对我有用1. 导入实体管理器2.现在您可以在课堂上的任何地方使用它。
use Doctrine'ORM'EntityManager;
$preData = $this->em->getUnitOfWork()->getOriginalEntityData($entity);
// $preData['active'] for old data and $entity->getActive() for new data
if($preData['active'] != $entity->getActive()){
echo 'Send email';
}