我正在使用这两个库使用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
具有所有必需的注释,以便使用可记录的学说扩展名正确地进行版本控制。
// 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 s 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 the
LoggableListener`是因为首先被调用,因此它不知道应该插入它们。
我缺少某些配置还是做错了什么?这个想法是记录对图片所做的更改。现在,在数据库中,日志条目仅由$pictureUpdatedAt
字段组成。
我调试了问题,仅能看到顺序,并且在LoggableListener
中,方法getObjectChangeSetData
仅返回已更改的$pictureUpdatedAt
字段。我认为这与嵌入式实体没有共同点,因为我认为侦听器的调用顺序是问题所在。我的第一个想法是更改侦听器的优先级,但是即使我这样做,调用的顺序也没有改变,主要是因为在调用onFlush
时,它正在触发preUpdate
方法,该方法将触发{{ 1}}。
答案 0 :(得分:2)
您是正确的,问题的根源是UploadListener
监听prePersist
和preUpdate
,而LoggableListener
监听onFlush
。由于onFlush
在preUpdate
之前被触发,因此永远不会记录文件更改。只需几个步骤即可解决。
首先,您可以编写自己的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
之前被触发,因此这将使新上传的设置比以前晚。
接下来,我们现在必须创建一个新的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];
}
}
现在,我们需要使用刚刚编写的监听器覆盖配置中的默认监听器。
# 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
如果这还不够,那么接下来会带来另一个问题:侦听器优先级。至少,我们需要确保在上传/清除监听器之后触发LoggableListener
。如果您使用任何其他Gedmo扩展,则需要确保按照所需顺序加载它们。 defaults set by VichUploaderExtension将CleanListener
设置为50
,将UploadListener
设置为0
。您可以在StofDoctrineExtensionsExtension
中看到Gedmo Listener defaults。
对我来说,我有一个依赖于缓慢字段的属性命名器,因此我想确保在UploadListener
之前调用SluggableListener
。我还使用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);
}
现在,只需确保清除缓存即可。