Проверьте, изменилось ли учение доктрины Symfony из представления формы

Вопрос

Могу ли я использовать диспетчер объектов Doctrine (или некоторую другую функцию Symfony), чтобы проверить, обновлен ли объект?

Фон

Я создаю CMS с возможностью сохранять "версии" каждой страницы. Таким образом, у меня есть аннотированная сущность Doctrine $view (которая в основном является "страницей" ), и этот объект имеет вложенные ассоциированные объекты, такие как $view->version (которые содержат большую часть информации, которая может быть обновлена ​​в разных версиях). отредактированный стандартной формой Symfony в CMS. Когда форма отправлена, она выполняет $em->persist($view), а Entity Manager определяет, было ли изменено какое-либо из полей. Если есть изменения, изменения сохраняются. изменения, менеджер объектов игнорирует сохранение и сохраняет вызов базы данных для обновления. Отлично.

Но до того, как сущность будет сохранена, моя система проверки версий проверяет, прошло ли это более 30 минут с момента последнего сохранения текущей версии или если пользователь, отправляющий форму, отличается от пользователя, который сохранил текущую версию, и если это так он клонирует $viewVersion. Таким образом, основная запись для $view остается одним и тем же идентификатором, но она работает с обновленной версией. Это отлично работает.

ОДНАКО... Если прошло какое-то время со времени последнего сохранения, и кто-то просто смотрит на запись, ничего не меняя и удаляя, я не хочу, чтобы система версий автоматически клонировала новую версию. Я хочу проверить и подтвердить, что объект фактически изменил. Менеджер Entity Manager делает это до сохранения объекта. Но я не могу полагаться на него, потому что, прежде чем позвонить $em->persist($view), я должен клонировать $view->version. Но перед тем, как я клонировал $view->version, мне нужно проверить, обновлено ли какое-либо из полей в сущности или вложенных объектах.

Основное решение

Решение должно рассчитать набор изменений:

$form = $this->createForm(new ViewType(), $view);
if ($request->isMethod( 'POST' )) {
    $form->handleRequest($request);
    if( $form->isValid() ) {
        $changesFound = array();
        $uow = $em->getUnitOfWork();
        $uow->computeChangeSets();

        // The Version (hard coded because it dynamically associated)
        $changeSet = $uow->getEntityChangeSet($view->getVersion());
        if(!empty($changeSet)) {
             $changesFound = array_merge($changesFound, $changeSet);
        }
        // Cycle through Each Association
        $metadata = $em->getClassMetadata("GutensiteCmsBundle:View\ViewVersion");
        $associations = $metadata->getAssociationMappings();
        foreach($associations AS $k => $v) {
            if(!empty($v['cascade'])
                && in_array('persist', $v['cascade'])
            ){
                $fn = 'get'.ucwords($v['fieldName']);
                $changeSet = $uow->getEntityChangeSet($view->getVersion()->{$fn}());
                if(!empty($changeSet)) {
                      $changesFound = array_merge($changesFound, $changeSet);
                 }
            }
        }
    }
}

Осложнение

Но я читал, что вы не должны использовать этот $uow->computerChangeSets() вне прослушивателя событий жизненного цикла. Говорят, что вы должны делать ручную разницу объектов, например. $version !== $versionOriginal. Но это не работает, потому что некоторые поля, такие как timePublish, всегда обновляются, поэтому они всегда разные. Так действительно ли невозможно использовать это для getEntityChangeSets() в контексте контроллера (вне прослушивателя событий)?

Как использовать прослушиватель событий? Я не знаю, как собрать все части.

ОБНОВЛЕНИЕ 1

Я пошел за советом и создал прослушиватель событий onFlush, и предположительно это должно загружаться автоматически. Но теперь страница имеет большую ошибку, которая возникает, когда мое определение службы для gutensite_cms.listener.is_versionable проходит в другой службе моего arguments: [ "@gutensite_cms.entity_helper" ]:

Fatal error: Uncaught exception 'Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException' with message 'Circular reference detected for service "doctrine.dbal.cms_connection", path: "doctrine.dbal.cms_connection".' in /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php:456 Stack trace: #0 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(604): Symfony\Component\DependencyInjection\Dumper\PhpDumper->addServiceInlinedDefinitionsSetup('doctrine.dbal.c...', Object(Symfony\Component\DependencyInjection\Definition)) #1 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(630): Symfony\Component\DependencyInjection\Dumper\PhpDumper->addService('doctrine.dbal.c...', Object(Symfony\Component\DependencyInjection\Definition)) #2 /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php(117): Symfony\Componen in /var/www/core/cms/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php on line 456

Определение моей службы

# This is the helper class for all entities (included because we reference it in the listener and it breaks it)
gutensite_cms.entity_helper:
    class: Gutensite\CmsBundle\Service\EntityHelper
    arguments: [ "@doctrine.orm.cms_entity_manager" ]

