Есть ли встроенный способ получить все измененные/обновленные поля в сущности Доктрины 2
Предположим, я извлекаю сущность $e
и изменяю ее состояние с помощью установщиков:
$e->setFoo('a');
$e->setBar('b');
Есть ли какая-либо возможность получить массив полей, которые были изменены?
В случае моего примера я хотел бы получить foo => a, bar => b
в результате
PS: да, я знаю, что могу изменить все средства доступа и реализовать эту функцию вручную, но я ищу какой-нибудь удобный способ сделать это
7 answers
Вы можете использовать
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);
Примечание. При попытке получить обновленные поля внутри предварительного обновления слушатель, не пересчитывайте набор изменений, как это уже было сделано. Просто вызовите getEntityChangeSet, чтобы получить все изменения, внесенные в сущность.
Большой знак "Осторожно" для тех, кто хочет проверить наличие изменений в объекте, используя метод, описанный выше.
$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()
, но теперь он не сможет найти изменения в сущности, так как эти еще не сохраненные изменения считаются исходными свойствами сущности.
Проверьте эту общедоступную (а не внутреннюю) функцию:
$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);
Вы можете отслеживать изменения с помощью Политики уведомления.
Во-первых, реализует интерфейс 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;
}
}
}
Итак... что делать, когда мы хотим найти набор изменений за пределами жизненного цикла Доктрины? Как упоминалось в моем комментарии к сообщению @Ocramius выше, возможно, можно создать метод "только для чтения", который не нарушает фактическое постоянство доктрины, но дает пользователю представление о том, что изменилось.
Вот пример того, о чем я думаю...
/**
* 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...?
Я не в курсе все внутренние элементы Доктрины, поэтому, возможно, пропустили что-то, что имеет побочный эффект, или неправильно поняли часть того, что делает этот метод, но (очень) быстрая проверка этого, кажется, дает мне результаты, которые я ожидаю увидеть.
Надеюсь, это кому-нибудь поможет!
На случай, если кого-то все еще интересует другой способ, чем принятый ответ (он не работал для меня, и я нашел его более запутанным, чем этот способ, по моему личному мнению).
Я установил пакет сериализатора JMS и для каждой сущности и для каждого свойства, которое я считаю изменением, я добавил @Group ({"changed_entity_group"}). Таким образом, я могу затем выполнить сериализацию между старой сущностью и обновленной сущностью, а после этого просто сказать $OLDJSON == $Обновленный JSON. Если свойства, которые вас интересуют или которые вы хотели бы рассмотреть, изменятся, JSON не будет таким же, и если вы даже захотите зарегистрировать то, что конкретно изменилось, вы можете превратить его в массив и выполнить поиск различий.
Я использовал этот метод, так как меня интересовали в основном несколько свойств группы сущностей, а не сама сущность целиком. Пример, где это было бы полезно, - если у вас есть @prePersist @preUpdate и у вас есть last_update дата, которая всегда будет обновляться, поэтому вы всегда будете получать информацию о том, что объект был обновлен с использованием единицы работы и тому подобного.
Надеюсь, этот метод кому-нибудь пригодится.
В моем случае для синхронизации данных с удаленного WS
на локальный DB
Я использовал этот способ для сравнения двух сущностей (проверьте, что старая сущность отличается от отредактированной сущности).
Я просто клонирую сохраненный объект, чтобы два объекта не сохранялись:
<?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
}