Symfony vich上传器和doctrine记录扩展问题?

3
我正在使用vich/uploader-bundle这两个库来创建一个带有图片的实体,并使用stof/doctrine-extensions-bundle提供的loggable doctrine扩展记录实体更改历史记录,该扩展由atlantic18/doctrineextensions提供支持。

问题在于:我有一个具有Vich可上传图片字段的实体,并使用注释使用doctrine的Gedmo loggable扩展。

/**
 * @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;

嵌入式实体类App\Entity\Embedded\VersionedFile已经具备了必要的注释,可以通过使用loggable doctrine扩展来实现版本控制。
// 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方法。此侦听器检查更改并安排插入日志条目的时间。

问题是,VichUploader的上传侦听器 (Vich\UploaderBundle\EventListener\Doctrine\UploadListener) 在记录侦听器之后被调用,然后上传文件,这会导致名称、大小等属性的更改。有关名称、大小等计算的更改在LoggableListener中不可用,因为它首先被调用,所以它并不知道应该插入这些更改。

我是否错过了一些配置或者做错了什么?想法是记录对图片所做的更改。目前,在数据库中,日志条目仅包括$pictureUpdatedAt字段。

我调试了这个问题,我能看到的只有顺序,以及在LoggableListener中,getObjectChangeSetData 方法仅返回已更改的 $pictureUpdatedAt 字段。我认为这与嵌入式实体无关,因为我认为侦听器的调用顺序是问题所在。我最初的想法是改变侦听器的优先级,但即使我这样做,调用的顺序也没有改变,主要是因为当 onFlush 被调用时,它会触发 uploader bundle 的 preUpdate 方法,从而触发 UploadListener。

1个回答

4
你是正确的,问题的根源在于 UploadListener 监听 prePersistpreUpdate,而 LoggableListener 监听 onFlush。由于 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,则此侦听器在更新文件字段时删除旧文件。由于它监听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 在上传/清理监听器之后被触发。如果您使用了其他 Gedmo 扩展,您需要确保它们按您需要的顺序加载。VichUploaderExtension 设置的默认值CleanListener 设置为 50,将 UploadListener 设置为 0。您可以在 StofDoctrineExtensionsExtension 中查看 Gedmo 监听器的默认值

对于我来说,我有一个属性名称器依赖于可缩放字段,因此我希望确保 SluggableListenerUploadListener 之前被调用。我还使用 softdeleteable 并希望软删除被记录为 "remove",因此我希望确保在 SoftDeleteableListener 之前注册 LoggableListener。您可以通过覆盖配置中的服务来更改这些优先级。
# 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);
}

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接