Gibt es eine integrierte Möglichkeit, alle geänderten / aktualisierten Felder in einer Doctrine 2-Entität abzurufen?

81

Angenommen, ich rufe eine Entität ab $eund ändere ihren Status mit Setzern:

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

Gibt es eine Möglichkeit, ein Array von Feldern abzurufen, die geändert wurden?

In meinem Beispiel möchte ich foo => a, bar => bals Ergebnis abrufen

PS: Ja, ich weiß, dass ich alle Accessoren ändern und diese Funktion manuell implementieren kann, aber ich suche nach einer praktischen Möglichkeit, dies zu tun

zerkms
quelle

Antworten:

148

Sie können verwenden Doctrine\ORM\EntityManager#getUnitOfWork, um eine zu bekommen Doctrine\ORM\UnitOfWork.

Dann lösen Sie einfach die Änderungssatzberechnung aus (funktioniert nur bei verwalteten Entitäten) über Doctrine\ORM\UnitOfWork#computeChangeSets().

Sie können auch ähnliche Methoden verwenden, z. B. Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity)wenn Sie genau wissen, was Sie überprüfen möchten, ohne das gesamte Objektdiagramm zu durchlaufen.

Danach können Sie Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity)alle Änderungen an Ihrem Objekt abrufen.

Etwas zusammensetzen:

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

Hinweis. Wenn Sie versuchen, die aktualisierten Felder in einem PreUpdate-Listener abzurufen , berechnen Sie den Änderungssatz nicht neu, da dies bereits geschehen ist. Rufen Sie einfach getEntityChangeSet auf, um alle an der Entität vorgenommenen Änderungen abzurufen.

Warnung: Wie in den Kommentaren erläutert, sollte diese Lösung nicht außerhalb von Doctrine-Ereignis-Listenern verwendet werden. Dies wird das Verhalten der Lehre brechen.

Ocramius
quelle
4
Der folgende Kommentar besagt, dass wenn Sie $ em-> computerChangeSets () aufrufen, das reguläre $ em-> persist (), das Sie später aufrufen, beschädigt wird, da es nicht so aussieht, als würde etwas geändert. Wenn ja, was ist die Lösung, nennen wir diese Funktion einfach nicht?
Chadwick Meyer
4
Sie dürfen diese API nicht außerhalb der Listener für Lebenszyklusereignisse von UnitOfWork verwenden.
Ocramius
6
Das solltest du nicht. Dafür ist das ORM nicht gedacht. Verwenden Sie in solchen Fällen die manuelle Abweichung, indem Sie vor und nach den angewendeten Vorgängen eine Kopie der Daten aufbewahren.
Ocramius
6
@Ocramius, es ist vielleicht nicht das, wofür es verwendet werden soll, aber es wäre zweifellos nützlich . Wenn es nur eine Möglichkeit gäbe, Doctrine zu verwenden, um die Änderungen ohne Nebenwirkungen zu berechnen. Wenn es beispielsweise eine neue Methode / Klasse gibt, möglicherweise in der UOW, die Sie aufrufen können, um nach einer Reihe von Änderungen zu fragen. Dies würde jedoch den tatsächlichen Persistenzzyklus in keiner Weise verändern / beeinflussen. Ist das möglich?
Caponica
3
Sehen Sie bessere Lösung von Mohamed Ramrami unten mit $ em-> getUnitOfWork () -> getOriginalEntityData ($ entity)
Wax Cage
41

Großes Vorsichtzeichen für diejenigen, die mit der oben beschriebenen Methode nach Änderungen an der Entität suchen möchten.

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

Die $uow->computeChangeSets()Methode wird intern von der persistierenden Routine so verwendet, dass die obige Lösung unbrauchbar wird. Das steht auch in den Kommentaren zur Methode : @internal Don't call from the outside. Nach dem Überprüfen der Änderungen an den Entitäten mit $uow->computeChangeSets()wird am Ende der Methode (für jede verwaltete Entität) der folgende Code ausgeführt:

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

Das $actualDataArray enthält die aktuellen Änderungen an den Eigenschaften der Entität. Sobald diese geschrieben sind $this->originalEntityData[$oid], werden diese noch nicht beibehaltenen Änderungen als die ursprünglichen Eigenschaften der Entität betrachtet.

