Есть ли встроенный способ получить все измененные / обновленные поля в объекте Doctrine 2

Предположим, что я получаю объект $e и изменяю его состояние с помощью сеттеров:

 $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, чтобы получить все изменения, внесенные в объект.

Большой знак beware для тех, кто хочет проверить изменения в сущности, используя метод, описанный выше.

 $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] , эти еще не сохраненные изменения считаются исходными свойствами объекта.

Позже, когда $uow->computeChangeSets() $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 в вашей сущности и сделать diff. Что-то вроде этого :

 $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; } } } 

Итак … что делать, когда мы хотим найти набор изменений за пределами жизненного цикла Doctrine? Как упоминалось в моем комментарии к сообщению @Ocramius выше, возможно, можно создать метод «readonly», который не противоречит фактическому упорству доктрины, но дает пользователю представление о том, что изменилось.

Вот пример того, о чем я думаю …

 /** * 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 Serializer Bundle и для каждого объекта и для каждого свойства, которое я считаю изменением, я добавил @Group ({"changed_entity_group"}). Таким образом, я могу сделать сериализацию между старой сущностью и обновленной сущностью, и после этого это просто вопрос о $ oldJson == $ updatedJson. Если свойства, которые вас интересуют или которые вы хотели бы рассмотреть, JSON не будет одинаковым, и если вы даже хотите зарегистрировать WHAT специально измененное, вы можете превратить его в массив и искать различия.

Я использовал этот метод, так как меня интересовали в основном несколько свойств кучки сущностей, а не целиком. Например, если это было бы полезно, если у вас есть @PrePersist @PreUpdate, и у вас есть дата last_update, которая всегда будет обновляться, поэтому вы всегда будете получать, что объект был обновлен с использованием единицы работы и тому подобного.

Надеюсь, этот метод полезен для всех.