gutensite_cms.listener.is_versionable:
    class: Gutensite\CmsBundle\EventListener\IsVersionableListener
    #only pass in the services we need
    # ALERT!!! passing this service actually causes a giant symfony fatal error
    arguments: [ "@gutensite_cms.entity_helper" ]
    tags:
        - {name: doctrine.event_listener, event: onFlush }

Мой прослушиватель событий: Gutensite\CmsBundle\EventListener\isVersionableListener

class IsVersionableListener
{


    private $entityHelper;

    public function __construct(EntityHelper $entityHelper) {
        $this->entityHelper = $entityHelper;
    }

    public function onFlush(OnFlushEventArgs $eventArgs)
    {

        // this never executes... and without it, the rest doesn't work either
        print('ON FLUSH EXECUTING');
        exit;

        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();
        $updatedEntities = $uow->getScheduledEntityUpdates();

        foreach($updatedEntities AS $entity) {

            // This is generic listener for all entities that have an isVersionable method (e.g. ViewVersion)
            // TODO: at the moment, we only want to do the following code for the viewVersion entity

            if (method_exists($entity, 'isVersionable') && $entity->isVersionable()) {

                // Get the Correct Repo for this entity (this will return a shortcut 
                // string for the repo, e.g. GutensiteCmsBundle:View\ViewVersion
                $entityShortcut = $this->entityHelper->getEntityBundleShortcut($entity);
                $repo = $em->getRepository($entityShortcut);

                // If the repo for this entity has an onFlush method, use it.
                // This allows us to keep the functionality in the entity repo
                if(method_exists($repo, 'onFlush')) {
                    $repo->onFlush($em, $entity);
                }

            }
        }

    }
}

ViewVersion Repo с событием onFlush: Gutensite\CmsBundle\Entity\View\ViewVersionRepository

/**
     * This is referenced by the onFlush event for this entity.
     *
     * @param $em
     * @param $entity
     */
    public function onFlush($em, $entity) {

        /**
         * Find if there have been any changes to this version (or it associated entities). If so, clone the version
         * which will reset associations and force a new version to be persisted to the database. Detach the original
         * version from the view and the entity manager so it is not persisted.
         */


        $changesFound = $this->getChanges($em, $entity);

        $timeModMin = (time() - $this->newVersionSeconds);

        // TODO: remove test
        print("\n newVersionSeconds: ".$this->newVersionSeconds);
        //exit;

        /**
         * Create Cloned Version if Necessary
         * If it has been more than 30 minutes since last version entity was save, it probably a new session.
         * If it is a new user, it is a new session
         * NOTE: If nothing has changed, nothing will persist in doctrine normally and we also won't find changes.
         */
        if($changesFound


            /**
             * Make sure it been more than default time.
             * NOTE: the timeMod field (for View) will NOT get updated with the PreUpdate annotation
             * (in /Entity/Base.php) if nothing has changed in the entity (it not updated).
             * So the timeMod on the $view entity may not get updated when you update other entities.
             * So here we reference the version timeMod.
            */
            && $entity->getTimeMod() < $timeModMin
            // TODO: check if it is a new user editing
            // && $entity->getUserMod() ....
        ) {
            $this->iterateVersion($em, $entity);
        }

    }


    public function getChanges($em, $entity) {

        $changesFound = array();

        $uow = $em->getUnitOfWork();
        $changes = $uow->getEntityChangeSet($entity);

        // Remove the timePublish as a valid field to compare changes. Since if they publish an existing version, we
        // don't need to iterate a version.
        if(!empty($changes) && !empty($changes['timePublish'])) unset($changes['timePublish']);
        if(!empty($changes)) $changesFound = array_merge($changesFound, $changes);

        // The Content is hard coded because it dynamically associated (and won't be found by the generic method below)
        $changes = $uow->getEntityChangeSet($entity->getContent());
        if(!empty($changes)) $changesFound = array_merge($changesFound, $changes);

        // Check Additional Dynamically Associated Entities
        // right now it just settings, but if we add more in the future, this will catch any that are
        // set to cascade = persist
        $metadata = $em->getClassMetadata("GutensiteCmsBundle:View\ViewVersion");
        $associations = $metadata->getAssociationMappings();
        foreach($associations AS $k => $v) {
            if(!empty($v['cascade'])
                && in_array('persist', $v['cascade'])
            ){
                $fn = 'get'.ucwords($v['fieldName']);
                $changes = $uow->getEntityChangeSet($entity->{$fn}());
                if(!empty($changeSet)) $changesFound = array_merge($changesFound, $changes);
            }
        }

        if(!$changesFound) $changesFound = NULL;
        return $changesFound;

    }




    /**
     * NOTE: This function gets called onFlush, before the entity is persisted to the database.
     *
     * VERSIONING:
     * In order to calculate a changeSet, we have to compare the original entity with the form submission.
     * This is accomplished with a global onFlush event listener that automatically checks if the entity is versionable,
     * and if it is, checks if an onFlush method exists on the entity repository. $this->onFlush compares the unitOfWork
     * changeSet and then calls this function to iterate the version.
     *
     * In order for versioning to work, we must
     *

     *
    */


    public function iterateVersion($em, $entity) {


        $persistType = 'version';


        // We use a custom __clone() function in viewVersion, viewSettings, and ViewVersionTrait (which is on each content type)

        // It ALSO sets the viewVersion of the cloned version, so that when the entity is persisted it can properly set the settings

        // Clone the version
        // this clones the $view->version, and the associated entities, and resets the associated ids to null

        // NOTE: The clone will remove the versionNotes, so if we decide we actually want to keep them
        // We should fetch them before the clone and then add them back in manually.
        $version = clone $entity();

        // TODO: Get the changeset for the original notes and add the versionNotes back
        //$version->setVersionNotes($versionModified->getVersionNotes());

        /**
         * Detach original entities from Entity Manager
         */

        // VERSION:
        // $view->version is not an associated entity with cascade=detach, it just an object container that we
        // manually add the current "version" to. But it is being managed by the Entity Manager, so
        // it needs to be detached

        // TODO: this can probably detach ($entity) was originally $view->getVersion()
        $em->detach($entity);

        // SETTINGS: The settings should cascade detach.

        // CONTENT:
        // $view->getVersion()->content is also not an associated entity, so we need to manually
        // detach the content as well, since we don't want the changes to be saved
        $em->detach($entity->getContent());


        // Cloning removes the viewID from this cloned version, so we need to add the new cloned version
        // to the $view as another version
        $entity->getView()->addVersion($version);


        // TODO: If this has been published as well, we need to mark the new version as the view version,
        // e.g. $view->setVersionId($version->getId())
        // This is just for reference, but should be maintained in case we need to utilize it
        // But how do we know if this was published? For the time being, we do this in the ContentEditControllerBase->persist().


    }

