Symfony vich 上传器和学说可记录扩展问题?

Symfony vich uploader and doctrine loggable extension problem?

我正在使用这两个库创建一个实体,该实体具有使用 vich/uploader-bundle and I am logging entity changes history using the loggable doctrine extension provided from stof/doctrine-extensions-bundle which provides the extension from atlantic18/doctrineextensions 的图片。

问题来了:我有一个具有 Vich 可上传图片字段的实体,它使用带有注释的 doctrine 的 Gedmo 可记录扩展。

/**
 * @var VersionedFile
 *
 * @ORM\Embedded(class="App\Entity\Embedded\VersionedFile")
 *
 * @Gedmo\Versioned()
 */
private $picture;

/**
 * @var File
 *
 * @Vich\UploadableField(
 *     mapping="user_picture",
 *     fileNameProperty="picture.name",
 *     size="picture.size",
 *     mimeType="picture.mimeType",
 *     originalName="picture.originalName",
 *     dimensions="picture.dimensions
 * )
 */
private $pictureFile;

/**
 * @var DateTimeInterface
 *
 * @ORM\Column(type="datetime", nullable=true)
 *
 * @Gedmo\Versioned()
 */
private $pictureUpdatedAt;

嵌入式实体 class App\Entity\Embedded\VersionedFile 具有所有需要的注释,以便使用可记录的学说扩展正确地进行版本控制。

// Not the whole code but just to get the idea for property versioning

/**
 * @ORM\Column(name="name", nullable=true)
 *
 * @Gedmo\Versioned()
 */
protected $name;

现在是问题。当我上传文件并保留实体时,会发生以下情况。实体管理器保留实体并调用 Gedmo 可记录侦听器 (Gedmo\Loggable\LoggableListener) 的 onFlush 方法。此侦听器检查更改并安排要插入的日志条目。

