Symfony vich загрузчик и проблема с расширением доктрины?

Я использую эти две библиотеки для создания объекта с изображением, используя vich/uploader-bundle, и я регистрирую изменения объекта история с использованием расширения доктрины loggable, предоставленного из stof/doctrine-extensions-bundle, который предоставляет расширение из atlantic18/doctrineextensions.

Итак, вот проблема: у меня есть объект с полем загружаемого изображения Vich, и он использует расширение 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;

Класс встроенных сущностей 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;

А теперь проблема. Когда я загружаю файл и сохраняю объект, происходит следующее. Диспетчер сущностей сохраняет сущность, и вызывается метод onFlush регистрируемого прослушивателя Gedmo (Gedmo\Loggable\LoggableListener). Эти прослушиватели проверяют изменения и планируют вставку записей журнала.

Проблема в том, что 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, который запускает UploadListener пакета загрузчика.


person Simeon Kolev    schedule 28.06.2019    source источник


Ответы (1)


Вы правы, корень проблемы в UploadListener прослушивает prePersist и preUpdate, пока LoggableListener слушает onFlush. Поскольку onFlush запускается до preUpdate, изменения файлов никогда не регистрируются. Это можно исправить в несколько шагов.

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 для обработки новых загрузок. prePersist запускается до onFlush, поэтому новые загрузки будут устанавливаться позже, чем раньше.

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 срабатывает после наших прослушивателей загрузки/очистки. Если вы используете какие-либо другие расширения Gedmo, вам необходимо убедиться, что они загружены в том порядке, в котором они вам нужны. значения по умолчанию, установленные VichUploaderExtension, устанавливают CleanListener до 50 и от UploadListener до 0. Вы можете увидеть прослушиватель Gedmo по умолчанию в StofDoctrineExtensionsExtension.

Для меня у меня есть имя свойства, которое зависит от slugable поля, поэтому я хочу убедиться, что SluggableListener вызывается до UploadListener. Я также использую softdeleteable и хочу, чтобы мягкое удаление регистрировалось как «удаление», поэтому я хочу убедиться, что LoggableListener зарегистрировано до 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);
}

Теперь просто убедитесь, что ваш кеш очищен.

person jeffhinshaw    schedule 29.08.2019