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


81

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

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

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

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

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

回答:


150

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の振る舞いを壊します。


4
以下のコメントは、$ em-> computerChangeSets()を呼び出すと、何も変更されていないように見えるため、後で呼び出す通常の$ em-> persist()が壊れることを示しています。もしそうなら、解決策は何ですか、私たちはその関数を呼び出さないのですか?
チャドウィックマイヤー

4
UnitOfWorkのライフサイクルイベントリスナーの外部でこのAPIを使用することは想定されていません。
オクラミウス2014

6
あなたはすべきではありません。これは、ORMの使用目的ではありません。このような場合は、適用された操作の前後にデータのコピーを保持することにより、手動差分を使用してください。
オクラミウス2014

6
@Ocramius、それはそれが使用されることを意図したものではないかもしれませんが、それは間違いなく役に立つでしょう。Doctrineを使用して副作用なしで変化を計算する方法があれば。たとえば、おそらくUOWに新しいメソッド/クラスがあった場合、それを呼び出して一連の変更を要求できます。しかし、これは実際の永続化サイクルを変更/影響することはありません。それは可能ですか?
caponica 2014

3
$ em-> getUnitOfWork()-> getOriginalEntityData($ entity)
Wax Cage

41

上記の方法を使用してエンティティの変更を確認したい場合は、大きな注意が必要です。

$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()れますが、これらのまだ永続化されていない変更はエンティティの元のプロパティと見なされるため、エンティティへの変更を見つけることができなくなります。 。


1
これは、チェックされた回答で@Ocramiusが指定したものとまったく同じです
zerkms 2013年

1
$ uow = clone $ em-> getUnitOfWork(); その問題を解決します
tvlooy 2014年

1
UoWのクローン作成はサポートされておらず、望ましくない結果につながる可能性があります。
オクラミウス2014

9
@Slavik Dereviankoでは、何を提案しますか?ただ電話しないの$uow->computerChangeSets()?またはどのような代替方法?
チャドウィックマイヤー

この投稿は本当に便利ですが(上記の回答に対する大きな警告です)、それ自体では解決策ではありません。代わりに、受け入れられた回答を編集しました。
MatthieuNapoli19年

39

このパブリック(内部ではない)関数を確認してください:

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

教義レポから:

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

あなたがしなければならないのはあなたのエンティティにtoArrayorserialize関数を実装してdiffを作ることだけです。このようなもの :

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

1
エンティティが別のエンティティ(OneToOneの場合もあります)に関連している状況にこれを適用するにはどうすればよいですか?この場合、top-lvlエンティティでgetOriginalEntityDataを実行すると、関連するエンティティの元のデータは実際には元ではなく、更新されます。
mu4ddi3 2018

5

通知ポリシーを使用して変更を追跡できます

まず、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;
        }
    }
}

7
エンティティ内のリスナー?!狂気!真剣に、追跡ポリシーは良い解決策のように見えます、エンティティの外部でリスナーを定義する方法はありますか(私はSymfony2DoctrineBundleを使用しています)。
ギルダス2014

これは間違った解決策です。ドメインイベントに目を向ける必要があります。github.com/gpslab/domain-event
ghost4


2

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

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

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

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


1

では... 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
さて、私たちが会ったことがあれば、あなたは鮮明なハイタッチを手に入れます!どうもありがとうございました。他の2つの関数でも非常に簡単に使用できます:hasChangesおよびgetChanges(後者は、変更セット全体ではなく、変更されたフィールドのみを取得します)。
rkeet 2017

0

私の場合、リモート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

私の場合、エンティティ内の関係の古い値を取得したいので、これに基づいてDoctrine \ ORM \ PersistentCollection :: getSnapshotを使用します


0

それは私のために働きます1.EntityManagerをインポートします2.これでクラスのどこでもこれを使用できます。

  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

Doctrine Event Listenersを操作しUnitOfWork、そのcomputeChangeSets 中で作業することは、おそらく推奨される方法です。

ただし、このリスナー内で新しいエンティティを永続化してフラッシュする場合は、多くの面倒に直面する可能性があります。どうやら、適切なリスナーはonFlushそれ自身の問題のセットだけです。

したがって、単純ですが軽量の比較を提案します。これは、EntityManagerInterface(上記の投稿の@Mohamed Ramramiに触発されて)注入するだけで、コントローラーやサービス内でも使用できます。

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

注意:文字列、日時、ブール、整数、浮動小数点数は正しく比較されますが、オブジェクトでは失敗します(循環参照の問題のため)。これらのオブジェクトをより詳細に比較することもできますが、たとえばテキスト変更の検出の場合、これで十分であり、イベントリスナーを処理するよりもはるかに簡単です。

より詳しい情報:

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.