web-dev-qa-db-ja.com

Doctrine 2エンティティで変更/更新されたすべてのフィールドを取得する組み込みの方法はありますか

エンティティ$eを取得し、セッターでその状態を変更するとします。

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

変更されたフィールドの配列を取得する可能性はありますか?

私の例の場合、結果としてfoo => a, bar => bを取得したいです

PS:はい、私はすべてのアクセサを変更してこの機能を手動で実装できることを知っていますが、これを行う便利な方法を探しています

76
zerkms

_Doctrine\ORM\EntityManager#getUnitOfWork_を使用して_Doctrine\ORM\UnitOfWork_を取得できます。

次に、Doctrine\ORM\UnitOfWork#computeChangeSets()を介して変更セットの計算をトリガーします(管理対象エンティティでのみ動作します)。

オブジェクトグラフ全体を繰り返し処理せずに、チェック対象を正確に把握している場合は、Doctrine\ORM\UnitOfWork#recomputeSingleEntityChangeSet(Doctrine\ORM\ClassMetadata $meta, $entity)などの同様のメソッドを使用することもできます。

その後、Doctrine\ORM\UnitOfWork#getEntityChangeSet($entity)を使用して、オブジェクトに対するすべての変更を取得できます。

それを一緒に入れて:

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

更新されたフィールドを取得しようとしている場合preUpdateリスナー内、変更セットは既に実行されているため、再計算しないでください。 getEntityChangeSetを呼び出すだけで、エンティティに加えられたすべての変更を取得できます。

警告:コメントで説明したように、このソリューションはDoctrineイベントリスナー以外では使用しないでください。これはDoctrineの動作を破壊します。

132
Ocramius

大きな注意記号上記の方法を使用してエンティティの変更を確認したい人のために。

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

$uow->computeChangeSets()メソッドは、上記のソリューションを使用できなくする方法で、永続化ルーチンによって内部的に使用されます。また、メソッドへのコメントに書かれているものは_@internal Don't call from the outside_です。 $uow->computeChangeSets()を使用してエンティティへの変更を確認した後、メソッドの最後で次のコードが実行されます(管理対象エンティティごとに)。

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

_$actualData_配列は、エンティティのプロパティに対する現在の変更を保持します。これらが_$this->originalEntityData[$oid]_に書き込まれるとすぐに、これらのまだ保持されていない変更はエンティティの元のプロパティと見なされます。

後で、変更をエンティティに保存するために$em->persist($entity)が呼び出されると、メソッド$uow->computeChangeSets()も含まれますが、エンティティへの変更を見つけることができなくなります。これらのまだ永続化されていない変更は、エンティティの元のプロパティと見なされるためです。

36

このパブリック(および内部ではない)関数を確認します。

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

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

しなければならないのは、エンティティにtoArrayまたはserialize関数を実装し、差分を作成することだけです。このようなもの :

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

Notify policies で変更を追跡できます。

まず、NotifyPropertyChangedインターフェースを実装します。

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

    private $_listeners = array();

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

次に、データを変更するすべてのメソッドで_ onPropertyChangedを呼び出すだけで、エンティティが次のようにスローされます。

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

誰かがまだ受け入れられている答えとは異なる方法に興味がある場合(それは私のために働いていなかったので、個人的な意見ではこの方法よりも厄介だとわかりました)。

JMS Serializer Bundle をインストールし、各エンティティと、変更を検討する各プロパティに@Group({"changed_entity_group"})を追加しました。この方法で、古いエンティティと更新されたエンティティの間でシリアル化を行うことができます。その後、$ oldJson == $ updatedJsonと言うだけです。興味のあるプロパティや変更を検討したいプロパティが変更された場合、JSONは同じではなく、具体的に変更されたWHATを登録したい場合は、配列に変換して違いを検索できます。

主にエンティティ全体ではなく、一連のエンティティのいくつかのプロパティに興味があるため、この方法を使用しました。これが役立つ例は、@ PrePersistと@PreUpdateがあり、last_updateの日付があり、常に更新されるため、作業単位やそのようなものを使用してエンティティが更新されたことを常に取得する場合です。

この方法が誰にとっても役立つことを願っています。

1
Benjamin Vison

変更を返します

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

だから... Doctrineライフサイクル外のチェンジセットを見つけたい場合はどうすればよいですか?上記の@Ocramius '投稿に関する私のコメントで述べたように、おそらく「読み取り専用」 「実際のDoctrine永続性を混乱させないが、変更された内容のビューをユーザーに提供するメソッド。

ここに私が考えていることの例があります...

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

ここでは静的メソッドとして表現されていますが、UnitOfWork内のメソッドになる可能性があります...?

私はDoctrineのすべての内部を把握しているわけではないので、副作用のあるものを見逃したり、この方法の一部を誤解したかもしれませんが、それを(非常に)簡単にテストすると、期待した結果が得られるようです見る。

これが誰かの助けになることを願っています!

1
caponica

私の場合、リモートWSからローカルDBへの同期データの場合、この方法を使用して2つのエンティティを比較しました(古いエンティティに編集済みエンティティとの差分があることを確認します)。

永続化されたエンティティを単純に複製して、2つのオブジェクトが永続化されないようにします。

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

オブジェクトを直接比較するよりも別の可能性:

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