Später, wenn das $em->persist($entity)aufgerufen wird, um die Änderungen an der Entität zu speichern, wird auch die Methode $uow->computeChangeSets()einbezogen. Jetzt können die Änderungen an der Entität nicht mehr gefunden werden, da diese noch nicht beibehaltenen Änderungen als die ursprünglichen Eigenschaften der Entität betrachtet werden .

Slavik Derevianko
quelle
1
Es ist genau das gleiche, was @Ocramius in der überprüften Antwort angegeben hat
zerkms
1
$ uow = clone $ em-> getUnitOfWork (); löst dieses Problem
tvlooy
1
Das Klonen der UoW ​​wird nicht unterstützt und kann zu unerwünschten Ergebnissen führen.
Ocramius
9
@Slavik Derevianko also was schlägst du vor? Einfach nicht anrufen $uow->computerChangeSets()? oder welche alternative Methode?
Chadwick Meyer
Obwohl dieser Beitrag wirklich nützlich ist (es ist eine große Warnung zu der obigen Antwort), ist er keine Lösung für sich. Ich habe stattdessen die akzeptierte Antwort bearbeitet.
Matthieu Napoli
37

Überprüfen Sie diese öffentliche (und nicht interne) Funktion:

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

Aus dem Doktrin- 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)

Alles, was Sie tun müssen, ist eine toArrayoder serializeFunktion in Ihrer Entität zu implementieren und einen Unterschied zu machen. Etwas wie das :

$originalData = $em->getUnitOfWork()->getOriginalEntityData($entity);
$toArrayEntity = $entity->toArray();
$changes = array_diff_assoc($toArrayEntity, $originalData);
Mohamed Ramrami
quelle
1
Wie kann dies auf Situationen angewendet werden, in denen eine Entität mit einer anderen verbunden ist (kann OneToOne sein)? In diesem Fall, wenn ich getOriginalEntityData auf der obersten Entität ausführe, sind die Originaldaten der zugehörigen Entitäten nicht wirklich original, sondern werden aktualisiert.
Mu4ddi3
5

Sie können die Änderungen mit Benachrichtigungsrichtlinien verfolgen .

Implementiert zunächst die NotifyPropertyChanged- Schnittstelle:

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

    private $_listeners = array();

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

Rufen Sie dann einfach _onPropertyChanged für jede Methode auf, die Daten ändert, und werfen Sie Ihre Entität wie folgt aus:

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;
        }
    }
}
Manix
quelle
7
Zuhörer in einer Entität?! Wahnsinn! Im Ernst, Tracking-Richtlinien scheinen eine gute Lösung zu sein. Gibt es eine Möglichkeit, Listener außerhalb der Entität zu definieren (ich verwende das Symfony2 DoctrineBundle).
Gildas
Dies ist die falsche Lösung. Sie sollten sich die Domain-Ereignisse ansehen. github.com/gpslab/domain-event
ghost404
3

Es werden Änderungen zurückgegeben

$entityManager->getUnitOfWork()->getEntityChangeSet($entity)
Omar Makled
quelle
es ist so offensichtlich.
Rawburner
2

Für den Fall, dass jemand immer noch an einer anderen Art als der akzeptierten Antwort interessiert ist (es hat bei mir nicht funktioniert und ich fand es meiner persönlichen Meinung nach chaotischer als auf diese Weise).

Ich habe das JMS Serializer Bundle installiert und auf jeder Entität und auf jeder Eigenschaft, die ich als Änderung betrachte, eine @Group ({"defined_entity_group"}) hinzugefügt. Auf diese Weise kann ich dann eine Serialisierung zwischen der alten Entität und der aktualisierten Entität vornehmen. Danach muss nur noch $ oldJson == $ updatedJson gesagt werden. Wenn sich die Eigenschaften, an denen Sie interessiert sind oder die Sie berücksichtigen möchten, nicht ändern, ist der JSON nicht identisch. Wenn Sie sogar registrieren möchten, WAS speziell geändert wurde, können Sie ihn in ein Array umwandeln und nach den Unterschieden suchen.

