簡體   English   中英

檢查Symfony Doctrine實體是否已從表單提交更改

[英]Check if a Symfony Doctrine Entity has Changed from Form Submission

我可以使用Doctrine實體管理器(或其他一些Symfony函數)來檢查實體是否已更新?

背景

我正在構建一個能夠保存每個頁面“版本”的CMS。 所以我有一個Doctrine注釋實體$view (基本上是“頁面”),這個實體有嵌套的關聯實體,比如$view->version (包含可以在不同版本中更新的大部分信息)。在CMS中使用標准Symfony表單進行編輯。提交表單時,它執行$em->persist($view) ,實體管理器檢測是否有任何字段已更改。如果有更改,則更改如果沒有更改,實體管理器將忽略持久化並保存自己的數據庫調用以進行更新。

但是在保存實體之前,我的版本控制系統會檢查自當前版本上次保存以來是否超過30分鍾,或者提交表單的用戶與保存當前版本的用戶不同,如果是,則克隆$viewVersion 所以$view的主要記錄仍然是相同的id,但它可以從更新的版本開始工作。 這非常有效。

但是......如果自上次保存以來已經有一段時間了,有人只是在不改變任何內容的情況下查看記錄,並且點擊保存,我不希望版本系統自動克隆新版本。 我想檢查並確認實體已實際更改 實體管理器在持久化實體之前執行此操作。 但是我不能依賴它,因為在我調用$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's 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()是否真的不可能使用它來獲取getEntityChangeSets() )?

我該如何使用事件監聽器 我不知道怎么把所有碎片放在一起。

更新1

我按照建議並創建了一個onFlush事件監聽器,大概應該自動加載。 但是現在頁面有一個很大的錯誤,當我的gutensite_cms.listener.is_versionable服務定義傳遞給我的另一個arguments: [ "@gutensite_cms.entity_helper" ]服務時會發生這種錯誤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);
                }

            }
        }

    }
}

使用onFlush事件的ViewVersion回購: 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's 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's 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's 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's not updated).
             * So the timeMod on the $view entity may not get updated when you update other entities.
             * So here we reference the version's 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's 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's 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's 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().


    }

所以我的理解是你基本上需要檢測doctrine是否要更新數據庫中的實體,以便記錄該更改或插入舊實體的版本。

你應該這樣做的方法是為onFlush事件添加一個監聽器。 您可以在此處閱讀有關注冊學說事件的更多信息。

例如,您需要在配置文件中添加一個新的服務定義:

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

然后,您將像任何symfony服務一樣創建類EventListener 在這個類中,將調用與事件同名的函數,(在本例中為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()只是我$entity->isVersionable()一個方法,您可以將其添加到實體中,以便輕松決定是否跟蹤此實體是否有更改。

注意:因為您在onFlush中執行此操作。 這意味着已經計算了將保存到DB的所有更改。 學說不會堅持任何新實體。 如果您創建新實體,則需要手動計算更改並保留它們。

第一件事: Doctrine有一個可版本化的擴展 (它最近被重命名為Loggable),它正是你所描述的,檢查出來,也許它解決了你的用例。

話雖如此,這聽起來像onFlush事件監聽器的工作。 UnitOfWork已經處於“更改計算”狀態,您可以在其中詢問所有實體的所有更改(您可以使用instanceof或類似的東西過濾它們)。

這仍然無法解決有關保存新版本和舊版本的問題。 我不是100%肯定這會起作用,因為在onFlush監聽器中持久存在會涉及變通方法(因為在onFlush中執行刷新會導致無限循環),但是會有$ em-> refresh($ entity)將實體回滾到其“默認”狀態(因為它是從數據庫構造的)。

所以你可以嘗試類似的東西,檢查實體是否有變化,如果有,克隆它,堅持新的,刷新舊的,並保存它們。 但是,您必須為您的關系做額外的工作,因為克隆只會在PHP中創建一個淺表副本。

我建議使用可版本化的擴展,因為它已經找到了所有內容,但是也可以讀取onFlush監聽器 ,也許你可以想出一些東西。


如果有人仍然對接受的答案采取不同的方式感興趣(這對我不起作用,而且在個人看來我發現它比這種方式更麻煩)。

我安裝了JMS Serializer Bundle,並且在每個實體和每個屬性上,我考慮了一個更改,我添加了@Group({“changed_entity_group”})。 這樣,我就可以在舊實體和更新后的實體之間進行序列化,之后只需說$ oldJson == $ updatedJson即可。 如果您感興趣的屬性或您想要考慮的屬性發生更改,則JSON將不同,如果您甚至想要注冊具體更改的內容,則可以將其轉換為數組並搜索差異。

我使用這種方法,因為我主要對一堆實體的一些屬性感興趣,而不是完全在實體中。 這將是有用的一個例子是,如果你有一個@PrePersist @PreUpdate並且你有一個last_update日期,那將永遠更新,因此你將始終得到實體是使用工作單元和類似的東西更新的。

希望這種方法對任何人都有幫助。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM