it-swarm.dev

Esiste un modo integrato per ottenere tutti i campi modificati/aggiornati in un'entità Doctrine 2

Supponiamo di recuperare un'entità $e e di modificarne lo stato con setter:

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

C'è qualche possibilità di recuperare una serie di campi che sono stati modificati?

In caso del mio esempio mi piacerebbe recuperare foo => a, bar => b come risultato

PS: sì, so che posso modificare tutti gli accessor e implementare questa funzione manualmente, ma sto cercando un modo pratico per farlo

70
zerkms

Puoi usare Doctrine\ORM\EntityManager#getUnitOfWork per ottenere un Doctrine\ORM\UnitOfWork.

Quindi basta attivare il calcolo del changeset (funziona solo sulle entità gestite) tramite Doctrine\ORM\UnitOfWork#computeChangeSets().

È possibile utilizzare anche metodi simili come Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity) se si conosce esattamente ciò che si desidera controllare senza iterare sull'intero grafico dell'oggetto.

Successivamente è possibile utilizzare Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity) per recuperare tutte le modifiche all'oggetto.

Mettendolo insieme:

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

Nota. Se stai cercando di ottenere i campi aggiornati all'interno di un listener preUpdate , non ricalcolare il set di modifiche, come è già stato fatto. Basta chiamare getEntityChangeSet per ottenere tutte le modifiche apportate all'entità.

130
Ocramius

Grande segno di attenzione per coloro che vogliono verificare le modifiche sull'entità usando il metodo descritto sopra.

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

Il metodo $uow->computeChangeSets() è utilizzato internamente dalla routine persistente in modo da rendere inutilizzabile la soluzione di cui sopra. Questo è anche ciò che è scritto nei commenti al metodo: @internal Don't call from the outside. Dopo aver verificato le modifiche alle entità con $uow->computeChangeSets(), il seguente pezzo di codice viene eseguito alla fine del metodo (per ogni entità gestita):

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

L'array $actualData contiene le modifiche correnti alle proprietà dell'entità. Non appena queste sono scritte in $this->originalEntityData[$oid], queste modifiche non ancora mantenute sono considerate proprietà originali dell'entità.

Successivamente, quando il $em->persist($entity) viene chiamato per salvare le modifiche all'entità, coinvolge anche il metodo $uow->computeChangeSets(), ma ora non sarà in grado di trovare le modifiche all'entità, poiché queste modifiche non ancora persistenti sono considerate proprietà originali di l'entità.

34

Controlla questa funzione pubblica (e non interna):

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

Da 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)

Tutto quello che devi fare è implementare una funzione toArray o serialize nella tua entità e creare una diff. Qualcosa come questo : 

$originalData = $em->getUnitOfWork()->getOriginalEntityData($entity);
$toArrayEntity = $entity->toArray();
$changes = array_diff_assoc($toArrayEntity, $originalData);
23
Mohamed Ramrami

È possibile tenere traccia delle modifiche con Notifica politiche .

Innanzitutto, implementa l'interfaccia NotifyPropertyChanged:

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

    private $_listeners = array();

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

Quindi, chiama il _onPropertyChanged su ogni metodo che modifica i dati e lancia la tua entità come di seguito:

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

Nel caso in cui qualcuno sia ancora interessato a un modo diverso rispetto alla risposta accettata (non funzionava per me e l'ho trovato più incisivo di così nella mia opinione personale).

Ho installato il JMS Serializer Bundle e su ogni entità e su ogni proprietà che considero una modifica ho aggiunto un @Group ({"changed_entity_group"}). In questo modo, posso quindi creare una serializzazione tra la vecchia entità e l'entità aggiornata e dopo è solo questione di dire $ oldJson == $ updatedJson. Se le proprietà a cui sei interessato o che desideri prendere in considerazione cambiano, il JSON non sarà lo stesso e se vuoi anche registrare WHAT specificamente modificato, puoi trasformarlo in un array e cercare le differenze.

Ho usato questo metodo poiché ero interessato principalmente ad alcune proprietà di un gruppo di entità e non all'entità interamente. Un esempio in cui questo sarebbe utile è se hai un @PrePersist @PreUpdate e hai una data last_update, che sarà sempre aggiornata quindi otterrai sempre che l'entità sia stata aggiornata usando l'unità di lavoro e cose del genere.

Spero che questo metodo sia utile a chiunque.

1
Benjamin Vison

Quindi ... cosa fare quando vogliamo trovare un changeset al di fuori del ciclo di vita di Doctrine? Come menzionato nel mio commento sul post di @Ocramius, forse è possibile creare un metodo "readonly" che non interferisca con la persistente persistenza di Doctrine, ma dà all'utente una visione di ciò che è cambiato.

Ecco un esempio di cosa sto pensando di ...

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

È formulato qui come metodo statico ma potrebbe diventare un metodo all'interno di UnitOfWork ...?

Non sono in grado di accelerare tutti gli aspetti interni di Doctrine, quindi potrebbe essermi sfuggito qualcosa che ha un effetto collaterale o una parte incompresa di ciò che questo metodo fa, ma un (molto) rapido test sembra darmi i risultati che mi aspetto vedere.

Spero che questo aiuti qualcuno!

1
caponica

Restituirà le modifiche

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

Nel mio caso, per sincronizzare i dati da un WS remoto a un DB locale, ho usato questo modo per confrontare due entità (controllare che l'entità precedente abbia diff da parte dell'entità modificata). 

Ho simulato clone l'entità persistente per avere due oggetti non persistenti:

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

Un'altra possibilità piuttosto che confrontare direttamente gli oggetti:

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