From ffc8db18da3cf85d86a86b95033ae91f43cbc5d3 Mon Sep 17 00:00:00 2001 From: Warxcell Date: Mon, 13 Sep 2021 00:53:26 +0300 Subject: [PATCH] Add transactions support --- .idea/php.xml | 6 +- README.md | 22 +++--- composer.json | 5 +- .../ArxyFilesExtension.php | 16 ++++- src/EventListener/DoctrineORMListener.php | 68 +++++++++++++------ src/Manager.php | 6 +- tests/Functional/ManagerTest.php | 38 +++++++++-- 7 files changed, 112 insertions(+), 49 deletions(-) diff --git a/.idea/php.xml b/.idea/php.xml index 803509b..4284e20 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -51,7 +51,6 @@ - @@ -122,6 +121,9 @@ + + + @@ -135,4 +137,4 @@ - \ No newline at end of file + diff --git a/README.md b/README.md index bad03a1..9bde985 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ services: Arxy\FilesBundle\Twig\FilesExtension: tags: - - { name: twig.extension } + - {name: twig.extension} Arxy\FilesBundle\NamingStrategy\IdToPathStrategy: ~ Arxy\FilesBundle\NamingStrategy\AppendExtensionStrategy: @@ -153,9 +153,9 @@ services: Arxy\FilesBundle\Storage\FlysystemStorage: ~ - Arxy\FilesBundle\Storage: + Arxy\FilesBundle\Storage: alias: 'Arxy\FilesBundle\Storage\FlysystemStorage' - + Arxy\FilesBundle\Manager: $class: 'App\Entity\File' @@ -163,15 +163,15 @@ services: alias: Arxy\FilesBundle\Manager Arxy\FilesBundle\EventListener\DoctrineORMListener: - arguments: [ "@Arxy\\FilesBundle\\ManagerInterface" ] # This can be omit, if using autowiring. + arguments: ["@Arxy\\FilesBundle\\ManagerInterface"] # This can be omit, if using autowiring. tags: - - { name: doctrine.event_listener, event: 'postPersist' } - - { name: doctrine.event_listener, event: 'preRemove' } + - {name: doctrine.event_listener, event: 'postPersist'} + - {name: doctrine.event_listener, event: 'preRemove'} Arxy\FilesBundle\Form\Type\FileType: - arguments: [ "@Arxy\\FilesBundle\\ManagerInterface" ] # This can be omit, if using autowiring. + arguments: ["@Arxy\\FilesBundle\\ManagerInterface"] # This can be omit, if using autowiring. tags: # This can be omit, if using autowiring. - - { name: form.type } + - {name: form.type} ``` or using pure PHP @@ -623,7 +623,7 @@ bin/console arxy:files:migrate-naming-strategy ```yaml MicrosoftAzure\Storage\Blob\BlobRestProxy: - factory: [ 'MicrosoftAzure\Storage\Blob\BlobRestProxy', 'createBlobService' ] + factory: ['MicrosoftAzure\Storage\Blob\BlobRestProxy', 'createBlobService'] arguments: $connectionString: 'DefaultEndpointsProtocol=https;AccountName=xxxxxxxx;EndpointSuffix=core.windows.net' @@ -728,7 +728,7 @@ There is also DelegatingManager, which can be used as router to different other ```yaml Arxy\FilesBundle\DelegatingManager: - $managers: [ '@manager_1', '@manager_2' ] + $managers: ['@manager_1', '@manager_2'] ``` Then you can do: `$manager->getManagerFor(File::class)->upload($file)`. Note: If you do @@ -975,4 +975,4 @@ Currently, only image preview generator exists. You can add your own image previ # Known issues -- If file entity is deleted within transaction and transaction is rolled back - file will be deleted. I'm waiting for DBAL 3.2.* release to be able to fix that. +No known issues. diff --git a/composer.json b/composer.json index 8f480f6..6dfcc33 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,9 @@ "gabrielelana/byte-units": "^0.5.0" }, "require-dev": { + "doctrine/dbal": "3.2.x-dev", "symfony/validator": "*", - "doctrine/orm": "*", + "doctrine/orm": "3.0.x-dev", "symfony/form": "*", "symfony/http-foundation": "*", "twig/twig": "*", @@ -29,7 +30,7 @@ "symfony/dependency-injection": "^5.2", "infection/infection": "^0.21.4", "symfony/symfony": "^4.4 | ^5.2", - "doctrine/doctrine-bundle": "^2.3", + "doctrine/doctrine-bundle": "2.5.x-dev", "liip/imagine-bundle": "^2.6", "vimeo/psalm": "^4.7", "league/flysystem-bundle": "^2.0", diff --git a/src/DependencyInjection/ArxyFilesExtension.php b/src/DependencyInjection/ArxyFilesExtension.php index b36561c..a3e2b2e 100644 --- a/src/DependencyInjection/ArxyFilesExtension.php +++ b/src/DependencyInjection/ArxyFilesExtension.php @@ -12,6 +12,8 @@ use Arxy\FilesBundle\Storage; use Arxy\FilesBundle\Twig\FilesExtension; use Arxy\FilesBundle\Twig\FilesRuntime; +use Doctrine\DBAL\Events as DbalEvents; +use Doctrine\ORM\Events as OrmEvents; use LogicException; use Psr\EventDispatcher\EventDispatcherInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -136,9 +138,17 @@ private function createListenerDefinition(string $driver, string $serviceId): De case 'orm': $definition = new Definition(DoctrineORMListener::class); $definition->setArgument('$manager', new Reference($serviceId)); - $definition->addTag('doctrine.event_listener', ['event' => 'postPersist', 'lazy' => true]); - $definition->addTag('doctrine.event_listener', ['event' => 'postRemove', 'lazy' => true]); - $definition->addTag('doctrine.event_listener', ['event' => 'onClear', 'lazy' => true]); + $definition->addTag('doctrine.event_listener', ['event' => OrmEvents::postPersist, 'lazy' => true]); + $definition->addTag('doctrine.event_listener', ['event' => OrmEvents::postRemove, 'lazy' => true]); + $definition->addTag('doctrine.event_listener', ['event' => OrmEvents::onClear, 'lazy' => true]); + $definition->addTag( + 'doctrine.event_listener', + ['event' => DbalEvents::onTransactionCommit, 'lazy' => true] + ); + $definition->addTag( + 'doctrine.event_listener', + ['event' => DbalEvents::onTransactionRollBack, 'lazy' => true] + ); return $definition; default: diff --git a/src/EventListener/DoctrineORMListener.php b/src/EventListener/DoctrineORMListener.php index c6766e4..d879d42 100644 --- a/src/EventListener/DoctrineORMListener.php +++ b/src/EventListener/DoctrineORMListener.php @@ -6,8 +6,9 @@ use Arxy\FilesBundle\ManagerInterface; use Arxy\FilesBundle\Model\File; -use Closure; use Doctrine\Common\Util\ClassUtils; +use Doctrine\DBAL\Event\TransactionCommitEventArgs; +use Doctrine\DBAL\Event\TransactionRollBackEventArgs; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\LifecycleEventArgs; use ReflectionObject; @@ -15,21 +16,15 @@ final class DoctrineORMListener { private ManagerInterface $manager; + /** @var class-string */ private string $class; - private Closure $move; - private Closure $remove; + private array $pendingMove = []; + private array $pendingRemove = []; public function __construct(ManagerInterface $manager) { $this->class = $manager->getClass(); $this->manager = $manager; - - $this->move = static function (File $file) use ($manager): void { - $manager->moveFile($file); - }; - $this->remove = static function (File $file) use ($manager): void { - $manager->remove($file); - }; } public function postPersist(LifecycleEventArgs $eventArgs): void @@ -37,9 +32,11 @@ public function postPersist(LifecycleEventArgs $eventArgs): void $entity = $eventArgs->getEntity(); $entityManager = $eventArgs->getEntityManager(); if ($this->supports($entity)) { - ($this->move)($entity); + $this->pendingMove[] = $entity; + } + foreach ($this->handleEmbeddable($entityManager, $entity) as $file) { + $this->pendingMove[] = $file; } - $this->handleEmbeddable($entityManager, $entity, $this->move); } public function postRemove(LifecycleEventArgs $eventArgs): void @@ -48,14 +45,48 @@ public function postRemove(LifecycleEventArgs $eventArgs): void $entityManager = $eventArgs->getEntityManager(); if ($this->supports($entity)) { - ($this->remove)($entity); + $this->pendingRemove[] = $entity; + } + foreach ($this->handleEmbeddable($entityManager, $entity) as $file) { + $this->pendingRemove[] = $file; + } + } + + public function onTransactionCommit(TransactionCommitEventArgs $eventArgs): void + { + if ($eventArgs->getConnection()->isTransactionActive()) { + return; + } + + $pendingMove = $this->pendingMove; + foreach ($pendingMove as $file) { + $this->manager->moveFile($file); } - $this->handleEmbeddable($entityManager, $entity, $this->remove); + + $pendingRemove = $this->pendingRemove; + foreach ($pendingRemove as $file) { + $this->manager->remove($file); + } + + $this->pendingMove = []; + $this->pendingRemove = []; + } + + public function onTransactionRollBack(TransactionRollBackEventArgs $eventArgs): void + { + if ($eventArgs->getConnection()->isTransactionActive()) { + return; + } + + $this->pendingMove = []; + $this->pendingRemove = []; } public function onClear(): void { $this->manager->clear(); + $this->pendingMove = []; + $this->pendingRemove = []; } private function supports(object $entity): bool @@ -63,11 +94,8 @@ private function supports(object $entity): bool return $entity instanceof $this->class; } - private function handleEmbeddable( - EntityManagerInterface $entityManager, - object $entity, - Closure $action - ): void { + private function handleEmbeddable(EntityManagerInterface $entityManager, object $entity): iterable + { $classMetadata = $entityManager->getClassMetadata(ClassUtils::getClass($entity)); foreach ($classMetadata->embeddedClasses as $property => $embeddedClass) { @@ -84,7 +112,7 @@ private function handleEmbeddable( if ($file === null) { continue; } - $action($file); + yield $file; } } } diff --git a/src/Manager.php b/src/Manager.php index 3e39a37..ee2c132 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -170,11 +170,7 @@ public function moveFile(File $file): void /** @psalm-suppress RedundantCondition */ if (is_resource($stream)) { - try { - ErrorHandler::wrap(static fn (): bool => fclose($stream)); - } catch (ErrorException $e) { - // nothing we can do - } + fclose($stream); } if ($this->eventDispatcher !== null) { diff --git a/tests/Functional/ManagerTest.php b/tests/Functional/ManagerTest.php index 92ec52b..3795308 100644 --- a/tests/Functional/ManagerTest.php +++ b/tests/Functional/ManagerTest.php @@ -10,6 +10,8 @@ use SplFileObject; use SplTempFileObject; +use function md5; + class ManagerTest extends AbstractFunctionalTest { protected ManagerInterface $embeddableManager; @@ -44,6 +46,10 @@ public function testSimpleUpload(): File $file = $this->manager->upload(new SplFileObject(__DIR__ . '/../files/image1.jpg')); $this->entityManager->persist($file); + + self::assertFalse( + $this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1') + ); $this->entityManager->flush(); self::assertTrue( @@ -191,16 +197,38 @@ public function testSimpleDelete(): void self::assertFalse($this->flysystem->fileExists($pathname)); } + public function testFileNotUploadedWithRollBack(): void + { + $file = $this->manager->upload(new SplFileObject(__DIR__ . '/../files/image1.jpg')); + + $this->entityManager->beginTransaction(); + + $this->entityManager->persist($file); + + self::assertFalse( + $this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1') + ); + $this->entityManager->flush(); + + self::assertFalse( + $this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1') + ); + + $this->entityManager->commit(); + + self::assertTrue( + $this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1') + ); + } + /** * @depends testSimpleUpload */ public function testFileNotDeletedWithRollback(): void { - self::markTestSkipped('Not implemented yet. Waiting DBAL 3.2.X Release'); - $file = $this->testSimpleUpload(); - $filepath = '9aa1c5fc/7c938816/6d7ce7fd/46648dd1/9aa1c5fc7c9388166d7ce7fd46648dd1'; + $filepath = '9aa1c5fc7c9388166d7ce7fd46648dd1'; self::assertTrue($this->flysystem->fileExists($filepath)); $this->entityManager->beginTransaction(); @@ -223,11 +251,9 @@ public function testFileNotDeletedWithRollback(): void */ public function testFileDeletedWithCommit(): void { - self::markTestSkipped('Not implemented yet. Waiting DBAL 3.2.X Release'); - $file = $this->testSimpleUpload(); - $filepath = '9aa1c5fc/7c938816/6d7ce7fd/46648dd1/9aa1c5fc7c9388166d7ce7fd46648dd1'; + $filepath = '9aa1c5fc7c9388166d7ce7fd46648dd1'; self::assertTrue($this->flysystem->fileExists($filepath));