web-dev-qa-db-fra.com

Vérifier si une entité de la doctrine Symfony a changé depuis la soumission du formulaire

Question

Puis-je utiliser le gestionnaire d'entités Doctrine (ou une autre fonction Symfony) pour vérifier si une entité a été mise à jour?

Contexte

Je construis un CMS avec la possibilité de sauvegarder des "versions" de chaque page. J'ai donc une entité annotée Doctrine $view (qui est essentiellement la "page"), et cette entité a des entités associées imbriquées telles que $view->version (qui contient la majorité des informations pouvant être mises à jour dans différentes révisions). Cette entité est modifiée avec une norme Formulaire Symfony dans le CMS. Lorsque le formulaire est soumis, il effectue une $em->persist($view) et Entity Manager détecte si l'un des champs a été modifié. Si des modifications sont apportées, les modifications sont conservées. S'il n'y a pas de modifications, le gestionnaire d'entités ignore le persist et enregistre lui-même un appel de base de données à mettre à jour.

Mais avant que l’entité ne soit enregistrée, mon système de gestion de versions vérifie si plus de 30 minutes se sont écoulées depuis la dernière sauvegarde de la version actuelle, ou si l’utilisateur qui soumet le formulaire est différent de celui qui a enregistré la version actuelle et, le cas échéant, le clone. $viewVersion. Donc, l'enregistrement principal pour $view reste le même id, mais il fonctionne à partir d'une révision mise à jour. Cela fonctionne très bien.

CEPENDANT ... Si cela fait un moment depuis la dernière sauvegarde et que quelqu'un ne regarde que l'enregistrement sans rien changer, et que la sauvegarde est réussie, je ne veux pas que le système de version clone automatiquement une nouvelle version. Je veux vérifier et confirmer que l'entité a réellement changé . Le gestionnaire d'entités le fait avant de persister une entité. Mais je ne peux pas compter dessus car avant d'appeler $em->persist($view), je dois cloner $view->version. Mais avant de cloner $view->version, je dois vérifier si l'un des champs de l'entité ou ses entités imbriquées ont été mis à jour.

Solution de base

La solution consiste à calculer l'ensemble de modifications:

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

La complication

Mais j'ai lu que vous ne devriez pas utiliser cette $uow->computerChangeSets() en dehors d'un écouteur d'événements de cycle de vie . Ils disent que vous devriez faire un diff manuel des objets, par exemple. $version !== $versionOriginal. Mais cela ne fonctionne pas car certains champs tels que timePublish sont toujours mis à jour, ils sont donc toujours différents. Donc, est-il vraiment impossible d'utiliser ceci pour getEntityChangeSets() dans le contexte d'un contrôleur (en dehors d'un écouteur d'événement)?

Comment utiliser un Event Listener? Je ne sais pas comment assembler toutes les pièces.

MISE À JOUR 1

J'ai suivi le conseil et créé un écouteur d'événement onFlush, qui devrait probablement se charger automatiquement. Mais maintenant, la page a une grosse erreur qui se produit lorsque ma définition de service pour gutensite_cms.listener.is_versionable passe dans un autre service de la mine 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

Ma définition de service

# 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 }

Mon écouteur d'événement: 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 avec l'événement 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'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().


    }
12
Chadwick Meyer

Donc, si j'ai bien compris, vous devez fondamentalement détecter si la doctrine va mettre à jour une entité dans la base de données afin que vous puissiez enregistrer cette modification ou insérer une version de l'ancienne entité.

Pour ce faire, vous devez ajouter un écouteur à l'événement onFlush. Vous pouvez en savoir plus sur l'enregistrement d'événements de doctrine ici .

Par exemple, vous devrez ajouter à votre fichier de configuration une nouvelle définition de service comme celle-ci:

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

Ensuite, vous créerez la classe EventListener comme n’importe quel service symfony. Dans cette classe, une fonction portant le même nom que l'événement sera appelée (onFlush dans ce cas)

Dans cette fonction, vous pouvez parcourir toutes les entités mises à jour:

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() est simplement une méthode que j'ai composée et que vous pouvez ajouter à vos entités pour décider facilement si cette entité est suivie ou non pour des modifications.

NOTE: Depuis que vous faites cela dans le onFlush. Cela signifie que toutes les modifications qui seront enregistrées dans la base de données ont été calculées. La doctrine ne persistera pas avec de nouvelles entités. Si vous créez de nouvelles entités, vous devrez calculer manuellement les modifications et les conserver.

6
Ramy Nasr

Première chose: il y a une extension versionable pour Doctrine (récemment renommée Loggable), qui fait exactement ce que vous décrivez, vérifiez-la, cela résoudra peut-être votre cas d'utilisation.

Cela dit, cela ressemble à un travail pour un écouteur d'événement onFlush. UnitOfWork est déjà dans un état "changements calculés", dans lequel vous pouvez simplement demander toutes les modifications sur toutes les entités (vous pouvez les filtrer avec une instance de, ou quelque chose comme ça).

Cela ne résout toujours pas le problème de l'enregistrement d'une nouvelle version, ainsi que de l'ancienne version. Je ne suis pas sûr à 100% que cela fonctionnera, car persister quelque chose dans un auditeur onFlush impliquera des solutions de contournement (car un flush dans un onFlush donnera une boucle infinie), mais il y a $ em-> refresh ($ entity) rétablit une entité à son état "par défaut" (telle qu'elle a été construite à partir de la base de données).

Vous pouvez donc essayer quelque chose comme: vérifiez si l'entité a été modifiée, s'il y en a une, clonez-la, conservez la nouvelle, actualisez l'ancienne et sauvegardez-la. Cependant, vous devrez faire des démarches supplémentaires pour vos relations, car le clonage ne crée qu'une copie superficielle en PHP.

Je conseillerais d’aller avec l’extension versionable, car elle contient tout ce qui a été déterminé, mais lisez aussi sur le écouteur onFlush aussi, vous pouvez peut-être trouver quelque chose.


1
K. Norbert

Dans le cas où quelqu'un est toujours intéressé par une manière différente de la réponse acceptée (cela ne fonctionnait pas pour moi et je l'ai trouvée plus compliquée que mon point de vue personnel).

J'ai installé l'ensemble de sérialiseur JMS et sur chaque entité et sur chaque propriété que je considère comme un changement, j'ai ajouté un @Group ({"modified_entity_group"}). De cette façon, je peux ensuite faire une sérialisation entre l'ancienne entité et l'entité mise à jour, après quoi il suffit de dire $ oldJson == $ updatedJson. Si les propriétés qui vous intéressent ou que vous souhaitez prendre en compte changent, le code JSON ne sera pas le même et si vous souhaitez même enregistrer WHAT spécifiquement modifié, vous pouvez le transformer en tableau et rechercher les différences.

J'ai utilisé cette méthode car je m'intéressais principalement à quelques propriétés d'un groupe d'entités et non à l'entité tout entière. Cela serait utile, par exemple, si vous avez un @PrePersist @PreUpdate et une date de dernière mise à jour, qui sera toujours mise à jour. Vous obtiendrez donc toujours que l'entité a été mise à jour à l'aide de l'unité de travail, etc.

J'espère que cette méthode est utile à tout le monde.

0
Benjamin Vison