Czy istnieje wbudowany sposób uzyskania wszystkich zmienionych / zaktualizowanych pól w encji Doctrine 2?


81

Załóżmy, że pobieram encję $ei modyfikuję jej stan za pomocą seterów:

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

Czy istnieje możliwość pobrania tablicy zmienionych pól?

W przypadku mojego przykładu chciałbym pobrać foo => a, bar => bwynik

PS: tak, wiem, że mogę zmodyfikować wszystkie akcesory i zaimplementować tę funkcję ręcznie, ale szukam jakiegoś wygodnego sposobu na zrobienie tego

Odpowiedzi:


150

Możesz użyć, Doctrine\ORM\EntityManager#getUnitOfWorkaby uzyskać Doctrine\ORM\UnitOfWork.

Następnie po prostu wyzwól obliczenia zestawu zmian (działa tylko na zarządzanych jednostkach) za pośrednictwem Doctrine\ORM\UnitOfWork#computeChangeSets().

Możesz również użyć podobnych metod, na przykład Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity)jeśli wiesz dokładnie, co chcesz sprawdzić, bez iteracji po całym grafie obiektów.

Następnie możesz użyć, Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity)aby pobrać wszystkie zmiany w obiekcie.

Składając to razem:

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

Uwaga. Jeśli próbujesz pobrać zaktualizowane pola w odbiorniku preUpdate , nie ponownie zestawu zmian, ponieważ zostało to już zrobione. Po prostu wywołaj metodę getEntityChangeSet, aby uzyskać wszystkie zmiany wprowadzone w encji.

Ostrzeżenie: jak wyjaśniono w komentarzach, tego rozwiązania nie należy używać poza odbiornikami zdarzeń Doctrine. To przerwie zachowanie Doctrine.


4
Poniższy komentarz mówi, że jeśli wywołasz $ em-> computerChangeSets (), to zepsuje to zwykłą $ em-> persist (), którą wywołasz później, ponieważ nie będzie wyglądać, jakby cokolwiek zostało zmienione. Jeśli tak, jakie jest rozwiązanie, czy po prostu nie wywołujemy tej funkcji?
Chadwick Meyer

4
Nie należy używać tego interfejsu API poza odbiornikami zdarzeń cyklu życia jednostki UnitOfWork.
Ocramius

6
Nie powinieneś. Nie do tego ma służyć ORM. W takich przypadkach należy używać ręcznego porównywania, zachowując kopię danych przed i po zastosowanych operacjach.
Ocramius

6
@Ocramius, może nie być tym, do czego ma być używany, ale bez wątpienia byłby przydatny . Gdyby tylko był sposób na wykorzystanie Doctrine do obliczenia zmian bez skutków ubocznych. Np. Gdyby istniała nowa metoda / klasa, być może w UOW, którą można wywołać, aby poprosić o tablicę zmian. Ale to w żaden sposób nie zmieni / nie wpłynie na rzeczywisty cykl trwałości. Czy to jest możliwe?
caponica

3
Zobacz lepsze rozwiązanie opublikowane przez Mohameda Ramrami poniżej, używając $ em-> getUnitOfWork () -> getOriginalEntityData ($ entity)
Wax Cage

41

Wielka uwaga na znak dla tych, którzy chcą sprawdzić zmiany w encji za pomocą metody opisanej powyżej.

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

$uow->computeChangeSets()Sposób jest stosowany wewnętrznie w rutynowych utrzymującej się w taki sposób, że powoduje, że rozwiązanie wyżej użytku. To także to, co jest napisane w komentarzach do metody: @internal Don't call from the outside. Po sprawdzeniu zmian w jednostkach za pomocą $uow->computeChangeSets(), następujący fragment kodu jest wykonywany na końcu metody (na każdą zarządzaną jednostkę):

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

$actualDataTablica posiada aktualne zmiany właściwości jednostki. Gdy tylko zostaną one zapisane $this->originalEntityData[$oid], te jeszcze nie utrwalone zmiany są uważane za oryginalne właściwości jednostki.

Później, gdy $em->persist($entity)jest wywoływana w celu zapisania zmian w encji, obejmuje również metodę $uow->computeChangeSets(), ale teraz nie będzie w stanie znaleźć zmian w encji, ponieważ te jeszcze nie utrwalone zmiany są uważane za oryginalne właściwości jednostki .


1
To dokładnie to samo, co @Ocramius podał w zaznaczonej odpowiedzi
zerkms

1
$ uow = klon $ em-> getUnitOfWork (); rozwiązuje ten problem
tvlooy

1
Klonowanie UoW nie jest obsługiwane i może prowadzić do niepożądanych rezultatów.
Ocramius,

9
@Slavik Derevianko, więc co proponujesz? Tylko nie dzwoń $uow->computerChangeSets()? lub jaką alternatywną metodę?
Chadwick Meyer

Chociaż ten post jest naprawdę przydatny (jest dużym ostrzeżeniem dla powyższej odpowiedzi), nie jest sam w sobie rozwiązaniem. Zamiast tego zredagowałem zaakceptowaną odpowiedź.
Matthieu Napoli

39

Sprawdź tę funkcję publiczną (a nie wewnętrzną):

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

Z repozytorium doktryny :

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

Wszystko, co musisz zrobić, to zaimplementować funkcję toArraylub serializew swojej encji i zrobić różnicę. Coś takiego :

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

