¿Existe una forma incorporada de obtener todos los campos modificados / actualizados en una entidad de Doctrine 2?


81

Supongamos que recupero una entidad $ey modifico su estado con setters:

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

¿Existe la posibilidad de recuperar una serie de campos que se han cambiado?

En el caso de mi ejemplo, me gustaría recuperar foo => a, bar => bcomo resultado

PD: sí, sé que puedo modificar todos los accesores e implementar esta función manualmente, pero estoy buscando una forma práctica de hacerlo

Respuestas:


148

Puede utilizar Doctrine\ORM\EntityManager#getUnitOfWorkpara obtener un Doctrine\ORM\UnitOfWork.

Luego, simplemente active el cálculo del conjunto de cambios (funciona solo en entidades administradas) a través de Doctrine\ORM\UnitOfWork#computeChangeSets().

También puede usar métodos similares como Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity)si supiera exactamente lo que desea verificar sin iterar sobre todo el gráfico del objeto.

Después de eso, puede usar Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity)para recuperar todos los cambios en su objeto.

Poniendo todo junto:

$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. Si intenta obtener los campos actualizados dentro de un oyente previo a la actualización , no vuelva a calcular el conjunto de cambios, como ya se ha hecho. Simplemente llame a getEntityChangeSet para obtener todos los cambios realizados en la entidad.

Advertencia: como se explica en los comentarios, esta solución no debe usarse fuera de los detectores de eventos de Doctrine. Esto romperá el comportamiento de Doctrine.


4
El siguiente comentario dice que si llama a $ em-> computerChangeSets (), se romperá el $ em-> persist () normal al que llama más tarde porque no parecerá que nada ha cambiado. Si es así, ¿cuál es la solución, simplemente no llamamos a esa función?
Chadwick Meyer

4
No se supone que debe usar esta API fuera de los oyentes de eventos del ciclo de vida de UnitOfWork.
Ocramius

6
No deberías. Eso no es para lo que está destinado el ORM. Utilice la diferenciación manual en tales casos, manteniendo una copia de los datos antes y después de las operaciones aplicadas.
Ocramius

6
@Ocramius, puede que no sea para lo que debe usarse, pero sin duda sería útil . Si tan solo hubiera una forma de usar Doctrine para calcular los cambios sin efectos secundarios. Por ejemplo, si hubiera un nuevo método / clase, tal vez en la UOW, al que podría llamar para solicitar una serie de cambios. Pero que no alteraría / afectaría el ciclo de persistencia real de ninguna manera. ¿Es eso posible?
caponica

3
Vea una mejor solución publicada por Mohamed Ramrami a continuación usando $ em-> getUnitOfWork () -> getOriginalEntityData ($ entity)
Wax Cage

41

Gran cartel de cuidado para aquellos que quieran verificar los cambios en la entidad utilizando el método descrito anteriormente.

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

El $uow->computeChangeSets()método es utilizado internamente por la rutina persistente de una manera que inutiliza la solución anterior. Eso es también lo que está escrito en los comentarios al método: @internal Don't call from the outside. Después de verificar los cambios en las entidades con $uow->computeChangeSets(), se ejecuta el siguiente fragmento de código al final del método (por cada entidad administrada):

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

La $actualDatamatriz contiene los cambios actuales en las propiedades de la entidad. Tan pronto como se escriben $this->originalEntityData[$oid], estos cambios aún no persistentes se consideran las propiedades originales de la entidad.

Más tarde, cuando $em->persist($entity)se llama a para guardar los cambios en la entidad, también involucra el método $uow->computeChangeSets(), pero ahora no podrá encontrar los cambios en la entidad, ya que estos cambios aún no persistentes se consideran las propiedades originales de la entidad. .


1
Es exactamente lo mismo que @Ocramius especificó en la respuesta marcada
zerkms

1
$ uow = clonar $ em-> getUnitOfWork (); resuelve ese problema
tvlooy

1
No se admite la clonación de UoW y puede producir resultados no deseados.
Ocramius

9
@Slavik Derevianko, ¿qué sugieres? ¿Simplemente no llames $uow->computerChangeSets()? o qué método alternativo?
Chadwick Meyer

Si bien esta publicación es realmente útil (es una gran advertencia a la respuesta anterior), no es una solución en sí misma. En su lugar, he editado la respuesta aceptada.
Matthieu Napoli

37

Verifique esta función pública (y no interna):

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

Desde el repositorio de doctrina :

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

Todo lo que tienes que hacer es implementar una función toArrayo serializeen tu entidad y hacer una diferencia. Algo como esto :

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

1
¿Cómo aplicar esto a una situación en la que la entidad está relacionada con otra (puede ser OneToOne)? En este caso, cuando ejecuto getOriginalEntityData en la entidad top-lvl, los datos originales de sus entidades relacionadas no son realmente originales, sino más bien actualizados.
mu4ddi3

5

Puede realizar un seguimiento de los cambios con Notificar políticas .

Primero, implementa la interfaz NotifyPropertyChanged :

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

    private $_listeners = array();

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

Luego, simplemente llame a _onPropertyChanged en cada método que cambie los datos arroje su entidad de la siguiente manera:

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

