Есть ли встроенный способ получить все измененные / обновленные поля в сущности Doctrine 2?

64

Предположим, что я получаю объект $e и изменяю его состояние с помощью seters:

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

Есть ли возможность получить массив полей, которые были изменены?

В случае моего примера я хотел бы получить foo => a, bar => b в результате

PS: да, я знаю, что могу модифицировать все аксессоры и реализовать эту функцию вручную, но я ищу удобный способ сделать это

Теги:
doctrine
doctrine2

6 ответов

113
Лучший ответ

Вы можете использовать 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, чтобы получить все изменения, внесенные в объект.

  • 2
    В приведенном ниже комментарии говорится, что если вы вызываете $ em-> computerChangeSets (), это нарушит обычную функцию $ em-> persist (), которую вы вызываете позже, потому что это не будет похоже на то, что что-то изменилось. Если да, то каково решение, мы просто не вызываем эту функцию?
  • 3
    Вы не должны использовать этот API вне прослушивателей событий жизненного цикла UnitOfWork.
Показать ещё 8 комментариев
29

Знак большой оповещения для тех, кто хочет проверить изменения в сущности, используя метод, описанный выше.

$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 указал в проверенном ответе
  • 1
    $ uow = clone $ em-> getUnitOfWork (); решает эту проблему
Показать ещё 3 комментария
10

Проверьте эту общедоступную (а не внутреннюю) функцию:

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

Из доктрины 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 в вашей сущности и выполнить diff. Что-то вроде этого:

$originalData = $em->getUnitOfWork()->getOriginalEntityData($entity);
$toArrayEntity = $entity->toArray();
$changes = array_diff_assoc($toArrayEntity, $originalData);
  • 2
    Идеальное решение, пожалуйста, upvote!
  • 0
    Как применить это к ситуации, когда сущность связана с другой (может быть OneToOne)? В этом случае, когда я запускаю getOriginalEntityData для объекта верхнего уровня, исходные данные связанных с ним объектов на самом деле не являются оригинальными, а скорее обновляются.
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).
  • 0
    Это неправильное решение. Вы должны смотреть в сторону событий домена. github.com/gpslab/domain-event
1

Если кто-то по-прежнему интересуется другим способом, чем принятый ответ (он не работал у меня, и я нашел его более беспорядочным, чем этот, по моему личному мнению).

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

Я использовал этот метод, так как меня интересовали в основном несколько свойств группы объектов, а не целиком. Например, если это было бы полезно, если у вас есть @PrePersist @PreUpdate, и у вас есть дата last_update, которая всегда будет обновляться, поэтому вы всегда будете получать, что объект был обновлен с использованием единицы работы и тому подобного.

Надеюсь, что этот метод полезен для всех.

1

Итак... что делать, когда мы хотим найти набор изменений за пределами жизненного цикла Doctrine? Как упоминалось в моем комментарии к сообщению @Ocramius выше, возможно, можно создать метод "readonly", который не вписывается в фактическую стойкость 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...?

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

Надеюсь, это поможет кому-то!

  • 1
    Ну, если мы когда-нибудь встретимся, вы получите четкую пятерку! Большое спасибо за это. Очень легко использовать также в 2 других функциях: hasChanges и getChanges (последняя, которая получает только измененные поля вместо всего набора изменений).

Ещё вопросы

Сообщество Overcoder
Наверх
Меню