1
Jak zastosować to do sytuacji, gdy Podmiot jest powiązany z innym (może to być OneToOne)? W tym przypadku, gdy uruchamiam getOriginalEntityData na Entity najwyższego poziomu, oryginalne dane powiązanych z nią jednostek nie są tak naprawdę oryginalne, ale raczej zaktualizowane.
mu4ddi3

5

Możesz śledzić zmiany za pomocą zasad powiadamiania .

Po pierwsze, implementuje interfejs NotifyPropertyChanged :

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

    private $_listeners = array();

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

Następnie po prostu wywołaj _onPropertyChanged dla każdej metody, która zmienia dane, rzuca twoją encję, jak poniżej:

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
Słuchacze wewnątrz jednostki ?! Szaleństwo! Poważnie, polityka śledzenia wygląda na dobre rozwiązanie, czy istnieje sposób na zdefiniowanie słuchaczy poza jednostką (używam Symfony2 DoctrineBundle).
Gildas

To jest złe rozwiązanie. Powinieneś spojrzeć na zdarzenia domeny. github.com/gpslab/domain-event
ghost404


2

W przypadku, gdy ktoś jest nadal zainteresowany w inny sposób niż zaakceptowana odpowiedź (nie działała ona dla mnie i uważam, że jest bardziej niechlujna niż ta w mojej osobistej opinii).

Zainstalowałem pakiet JMS Serializer Bundle i na każdej encji i każdej właściwości, którą uważam za zmianę, dodałem @Group ({"modified_entity_group"}). W ten sposób mogę następnie dokonać serializacji między starą jednostką a zaktualizowaną jednostką, a potem wystarczy powiedzieć $ oldJson == $ updatedJson. Jeśli właściwości, które Cię interesują lub które chciałbyś rozważyć zmiany, JSON nie będzie taki sam, a jeśli nawet chcesz zarejestrować CO konkretnie zmieniono, możesz przekształcić je w tablicę i wyszukać różnice.

Użyłem tej metody, ponieważ interesowało mnie głównie kilka właściwości wielu podmiotów, a nie całość. Przykładem, w którym byłoby to przydatne, jest sytuacja, gdy masz @PrePersist @PreUpdate i masz datę ostatniej_aktualizacji, która będzie zawsze aktualizowana, dlatego zawsze otrzymasz informację, że jednostka została zaktualizowana przy użyciu jednostki pracy i tym podobnych.

Mam nadzieję, że ta metoda jest pomocna dla każdego.


1

Więc ... co zrobić, gdy chcemy znaleźć zestaw zmian poza cyklem życia Doktryny? Jak wspomniałem w moim komentarzu do posta @Ocramius powyżej, być może jest możliwe utworzenie metody „tylko do odczytu”, która nie zakłóca rzeczywistej trwałości Doctrine, ale daje użytkownikowi pogląd na to, co się zmieniło.

Oto przykład tego, o czym myślę ...

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

Jest tu wyrażona jako metoda statyczna, ale może stać się metodą wewnątrz UnitOfWork ...?

Nie jestem na bieżąco ze wszystkimi wewnętrznymi elementami Doctrine, więc mogłem przegapić coś, co ma efekt uboczny lub źle zrozumiał część tego, co robi ta metoda, ale (bardzo) szybki test wydaje mi się, że spodziewam się wyników zobaczyć.

Mam nadzieję, że to komuś pomoże!


1
Cóż, jeśli kiedykolwiek się spotkamy, dostaniesz ostrą piątkę! Bardzo, bardzo dziękuję za to. Bardzo łatwy w użyciu również w 2 innych funkcjach: hasChangesi getChanges(ta ostatnia, aby uzyskać tylko zmienione pola zamiast całego zestawu zmian).
rkeet

0

W moim przypadku do synchronizacji danych z pilota WSdo lokalnegoDB użyłem w ten sposób porównania dwóch encji (sprawdź, czy il stara encja ma różnice w stosunku do edytowanej encji).

Symptycznie klonuję utrwaloną istotę, aby dwa obiekty nie zostały utrwalone:

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

Inna możliwość zamiast bezpośredniego porównywania obiektów:

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

U mnie to działa 1. import EntityManager 2. Teraz możesz użyć tego w dowolnym miejscu w klasie.

  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

Praca z UnitOfWorki computeChangeSets w obrębie Słuchaczy Doktryna zdarzeń jest prawdopodobnie preferowaną metodą.

Jednak : jeśli chcesz przetrwać i opróżnić nową jednostkę w tym słuchaczu, możesz napotkać wiele problemów. Jak się wydaje, jedynym właściwym słuchaczem byłby onFlushwłasny zestaw problemów.

Proponuję więc proste, ale lekkie porównanie, którego można używać w kontrolerach, a nawet usługach, po prostu wstrzykując EntityManagerInterface(zainspirowane @Mohamed Ramrami w poście powyżej):

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

Uwaga : poprawnie porównuje łańcuchy, czasy dat, wartości logiczne, liczby całkowite i zmiennoprzecinkowe, ale nie działa poprawnie na obiektach (z powodu problemów z cyklicznymi odwołaniami). Można by bardziej szczegółowo porównać te obiekty, ale np. W przypadku wykrywania zmiany tekstu jest to wystarczające i znacznie prostsze niż obsługa detektorów zdarzeń.

Więcej informacji:

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.