Чи існує вбудований спосіб отримати всі змінені / оновлені поля в сутності Doctrine 2


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

Примітка. Якщо ви намагаєтесь отримати оновлені поля в програмі попереднього оновлення , не переобчислюйте набір змін, як це вже було зроблено. Просто зателефонуйте getEntityChangeSet, щоб отримати всі зміни, внесені в сутність.

Попередження: Як пояснюється в коментарях, це рішення не слід використовувати поза слухачами подій Doctrine. Це порушить поведінку Доктрини.


4
У коментарі нижче сказано, що якщо ви зателефонуєте $ em-> computerChangeSets (), це призведе до порушення звичайного $ em-> persist (), який ви викликаєте пізніше, оскільки це не буде виглядати як щось змінене. Якщо так, то яке рішення, чи просто ми не викликаємо цю функцію?
Chadwick Meyer

4
Ви не повинні використовувати цей API за межами прослуховувачів подій життєвого циклу UnitOfWork.
Окрамій,

6
Ти не повинен. Це не те, для чого призначений ORM. У таких випадках використовуйте ручну різницю, зберігаючи копію даних до та після застосованих операцій.
Окрамій,

6
@Ocramius, можливо, це не те, для чого він призначений, але це, безсумнівно, буде корисним . Якби тільки існував спосіб використовувати Доктрину для обчислення змін без побічних ефектів. Наприклад, якщо був новий метод / клас, можливо, в UOW, до якого ви могли б зателефонувати, щоб попросити масив змін. Але що жодним чином не змінить / вплине на фактичний цикл збереження. Це можливо?
капоніка

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

1
$ uow = клон $ em-> getUnitOfWork (); вирішує цю проблему
tvlooy

1
Клонування UoW не підтримується і може призвести до небажаних результатів.
Окрамій

9
@ Славік Деревянко, то що ти пропонуєш? Тільки не дзвоніть $uow->computerChangeSets()? чи який альтернативний метод?
Chadwick Meyer

Хоча ця публікація дійсно корисна (вона є великим попередженням до відповіді вище), вона сама по собі не є рішенням. Натомість я відредагував прийняту відповідь.
Наполі

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)

Все, що вам потрібно зробити, це реалізувати функцію toArrayабо serializeфункцію у вашій сутності та зробити різницю. Щось на зразок цього :

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

1
Як застосувати це до ситуації, коли сутність пов'язана з іншою (може бути OneToOne)? У цьому випадку, коли я запускаю getOriginalEntityData у сутності top-lvl, оригінальні дані суміжних сутностей насправді не є оригінальними, а скоріше оновлені.
mu4ddi3

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
Слухачі всередині сутності ?! Божевілля! Серйозно кажучи, політика відстеження виглядає як хороше рішення, чи є спосіб визначити слухачів за межами сутності (я використовую Symfony2 DoctrineBundle).
Gildas

Це неправильне рішення. Ви повинні дивитись на події домену. github.com/gpslab/domain-event
ghost404


2

На випадок, якщо когось все-таки цікавить інший спосіб, ніж прийнята відповідь (для мене це не спрацювало, і я вважав це більш брудним, ніж такий спосіб, на мою особисту думку).

Я встановив набір JMS Serializer Bundle, а також на кожну сутність і на кожну властивість, яку я вважаю зміною, додав @Group ({"changed_entity_group"}). Таким чином, я можу потім зробити серіалізацію між старою суттю та оновленою суттю, і після цього це просто питання сказати $ oldJson == $ updatedJson. Якщо властивості, які вас цікавлять, або які ви хочете розглянути, JSON не буде однаковим, і якщо ви навіть хочете зареєструвати ЩО конкретно змінено, ви можете перетворити його на масив і шукати відмінності.

Я використав цей метод, оскільки мене цікавили переважно декілька властивостей групи сутностей, а не сутність цілком. Прикладом, коли це було б корисно, є, якщо у вас є @PrePersist @PreUpdate і у вас є дата останнього оновлення, яка завжди буде оновлюватися, тому ви завжди отримуватимете, що сутність оновлена ​​за допомогою одиниці роботи та подібних речей.

Сподіваюся, цей метод корисний кожному.


1

Отже ... що робити, коли ми хочемо знайти набір змін поза життєвим циклом Доктрини? Як згадувалось у моєму коментарі до публікації @Ocramius вище, можливо, можливо створити метод "лише для читання", який не змішуватиметься з фактичною стійкістю Доктрини, але дає користувачеві уявлення про те, що змінилося.

Ось приклад того, про що я думаю ...

/**
 * 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 ...?

Я не в курсі всіх внутрішніх аспектів Доктрини, тому міг пропустити щось, що має побічний ефект або неправильно зрозуміти частину того, що робить цей метод, але (дуже) швидкий тест, здається, дає мені результати, яких я очікую бачити.

Сподіваюся, це комусь допоможе!


1
Ну, якщо ми коли-небудь зустрінемось, ви отримаєте чітку п’ятірку! Щиро дякую за це. Дуже простий у використанні також у 2 інших функціях: hasChangesі getChanges(остання отримати лише змінені поля замість цілого набору змін).
rkeet

0

У моєму випадку для синхронізації даних з віддаленого WSна локальний DBя використав цей спосіб для порівняння двох сутностей (перевірте, чи старий об’єкт відрізняється від відредагованого).

Я з симпатією клоную персистуючу сутність, щоб два об'єкти не зберігалися:

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

Праця з слухачами подій доктрини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;
        }
    }
}

Примітка : він порівнює рядки, дати, були, цілі числа та плаваючі дані, проте не спрацьовує на об'єктах (через проблеми з циркулярним посиланням). Можна порівнювати ці об’єкти більш глибоко, але, наприклад, для виявлення текстових змін цього досить і набагато простіше, ніж обробляти прослуховувачі подій.

Більше інформації:

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.