问题是 VichUploaders upload listener (Vich\UploaderBundle\EventListener\Doctrine\UploadListener) is called after the loggable listener and then the file is uploaded which changes the properties name, size, etc. The computed changes about name, size, etc. are not available in theLoggableListener` 因为它先被调用所以它不知道应该插入它们。

我是不是遗漏了某些配置,还是做错了什么。这个想法是记录对图片所做的更改。目前在数据库中,日志条目仅包含 $pictureUpdatedAt 字段。

我调试了问题,我只能看到顺序,在 LoggableListener 中,方法 getObjectChangeSetData 仅返回已更改的 $pictureUpdatedAt 字段。我不认为这与嵌入式实体有任何共同之处,因为我认为听众的调用顺序是问题所在。我的第一个想法是改变监听器的优先级,但即使我这样做了,调用的顺序也没有改变,主要是因为当 onFlush 被调用时,它触发了 preUpdate 方法,该方法触发了 [=上传程序包的 26=]。

你是对的,问题的根源是UploadListener listens to prePersist and preUpdate while the LoggableListeneronFlush。由于 onFlushpreUpdate 之前触发,因此永远不会记录文件更改。这可以通过几个步骤解决。

1。创建新的 UploadListener

首先,您可以编写自己的 UploadListener 来监听 onFlush

// src/EventListener/VichUploadListener.php using Flex
// src/AppBundle/EventListener/VichUploadListener.php otherwise
namespace App\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Vich\UploaderBundle\EventListener\Doctrine\UploadListener;

class VichUploadListener extends UploadListener
{
    public function onFlush(OnFlushEventArgs $args): void
    {
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            $this->preUpdate(new LifecycleEventArgs($entity, $em));
        }

        // Required if using property namer on sluggable field. Otherwise, you
        // can also subscribe to "prePersist" and remove this foreach.
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
            // We use "preUpdate" here so the changeset is recomputed.
            $this->preUpdate(new LifecycleEventArgs($entity, $em));
        }
    }

    public function getSubscribedEvents(): array
    {
        return [Events::onFlush];
    }
}

在这个例子中,我重用了原来的 UploadListener 让事情变得更简单。由于我们正在收听 onFlush,因此在上传文件后重新计算实体变更集很重要,这就是为什么我对计划的更新和插入使用 "preUpdate" 方法。

像这样更改事件时,您必须小心。如果您有另一个侦听器期望您的文件字段之一的值被设置(或取消设置),这可能会改变预期的行为。如果您使用第二个 foreach 来处理新的上传,则尤其如此。 prePersistonFlush 之前触发,因此这会使新上传的设置比以前晚。

2。创建新的 CleanListener

接下来,我们现在必须创建一个新的 CleanListener。如果 delete_on_update 设置为 true,则此侦听器会在我们更新文件字段时删除旧文件。由于它监听 preUpdate,我们必须将其更改为 onFlush,以便正确删除旧文件。

// src/EventListener/VichCleanListener.php on Flex
// src/AppBundle/EventListener/VichCleanListener.php otherwise
namespace App\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Vich\UploaderBundle\EventListener\Doctrine\CleanListener;

class VichCleanListener extends CleanListener
{
    public function onFlush(OnFlushEventArgs $args): void
    {
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            $this->preUpdate(new LifecycleEventArgs($entity, $em));
        }
    }

    public function getSubscribedEvents(): array
    {
        return [Events::onFlush];
    }
}

3。配置新侦听器

现在,我们需要用刚刚编写的配置覆盖默认侦听器。

# config/services.yaml on Flex
# app/config/services.yml otherwise
services:
    # ...

    vich_uploader.listener.upload.orm:
        class: 'App\EventListener\VichUploadListener'
        parent: 'vich_uploader.listener.doctrine.base'
        autowire: false
        autoconfigure: false
        public: false
    vich_uploader.listener.clean.orm:
        class: 'App\EventListener\VichCleanListener'
        parent: 'vich_uploader.listener.doctrine.base'
        autowire: false
        autoconfigure: false
        public: false

4。更改 Gedmo 扩展优先级

如果这还不够,那么现在您提出的另一个问题是:侦听器优先级。至少,我们需要确保 LoggableListener 在我们的 upload/clean 侦听器之后被触发。如果您正在使用任何其他 Gedmo 扩展,您需要确保它们按照您需要的顺序加载。 StofDoctrineExtensionsExtension.

中的defaults set by VichUploaderExtension set the CleanListener to 50 and the UploadListener to 0. You can see the Gedmo Listener defaults

对我来说,我有一个 属性 依赖于 sluggable 字段的命名器,所以我想确保 SluggableListener is called before the UploadListener. I also use softdeleteable and want soft deletes logged as "remove", so I want to make sure LoggableListener is registered before SoftDeleteableListener。您可以通过覆盖配置中的服务来更改这些优先级。

# config/services.yaml on Flex
# app/config/services.yml otherwise
services:
    # ...

    stof_doctrine_extensions.listener.sluggable:
        class: '%stof_doctrine_extensions.listener.sluggable.class%'
        autowire: false
        autoconfigure: false
        public: false
        calls:
            - { method: 'setAnnotationReader', arguments: ['@annotation_reader'] }
        tags:
            - { name: 'doctrine.event_subscriber', connection: 'default', priority: 5 }

    stof_doctrine_extensions.listener.loggable:
        class: '%stof_doctrine_extensions.listener.loggable.class%'
        autowire: false
        autoconfigure: false
        public: false
        calls:
            - { method: 'setAnnotationReader', arguments: ['@annotation_reader'] }
        tags:
            - { name: 'doctrine.event_subscriber', connection: 'default', priority: -1 }

    stof_doctrine_extensions.listener.softdeleteable:
        class: '%stof_doctrine_extensions.listener.softdeleteable.class%'
        autowire: false
        autoconfigure: false
        public: false
        calls:
            - { method: 'setAnnotationReader', arguments: ['@annotation_reader'] }
        tags:
            - { name: 'doctrine.event_subscriber', connection: 'default', priority: -2 }

或者,您可以创建一个编译器传递来仅更改这些服务的 doctrine.event_subscriber 标记的优先级。

// src/DependencyInjection/Compiler/DoctrineExtensionsCompilerPass.php on Flex
// src/AppBundle/DependencyInjection/Compiler/DoctrineExtensionsCompilerPass.php otherwise
namespace App\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class DoctrineExtensionsCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $listenerPriorities = [
            'sluggable' => 5,
            'loggable' => -1,
            'softdeleteable' => -2,
        ];

        foreach ($listenerPriorities as $ext => $priority) {
            $id = sprintf('stof_doctrine_extensions.listener.%s', $ext);

            if (!$container->hasDefinition($id)) {
                continue;
            }

            $definition = $container->getDefinition($id);
            $tags = $definition->getTag('doctrine.event_subscriber');
            $definition->clearTag('doctrine.event_subscriber');

            foreach ($tags as $tag) {
                $tag['priority'] = $priority;
                $definition->addTag('doctrine.event_subscriber', $tag);
            }
        }
    }
}

如果你走这条路,请确保注册编译器传递具有更高的优先级(高于 0)以确保它在 RegisterEventListenersAndSubscribersPass.

之前是 运行
// src/Kernel.php on Flex
// src/AppBundle/AppBundle.php otherwsie

// ...

use App\DependencyInjection\Compiler\DoctrineExtensionsCompilerPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;

// ...

protected function build(ContainerBuilder $container)
{
    $container->addCompilerPass(new DoctrineExtensionsCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 5);
}

现在,只需确保您的缓存已清除。