7
¿Escuchas dentro de una entidad? ¡Locura! En serio, la política de seguimiento parece una buena solución, ¿hay alguna forma de definir oyentes fuera de la entidad (estoy usando Symfony2 DoctrineBundle)?
Gildas

Ésta es la solución incorrecta. Debería mirar hacia los eventos de dominio. github.com/gpslab/domain-event
ghost404


2

En caso de que alguien todavía esté interesado de una manera diferente a la respuesta aceptada (no funcionó para mí y lo encontré más complicado que así en mi opinión personal).

Instalé el JMS Serializer Bundle y en cada entidad y en cada propiedad que considero un cambio agregué un @Group ({"modified_entity_group"}). De esta manera, puedo hacer una serialización entre la entidad anterior y la entidad actualizada y, después de eso, solo es cuestión de decir $ oldJson == $ updatedJson. Si las propiedades que le interesan o que le gustaría considerar cambian, el JSON no será el mismo y si incluso desea registrar QUÉ cambió específicamente, puede convertirlo en una matriz y buscar las diferencias.

Usé este método porque estaba interesado principalmente en algunas propiedades de un grupo de entidades y no en la entidad por completo. Un ejemplo en el que esto sería útil es si tiene un @PrePersist @PreUpdate y tiene una fecha de última actualización, que siempre se actualizará, por lo que siempre obtendrá que la entidad se actualizó usando la unidad de trabajo y cosas así.

Espero que este método sea útil para cualquiera.


1

Entonces ... ¿qué hacer cuando queremos encontrar un conjunto de cambios fuera del ciclo de vida de Doctrine? Como mencioné anteriormente en mi comentario sobre la publicación de @Ocramius, tal vez sea posible crear un método de "solo lectura" que no interfiera con la persistencia real de Doctrine pero le dé al usuario una vista de lo que ha cambiado.

Aquí hay un ejemplo de lo que estoy pensando ...

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

Está redactado aquí como un método estático, pero ¿podría convertirse en un método dentro de UnitOfWork ...?

No estoy al tanto de todos los aspectos internos de Doctrine, por lo que podría haber pasado por alto algo que tiene un efecto secundario o haber entendido mal una parte de lo que hace este método, pero una prueba (muy) rápida parece darme los resultados que espero. para ver.

¡Espero que esto ayude a alguien!


1
Bueno, si alguna vez nos vemos, ¡chocarás los cinco! Muchas, muchas gracias por este. Muy fácil de usar también en otras 2 funciones: hasChangesy getChanges(la última para obtener solo los campos modificados en lugar de todo el conjunto de cambios).
rkeet

0

En mi caso, para sincronizar datos de un remoto WSa un local DB, utilicé esta forma para comparar dos entidades (verifique que la entidad anterior tenga diferencias con la entidad editada).

Simplemente clono la entidad persistente para que no persistan dos objetos:

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

Otra posibilidad en lugar de comparar objetos directamente:

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

en mi caso, quiero obtener el valor anterior de la relación en la entidad, así que uso la base Doctrine \ ORM \ PersistentCollection :: getSnapshot en esto


0

Me funciona 1. Importar EntityManager 2. Ahora puedes usar esto en cualquier lugar de la clase.

  use Doctrine\ORM\EntityManager;



    $preData = $this->em->getUnitOfWork()->getOriginalEntityData($entity);
    // $preData['active'] for old data and $entity->getActive() for new data
    if($preData['active'] != $entity->getActive()){
        echo 'Send email';
    }

0

Trabajar con UnitOfWorky computeChangeSets dentro de un escucha de eventos de Doctrine es probablemente el método preferido.

Sin embargo : si desea persistir y vaciar una nueva entidad dentro de este oyente, puede enfrentarse a muchas molestias. Como parece, el único oyente adecuado sería onFlushcon su propio conjunto de problemas.

Entonces sugiero una comparación simple pero liviana, que se puede usar dentro de los Controladores e incluso en los Servicios simplemente inyectando el EntityManagerInterface(inspirado en @Mohamed Ramrami en la publicación anterior):

$uow = $entityManager->getUnitOfWork();
$originalEntityData = $uow->getOriginalEntityData($blog);

// for nested entities, as suggested in the docs
$defaultContext = [
    AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
        return $object->getId();
    },
];
$normalizer = new Serializer([new DateTimeNormalizer(), new ObjectNormalizer(null, null, null, null, null,  null, $defaultContext)]);
$yourEntityNormalized = $normalizer->normalize();
$originalNormalized = $normalizer->normalize($originalEntityData);

$changed = [];
foreach ($originalNormalized as $item=>$value) {
    if(array_key_exists($item, $yourEntityNormalized)) {
        if($value !== $yourEntityNormalized[$item]) {
            $changed[] = $item;
        }
    }
}

Nota : compara cadenas, fechas, bools, enteros y flotantes correctamente, pero falla en los objetos (debido a los problemas de referencia circular). Uno podría comparar estos objetos con más profundidad, pero, por ejemplo, para la detección de cambios de texto, esto es suficiente y mucho más simple que manejar Event Listeners.

Más información:

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.