Ich habe diese Methode verwendet, da ich mich hauptsächlich für einige Eigenschaften einer Reihe von Entitäten interessierte und nicht für die Entität vollständig. Ein Beispiel, bei dem dies nützlich wäre, ist, wenn Sie ein @ PrePersist @ PreUpdate und ein Datum für das letzte Update haben, das immer aktualisiert wird. Daher erhalten Sie immer, dass die Entität mit einer Arbeitseinheit und dergleichen aktualisiert wurde.

Hoffe, diese Methode ist für jeden hilfreich.

Benjamin Vison
quelle
1

Also ... was tun, wenn wir einen Änderungssatz außerhalb des Doctrine-Lebenszyklus finden möchten? Wie in meinem Kommentar zu @Ocramius 'Beitrag oben erwähnt, ist es möglicherweise möglich, eine "schreibgeschützte" Methode zu erstellen, die nicht mit der tatsächlichen Doctrine-Persistenz in Konflikt gerät, sondern dem Benutzer einen Überblick darüber gibt, was sich geändert hat.

Hier ist ein Beispiel dafür, woran ich denke ...

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

Es wird hier als statische Methode formuliert, könnte aber zu einer Methode in UnitOfWork werden ...?

Ich bin nicht mit allen Interna von Doctrine auf dem Laufenden, habe also möglicherweise etwas übersehen, das einen Nebeneffekt hat oder einen Teil der Funktionsweise dieser Methode missverstanden hat, aber ein (sehr) schneller Test scheint mir die erwarteten Ergebnisse zu liefern sehen.

Ich hoffe das hilft jemandem!

Caponica
quelle
1
Nun, wenn wir uns jemals treffen, bekommen Sie eine knackige High Five! Vielen Dank für diesen einen. Sehr einfach, auch in 2 anderen Funktionen zu verwenden: hasChangesund getChanges(letztere, um nur die geänderten Felder anstelle des gesamten Änderungssatzes zu erhalten).
Rkeet
0

In meinem Fall habe ich zum Synchronisieren von Daten von einer entfernten WSzu einer lokalen DBEntität auf diese Weise zwei Entitäten verglichen (überprüfen Sie, ob die alte Entität von der bearbeiteten Entität abweicht).

Ich klone die persistierte Entität sympathisch, damit zwei Objekte nicht persistiert werden:

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

Eine andere Möglichkeit, anstatt Objekte direkt zu vergleichen:

<?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
}
kxo
quelle
0

In meinem Fall möchte ich den alten Wert der Beziehung in der Entität erhalten, daher verwende ich die Doctrine \ ORM \ PersistentCollection :: getSnapshot-Basis auf dieser Basis

ghazaleh javaheri
quelle
0

Es funktioniert für mich 1. EntityManager importieren 2. Jetzt können Sie dies überall in der Klasse verwenden.

  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';
    }
Khandaker Toihidul Islam
quelle
0

Die Arbeit mit UnitOfWorkund computeChangeSets innerhalb eines Doctrine Event Listeners ist wahrscheinlich die bevorzugte Methode.

Allerdings : Wenn Sie eine neue Entität in diesem Listener beibehalten und löschen möchten, kann dies zu erheblichen Problemen führen. Wie es scheint, wäre der einzig richtige Zuhörer onFlushmit seinen eigenen Problemen.

Daher schlage ich einen einfachen, aber leichten Vergleich vor, der innerhalb von Controllern und sogar Diensten verwendet werden kann, indem einfach das injiziert wird EntityManagerInterface(inspiriert von @Mohamed Ramrami im obigen Beitrag):

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

Hinweis : Es vergleicht Zeichenfolgen, Datums- und Uhrzeitangaben, Bools, Ganzzahlen und Gleitkommazahlen korrekt, schlägt jedoch bei Objekten fehl (aufgrund der Zirkelreferenzprobleme). Man könnte diese Objekte genauer vergleichen, aber für die Erkennung von Textänderungen ist dies ausreichend und viel einfacher als die Handhabung von Ereignis-Listenern.

Mehr Info:

ptmr.io
quelle