Ответ 1

Итак, я понимаю, что вам в основном нужно определить, собирается ли доктрина обновлять сущность в базе данных, чтобы вы могли записать это изменение или вставить версию старого объекта.

Как вы должны это сделать, добавив слушателя к событию onFlush. Вы можете больше узнать о регистрации событий доктрины здесь.

Например, вам нужно добавить в свой конфигурационный файл новое определение службы:

my.flush.listener:
        class: Gutensite\CmsBundle\EventListener\IsVersionableListener
        calls:
            - [setEntityHelper, ["@gutensite_cms.entity_helper"]]
        tags:
            -  {name: doctrine.event_listener, event: onFlush}

Затем вы создадите класс EventListener, как любой сервис symfony. В этом классе будет вызываться функция с тем же именем, что и событие, (onFlush в этом случае)

Внутри этой функции вы можете просмотреть все обновленные объекты:

namespace Gutensite\CmsBundle\EventListener;

class IsVersionableListener {

    private $entityHelper;

    public function onFlush(OnFlushEventArgs $eventArgs)
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();
        $updatedEntities = $uow->getScheduledEntityUpdates();

        foreach ($updatedEntities as $entity) {
            if ($entity->isVersionable()) {
                $changes = $uow->getEntityChangeSet($entity);
                //Do what you want with the changes...
            }
        }
    }

    public function setEntityHelper($entityHelper)
    {
        $this->entityHelper = $entityHelper;

        return $this;
    }
}

$entity->isVersionable() - это всего лишь метод, который я составил, который вы можете добавить к своим объектам, чтобы легко решить, отслеживается ли этот объект для изменений или нет.

ПРИМЕЧАНИЕ.. Поскольку вы делаете это в onFlush. Это означает, что все изменения, которые будут сохранены в БД, были вычислены. Учение не будет сохраняться ни в каких новых сущностях. Если вы создаете новые объекты, вам нужно будет вручную вычислить изменения и сохранить их.

Ответ 2

Первое: существует версия версия для Doctrine (она недавно была переименована в Loggable), которая делает именно то, что вы описываете, проверяйте это, возможно, оно решает ваш прецедент.

С учетом сказанного это звучит как работа для слушателя событий onFlush. UnitOfWork уже находится в состоянии "с переменным вычислением", где вы можете просто запросить все изменения для всех объектов (вы можете отфильтровать их с помощью instanceof или что-то в этом роде).

Это все еще не решает проблему с сохранением новой и старой версии. Я не уверен на 100%, что это сработает, потому что сохранение чего-то в прослушивателе onFlush будет связано с обходными методами (так как выполнение flush в onFlush приведет к бесконечному циклу), но есть $em- > refresh ($ entity), который будет откат объекта в состояние "по умолчанию" (поскольку он был создан из базы данных).

Итак, вы можете попробовать что-то вроде, проверить, есть ли изменения в объекте, если есть, клонировать его, перенести на новый, обновить старый и сохранить. Вы должны будете сделать дополнительную работу для своих отношений, потому что клонирование создает только мелкую копию в PHP.

Я бы посоветовал использовать расширяемое расширение, так как он все понял, но прочитал в onFlush listener, возможно, вы можете что-то придумать.


Ответ 3

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

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

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

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