Is there a built-in way to get all changed / updated fields in a Doctrine 2 object

Suppose I get an object $e and change its state using seters:

 $e->setFoo('a'); $e->setBar('b'); 

Is it possible to get an array of fields that have been changed?

In the case of my example, I would like to get foo => a, bar => b as a result

PS: yes, I know that I can modify all accessors and implement this function manually, but I'm looking for a convenient way to do this

+76
php symfony doctrine doctrine2 doctrine-orm
Jan 29 2018-12-12T00:
source share
8 answers

You can use Doctrine\ORM\EntityManager#getUnitOfWork to get Doctrine\ORM\UnitOfWork .

Then just run the calculation Doctrine\ORM\UnitOfWork#computeChangeSets() changes (only works on managed objects) via Doctrine\ORM\UnitOfWork#computeChangeSets() .

You can also use similar methods such as Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity) if you know exactly what you want to check without repeating the entire graph of the object.

After that, you can use Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity) to get all the changes to your object.

Putting it together:

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

The note. If you are trying to get updated fields in a preUpdate listener , do not recount the set of changes as it has already been done. Just call getEntityChangeSet to get all the changes made to the entity.

Warning: as explained in the comments, this solution should not be used outside of Doctrine event listeners. This violates the doctrine of behavior.

+135
Jan 29 '12 at 23:20
source share

A big warning sign for those who want to check for changes in an entity using the method described above.

 $uow = $em->getUnitOfWork(); $uow->computeChangeSets(); 

The $uow->computeChangeSets() method is used internally with the existing procedure in such a way that makes the above solution unusable. This is also what is written in the method comments: @internal Don't call from the outside . After checking the changes of objects using $uow->computeChangeSets() ), the following code fragment is executed at the end of the method (for each managed object):

 if ($changeSet) { $this->entityChangeSets[$oid] = $changeSet; $this->originalEntityData[$oid] = $actualData; $this->entityUpdates[$oid] = $entity; } 

The $actualData contains the current changes to the properties of the object. As soon as they are written to $this->originalEntityData[$oid] , these changes that have not yet been saved are considered the original properties of the object.

Later, when $em->persist($entity) is called to save changes to the entity, it also includes the $uow->computeChangeSets() method, but now it will not be able to find changes in the entity, since they have not yet been saved, the changes are considered to be the original properties object.

+36
May 14 '13 at 22:53
source share

Check out this public (rather than internal) function:

$this->em->getUnitOfWork()->getOriginalEntityData($entity);

From the REPO teachings:

 /** * 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) 

All you have to do is implement the toArray or serialize function in your entity and create a diff. Something like that:

 $originalData = $em->getUnitOfWork()->getOriginalEntityData($entity); $toArrayEntity = $entity->toArray(); $changes = array_diff_assoc($toArrayEntity, $originalData); 
+28
Apr 15 '16 at 17:30
source share

You can track changes using Notify Policies .

Firstly, it implements the NotifyPropertyChanged interface:

 /** * @Entity * @ChangeTrackingPolicy("NOTIFY") */ class MyEntity implements NotifyPropertyChanged { // ... private $_listeners = array(); public function addPropertyChangedListener(PropertyChangedListener $listener) { $this->_listeners[] = $listener; } } 

Then just call _onPropertyChanged for each method that modifies the data, throwing your entity as shown below:

 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; } } } 
+5
09 Oct '13 at 0:50
source share

If someone is still interested in a different way than the accepted answer (it didn't work for me, and I found it more messy than this one, in my personal opinion).

I installed the JMS Serializer Bundle for each object and for each property that I consider to be a change, I added @Group ({"changed_entity_group"}). That way, I can serialize between the old entity and the updated entity, and after that it's just a question about $ oldJson == $ updatedJson. If the properties that interest you or that you would like to consider, JSON will not be the same, and if you even want to register WHAT specifically changed, you can turn it into an array and find the differences.

I used this method, since I was mainly interested in several properties of a group of objects, and not as a whole. For example, if it would be useful if you have @PrePersist @PreUpdate and you have a last_update date that will always be updated, so you will always get that the object was updated using a unit of work and the like.

Hope this method is useful for everyone.

+2
Mar 22 '16 at 12:17
source share

So ... what to do when we want to find a set of changes outside the Doctrine life cycle? As mentioned in my comment on the @Ocramius post above, it is possible to create a β€œreadonly” method that does not fit into the actual durability of Doctrine but gives the user an idea of ​​what has changed.

Here is an example of what I'm thinking ...

 /** * 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]; } 

It is formulated here as a static method, but can become a method inside UnitOfWork ...?

I am not aware of all the internal affairs of the Doctrine, so I may have missed something that has a side effect or misunderstands part of what this method does, but the (very) quick test seems to give me the results that I expect to see .

Hope this helps someone!

+1
Aug 22 '14 at 22:47
source share

He will return the changes

 $em->getUnitOfWork()->getEntityChangeSet($entity) 
+1
Apr 09 '19 at 13:14
source share

In my case, to synchronize data from a remote WS to a local DB I used this method to compare two entities (check that the old entity has differences from the edited entity).

I just clone a persistent entity so that two objects are not persisted:

 <?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 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); 

Another possibility, rather than comparing objects directly:

 <?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 } 
0
Apr 09 '18 at 10:20
source share



All Articles