Existe - t-il un moyen intégré d'obtenir tous les champs modifiés/mis à jour dans une entité Doctrine 2

Supposons que je récupère une entité $e et modifie son état avec des setters:

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

Est-il possible de récupérer un tableau de champs qui ont été modifiés?

Dans le cas de mon exemple, je voudrais récupérer foo => a, bar => b en conséquence

PS: Oui, je sais que je peux modifier tous les accesseurs et implémenter cette fonctionnalité manuellement, mais je cherche un moyen pratique de le faire

68
demandé sur Jon Winstanley 2012-01-30 02:58:45

7 réponses

Vous pouvez utiliser Doctrine\ORM\EntityManager#getUnitOfWork pour obtenir un Doctrine\ORM\UnitOfWork.

Ensuite, déclenchez simplement le calcul de changeset (fonctionne uniquement sur les entités gérées) via Doctrine\ORM\UnitOfWork#computeChangeSets().

Vous pouvez également utiliser des méthodes similaires comme Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity) Si vous savez exactement ce que vous voulez vérifier sans itérer sur le graphe d'objet entier.

Après cela, vous pouvez utiliser Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity) pour récupérer toutes les modifications apportées à votre objet.

Mettre ensemble:

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

Remarque. Si vous essayez d'obtenir les champs mis à jour dans une preUpdate listener , ne recalculez pas le jeu de modifications, comme cela a déjà été fait. Appelez simplement getentitychangeset pour obtenir toutes les modifications apportées à l'entité.

128
répondu Ocramius 2013-09-12 16:49:30

Big beware sign pour ceux qui veulent vérifier les modifications sur l'entité en utilisant la méthode décrite ci-dessus.

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

La méthode $uow->computeChangeSets() est utilisée en interne par la routine persistante d'une manière qui rend la solution ci-dessus inutilisable. C'est aussi ce qui est écrit dans les commentaires à la méthode: @internal Don't call from the outside. Après avoir vérifié les modifications apportées aux entités avec $uow->computeChangeSets(), le code suivant est exécuté à la fin de la méthode (pour chaque entité gérée):

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

Le $actualData tableau contient les modifications actuelles des propriétés de l'entité. Dès qu'elles sont écrites dans $this->originalEntityData[$oid], ces modifications non encore persistantes sont considérées comme les propriétés d'origine de l'entité.

Plus Tard, lorsque le $em->persist($entity) est appelé à enregistrer les modifications apportées à l'entité, il implique également la méthode $uow->computeChangeSets(), mais maintenant, il ne sera pas en mesure de trouver les modifications apportées à l'entité, comme ces pas encore persisté changements sont considérés comme les propriétés d'origine de l'entité.

32
répondu Slavik Derevianko 2013-05-15 16:16:09

Vérifiez cette fonction publique (et non interne):

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

, De la doctrine repo:

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

Tout ce que vous avez à faire est d'implémenter une fonction toArray ou serialize dans votre entité et de faire un diff. Quelque chose comme ceci :

$originalData = $em->getUnitOfWork()->getOriginalEntityData($entity);
$toArrayEntity = $entity->toArray();
$changes = array_diff_assoc($toArrayEntity, $originalData);
19
répondu Mohamed Ramrami 2018-01-22 11:03:40

Vous pouvez suivre les changements avec Informer les politiques de.

Tout d'abord, implémente l'interfaceNotifyPropertyChanged :

/**
 * @Entity
 * @ChangeTrackingPolicy("NOTIFY")
 */
class MyEntity implements NotifyPropertyChanged
{
    // ...

    private $_listeners = array();

    public function addPropertyChangedListener(PropertyChangedListener $listener)
    {
        $this->_listeners[] = $listener;
    }
}

Ensuite, appelez simplement _onPropertyChanged sur chaque méthode qui modifie les données, lancez votre entité comme suit:

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
répondu manix 2013-10-09 00:50:19

Donc... que faire lorsque nous voulons trouver un ensemble de modifications en dehors du cycle de vie de la Doctrine? Comme mentionné dans mon commentaire sur le post de @Ocramius ci-dessus, il est peut-être possible de créer une méthode "en lecture seule" qui ne gâche pas la persistance de la Doctrine réelle mais donne à l'utilisateur une vue de ce qui a changé.

Voici un exemple de ce à quoi je pense...

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

Il est formulé ici comme une méthode statique mais pourrait devenir une méthode dans UnitOfWork...?

Je ne suis pas à la vitesse sur tous les internes de la Doctrine, donc pourrait avoir manqué quelque chose qui a un effet secondaire ou mal compris une partie de ce que cette méthode fait, mais un test (très) rapide de celui-ci semble me donner les résultats que je m'attends à voir.

J'espère que cela aide quelqu'un!

1
répondu caponica 2014-08-22 22:47:45

Dans le cas où quelqu'un est toujours intéressé d'une manière différente de la réponse acceptée (cela ne fonctionnait pas pour moi et je l'ai trouvé plus messier que de cette façon dans mon opinion personnelle).

J'ai installé le JMS Serializer Bundle et sur chaque entité et sur chaque propriété que je considère comme un changement j'ai ajouté un @Group ({"changed_entity_group"}). De cette façon, je peux ensuite faire une sérialisation entre l'ancienne entité et l'entité mise à jour et après cela, il suffit de dire $oldJson == $updatedJson. Si les propriétés qui vous intéressent ou que vous souhaitez envisager des modifications, le JSON ne sera pas le même et si vous voulez même enregistrer ce qui a spécifiquement changé, vous pouvez le transformer en un tableau et rechercher les différences.

J'ai utilisé cette méthode car je m'intéressais principalement à quelques propriétés d'un tas d'entités et non à l'entité entièrement. Un exemple où cela serait utile est si vous avez un @PrePersist @PreUpdate et que vous avez un last_update date, qui sera toujours mis à jour, donc vous obtiendrez toujours que l'entité a été mise à jour en utilisant l'Unité de travail et des choses comme ça.

J'espère que cette méthode est utile à tout le monde.

1
répondu Benjamin Vison 2016-03-22 12:17:30

Dans mon cas, pour synchroniser les données d'un WS distant vers un DB local, j'ai utilisé cette méthode pour comparer deux entités (vérifiez que l'ancienne entité a des diffs de l'entité éditée).

Je clone symply l'entité persistante pour avoir deux objets non persistants:

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

Une autre possibilité plutôt que de comparer les objets directement:

<?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
répondu kxxxxoo 2018-05-07 06:33:35