web-dev-qa-db-fra.com

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 à l'aide de 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, j'aimerais récupérer foo => a, bar => b à la suite

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.

70
zerkms

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

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

Vous pouvez également utiliser des méthodes similaires, telles que Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity), si vous savez exactement ce que vous voulez vérifier sans parcourir le graphe d'objets en 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'extraire les champs mis à jour dans un écouteur preUpdate , ne recalculez pas l'ensemble des modifications, comme cela a déjà été fait. Appelez simplement getEntityChangeSet pour obtenir toutes les modifications apportées à l'entité.

130
Ocramius

Big beware sign pour ceux qui veulent vérifier les modifications apportées à 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 de manière à rendre la solution ci-dessus inutilisable. C'est également ce qui est écrit dans les commentaires de 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 tableau $actualData contient les modifications actuelles apportées aux propriétés de l'entité. Dès qu'ils sont écrits 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 la $em->persist($entity) est appelée pour enregistrer les modifications apportées à l'entité, elle implique également la méthode $uow->computeChangeSets(), mais elle ne pourra plus rechercher les modifications apportées à l'entité, car ces modifications non encore persistantes sont considérées comme les propriétés d'origine l'entité.

34

Vous pouvez suivre les modifications avec Règles de notification .

Tout d'abord, implémente l'interface NotifyPropertyChanged:

/**
 * @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 et transmettez 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
manix

Dans le cas où quelqu'un est toujours intéressé par un moyen différent de la réponse acceptée (cela ne fonctionnait pas pour moi et je l'ai trouvé plus compliqué que mon point de vue personnel).

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 ({"modified_entity_group"}). De cette façon, je peux ensuite faire une sérialisation entre l'ancienne entité et l'entité mise à jour et ensuite, il suffit de dire $ oldJson == $ updatedJson. Si les propriétés qui vous intéressent ou que vous souhaitez prendre en compte changent, le JSON ne sera pas le même et si vous souhaitez même enregistrer WHAT spécifiquement modifié, vous pouvez le transformer en tableau et rechercher les différences.

J'ai utilisé cette méthode car je m'intéressais principalement à quelques propriétés d'un groupe d'entités et non à l'entité entière. Cela serait utile, par exemple, si vous avez @PrePersist @PreUpdate et que vous avez une date de dernière mise à jour, qui sera toujours mise à jour. Par conséquent, vous obtiendrez toujours que l'entité a été mise à jour à l'aide d'une unité de travail, etc.

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

1
Benjamin Vison

Alors ... que faire quand on veut trouver un changeset en dehors du cycle de vie de 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 perturbe pas la persistance réelle de Doctrine, mais donne à l'utilisateur une vue de ce qui a changé.

Voici un exemple de ce que 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];
}

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

Je ne suis pas au courant de tous les éléments internes de Doctrine, alors j'ai peut-être oublié quelque chose qui a un effet secondaire ou une partie mal comprise de ce que fait cette méthode, mais un test (très) rapide semble me donner les résultats que j'attends à voir.

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

1
caponica

Il retournera les changements

$entityManager->getUnitOfWork()->getEntityChangeSet($entity)
0
Omar Makled

Dans mon cas, pour synchroniser des données depuis un WS distant vers un DB local, j’utilisais cette méthode pour comparer deux entités (c’est pourquoi l’ancienne entité a des différences avec l’entité modifiée). 

Je clone de manière symétrique l'entité persistante pour que deux objets ne persistent pas:

<?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 directement les objets:

<?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
kxxxxoo