2014-07-14 22:25
浏览 43

检查Symfony Doctrine实体是否已从表单提交更改


Can I use the Doctrine entity manager (or some other Symfony function) to check if an entity has been updated?


I am building a CMS with the ability to save "versions" of each page. So I have a Doctrine annotated entity $view (which is basically the "page), and this entity has nested associated entities like $view->version (which contain the majority of the information that can be updated in different revisions). This entity is edited with a standard Symfony form in the CMS. When the form is submitted, it does a $em->persist($view) and the Entity Manager detects if any of the fields have been changed. If there are changes, the changes are persisted. If there are no changes, the entity manager ignores the persist and saves itself a database call to update. Great.

But before the entity is saved, my versioning system checks if it's been more than 30 minutes since the current version was last save, or if the user submitting the form is different than the user who saved the current version, and if so it clones the $viewVersion. So the main record for $view remains the same id, but it works from an updated revision. This works great.

HOWEVER... If it's been a while since the last save, and someone just looks at the record without changing anything, and hits save, I don't want the version system to clone a new version automatically. I want to check and confirm that the entity has actually changed. The Entity Manager does this before persisting an entity. But I can't rely on it because before I call $em->persist($view) I have to clone $view->version. But before I clone $view->version I need to check if any of the fields in the entity or it's nested entities have been updated.

Basic Solution

The solution is to calculate the change set:

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

        // 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) {
                && in_array('persist', $v['cascade'])
                $fn = 'get'.ucwords($v['fieldName']);
                $changeSet = $uow->getEntityChangeSet($view->getVersion()->{$fn}());
                if(!empty($changeSet)) {
                      $changesFound = array_merge($changesFound, $changeSet);

The Complication

But I read that you shouldn't use this $uow->computerChangeSets() outside of a the lifecycle events listener. They say you should do a manual diff of the objects, e.g. $version !== $versionOriginal. But that doesn't work because some fields like timePublish always get updated, so they are always different. So is it really not possible to use this to getEntityChangeSets() in the context of a controller (outside of an event listener)?

How should I use an Event Listener? I don't know how to put all the pieces together.


I followed the advice and created an onFlush event listener, and presumably that should load automatically. But now the page has a big error which happens when my service definition for gutensite_cms.listener.is_versionable passes in another service of 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

My Service Definition

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

    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" ]
        - {name: doctrine.event_listener, event: onFlush }

My Event Listener: 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');

        $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 with onFlush Event: 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
 newVersionSeconds: ".$this->newVersionSeconds);

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

             * 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) {
                && 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.
     * 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

         * 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()

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

        // Cloning removes the viewID from this cloned version, so we need to add the new cloned version
        // to the $view as another 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().

  • 写回答
  • 好问题 提建议
  • 追加酬金
  • 关注问题
  • 邀请回答

3条回答 默认 最新

相关推荐 更多相似问题