Skip to content

Commit 0e0a24c

Browse files
committed
Add transactions support
1 parent 49766a1 commit 0e0a24c

File tree

7 files changed

+146
-50
lines changed

7 files changed

+146
-50
lines changed

.idea/php.xml

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ services:
142142

143143
Arxy\FilesBundle\Twig\FilesExtension:
144144
tags:
145-
- { name: twig.extension }
145+
- {name: twig.extension}
146146

147147
Arxy\FilesBundle\NamingStrategy\IdToPathStrategy: ~
148148
Arxy\FilesBundle\NamingStrategy\AppendExtensionStrategy:
@@ -153,25 +153,25 @@ services:
153153

154154
Arxy\FilesBundle\Storage\FlysystemStorage: ~
155155

156-
Arxy\FilesBundle\Storage:
156+
Arxy\FilesBundle\Storage:
157157
alias: 'Arxy\FilesBundle\Storage\FlysystemStorage'
158-
158+
159159
Arxy\FilesBundle\Manager:
160160
$class: 'App\Entity\File'
161161

162162
Arxy\FilesBundle\ManagerInterface:
163163
alias: Arxy\FilesBundle\Manager
164164

165165
Arxy\FilesBundle\EventListener\DoctrineORMListener:
166-
arguments: [ "@Arxy\\FilesBundle\\ManagerInterface" ] # This can be omit, if using autowiring.
166+
arguments: ["@Arxy\\FilesBundle\\ManagerInterface"] # This can be omit, if using autowiring.
167167
tags:
168-
- { name: doctrine.event_listener, event: 'postPersist' }
169-
- { name: doctrine.event_listener, event: 'preRemove' }
168+
- {name: doctrine.event_listener, event: 'postPersist'}
169+
- {name: doctrine.event_listener, event: 'preRemove'}
170170

171171
Arxy\FilesBundle\Form\Type\FileType:
172-
arguments: [ "@Arxy\\FilesBundle\\ManagerInterface" ] # This can be omit, if using autowiring.
172+
arguments: ["@Arxy\\FilesBundle\\ManagerInterface"] # This can be omit, if using autowiring.
173173
tags: # This can be omit, if using autowiring.
174-
- { name: form.type }
174+
- {name: form.type}
175175
```
176176
177177
or using pure PHP
@@ -623,7 +623,7 @@ bin/console arxy:files:migrate-naming-strategy
623623
624624
```yaml
625625
MicrosoftAzure\Storage\Blob\BlobRestProxy:
626-
factory: [ 'MicrosoftAzure\Storage\Blob\BlobRestProxy', 'createBlobService' ]
626+
factory: ['MicrosoftAzure\Storage\Blob\BlobRestProxy', 'createBlobService']
627627
arguments:
628628
$connectionString: 'DefaultEndpointsProtocol=https;AccountName=xxxxxxxx;EndpointSuffix=core.windows.net'
629629

@@ -728,7 +728,7 @@ There is also DelegatingManager, which can be used as router to different other
728728

729729
```yaml
730730
Arxy\FilesBundle\DelegatingManager:
731-
$managers: [ '@manager_1', '@manager_2' ]
731+
$managers: ['@manager_1', '@manager_2']
732732
```
733733

734734
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
975975

976976
# Known issues
977977

978-
- 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.
978+
No known issues.

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
"gabrielelana/byte-units": "^0.5.0"
1414
},
1515
"require-dev": {
16+
"doctrine/dbal": "3.2.x-dev",
1617
"symfony/validator": "*",
17-
"doctrine/orm": "*",
18+
"doctrine/orm": "3.0.x-dev",
1819
"symfony/form": "*",
1920
"symfony/http-foundation": "*",
2021
"twig/twig": "*",
@@ -29,7 +30,7 @@
2930
"symfony/dependency-injection": "^5.2",
3031
"infection/infection": "^0.21.4",
3132
"symfony/symfony": "^4.4 | ^5.2",
32-
"doctrine/doctrine-bundle": "^2.3",
33+
"doctrine/doctrine-bundle": "2.5.x-dev",
3334
"liip/imagine-bundle": "^2.6",
3435
"vimeo/psalm": "^4.7",
3536
"league/flysystem-bundle": "^2.0",

src/DependencyInjection/ArxyFilesExtension.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Arxy\FilesBundle\Storage;
1313
use Arxy\FilesBundle\Twig\FilesExtension;
1414
use Arxy\FilesBundle\Twig\FilesRuntime;
15+
use Doctrine\DBAL\Events as DbalEvents;
16+
use Doctrine\ORM\Events as OrmEvents;
1517
use LogicException;
1618
use Psr\EventDispatcher\EventDispatcherInterface;
1719
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -136,9 +138,17 @@ private function createListenerDefinition(string $driver, string $serviceId): De
136138
case 'orm':
137139
$definition = new Definition(DoctrineORMListener::class);
138140
$definition->setArgument('$manager', new Reference($serviceId));
139-
$definition->addTag('doctrine.event_listener', ['event' => 'postPersist', 'lazy' => true]);
140-
$definition->addTag('doctrine.event_listener', ['event' => 'postRemove', 'lazy' => true]);
141-
$definition->addTag('doctrine.event_listener', ['event' => 'onClear', 'lazy' => true]);
141+
$definition->addTag('doctrine.event_listener', ['event' => OrmEvents::postPersist, 'lazy' => true]);
142+
$definition->addTag('doctrine.event_listener', ['event' => OrmEvents::postRemove, 'lazy' => true]);
143+
$definition->addTag('doctrine.event_listener', ['event' => OrmEvents::onClear, 'lazy' => true]);
144+
$definition->addTag(
145+
'doctrine.event_listener',
146+
['event' => DbalEvents::onTransactionCommit, 'lazy' => true]
147+
);
148+
$definition->addTag(
149+
'doctrine.event_listener',
150+
['event' => DbalEvents::onTransactionRollBack, 'lazy' => true]
151+
);
142152

143153
return $definition;
144154
default:

src/EventListener/DoctrineORMListener.php

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,38 @@
66

77
use Arxy\FilesBundle\ManagerInterface;
88
use Arxy\FilesBundle\Model\File;
9-
use Closure;
109
use Doctrine\Common\Util\ClassUtils;
10+
use Doctrine\DBAL\Event\TransactionCommitEventArgs;
11+
use Doctrine\DBAL\Event\TransactionRollBackEventArgs;
1112
use Doctrine\ORM\EntityManagerInterface;
1213
use Doctrine\ORM\Event\LifecycleEventArgs;
14+
use Doctrine\ORM\Event\OnClearEventArgs;
1315
use ReflectionObject;
1416

1517
final class DoctrineORMListener
1618
{
1719
private ManagerInterface $manager;
20+
/** @var class-string<File> */
1821
private string $class;
19-
private Closure $move;
20-
private Closure $remove;
22+
private array $pendingMove = [];
23+
private array $pendingRemove = [];
2124

2225
public function __construct(ManagerInterface $manager)
2326
{
2427
$this->class = $manager->getClass();
2528
$this->manager = $manager;
26-
27-
$this->move = static function (File $file) use ($manager): void {
28-
$manager->moveFile($file);
29-
};
30-
$this->remove = static function (File $file) use ($manager): void {
31-
$manager->remove($file);
32-
};
3329
}
3430

3531
public function postPersist(LifecycleEventArgs $eventArgs): void
3632
{
3733
$entity = $eventArgs->getEntity();
3834
$entityManager = $eventArgs->getEntityManager();
3935
if ($this->supports($entity)) {
40-
($this->move)($entity);
36+
$this->pendingMove[] = $entity;
37+
}
38+
foreach ($this->handleEmbeddable($entityManager, $entity) as $file) {
39+
$this->pendingMove[] = $file;
4140
}
42-
$this->handleEmbeddable($entityManager, $entity, $this->move);
4341
}
4442

4543
public function postRemove(LifecycleEventArgs $eventArgs): void
@@ -48,26 +46,63 @@ public function postRemove(LifecycleEventArgs $eventArgs): void
4846
$entityManager = $eventArgs->getEntityManager();
4947

5048
if ($this->supports($entity)) {
51-
($this->remove)($entity);
49+
$this->pendingRemove[] = $entity;
50+
}
51+
foreach ($this->handleEmbeddable($entityManager, $entity) as $file) {
52+
$this->pendingRemove[] = $file;
53+
}
54+
}
55+
56+
public function onTransactionCommit(TransactionCommitEventArgs $eventArgs): void
57+
{
58+
if ($eventArgs->getConnection()->isTransactionActive()) {
59+
return;
60+
}
61+
62+
$pendingMove = $this->pendingMove;
63+
foreach ($pendingMove as $file) {
64+
$this->manager->moveFile($file);
65+
}
66+
67+
$pendingRemove = $this->pendingRemove;
68+
foreach ($pendingRemove as $file) {
69+
$this->manager->remove($file);
5270
}
53-
$this->handleEmbeddable($entityManager, $entity, $this->remove);
71+
72+
$this->clearPending();
73+
}
74+
75+
public function onTransactionRollBack(TransactionRollBackEventArgs $eventArgs): void
76+
{
77+
if ($eventArgs->getConnection()->isTransactionActive()) {
78+
return;
79+
}
80+
81+
$this->clearPending();
5482
}
5583

56-
public function onClear(): void
84+
private function clearPending(): void
5785
{
86+
$this->pendingMove = [];
87+
$this->pendingRemove = [];
88+
}
89+
90+
public function onClear(OnClearEventArgs $eventArgs): void
91+
{
92+
if ($eventArgs->getEntityManager()->getConnection()->isTransactionActive()) {
93+
return;
94+
}
5895
$this->manager->clear();
96+
$this->clearPending();
5997
}
6098

6199
private function supports(object $entity): bool
62100
{
63101
return $entity instanceof $this->class;
64102
}
65103

66-
private function handleEmbeddable(
67-
EntityManagerInterface $entityManager,
68-
object $entity,
69-
Closure $action
70-
): void {
104+
private function handleEmbeddable(EntityManagerInterface $entityManager, object $entity): iterable
105+
{
71106
$classMetadata = $entityManager->getClassMetadata(ClassUtils::getClass($entity));
72107

73108
foreach ($classMetadata->embeddedClasses as $property => $embeddedClass) {
@@ -84,7 +119,7 @@ private function handleEmbeddable(
84119
if ($file === null) {
85120
continue;
86121
}
87-
$action($file);
122+
yield $file;
88123
}
89124
}
90125
}

src/Manager.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,7 @@ public function moveFile(File $file): void
170170

171171
/** @psalm-suppress RedundantCondition */
172172
if (is_resource($stream)) {
173-
try {
174-
ErrorHandler::wrap(static fn (): bool => fclose($stream));
175-
} catch (ErrorException $e) {
176-
// nothing we can do
177-
}
173+
fclose($stream);
178174
}
179175

180176
if ($this->eventDispatcher !== null) {

tests/Functional/ManagerTest.php

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use SplFileObject;
1111
use SplTempFileObject;
1212

13+
use function md5;
14+
1315
class ManagerTest extends AbstractFunctionalTest
1416
{
1517
protected ManagerInterface $embeddableManager;
@@ -44,6 +46,10 @@ public function testSimpleUpload(): File
4446
$file = $this->manager->upload(new SplFileObject(__DIR__ . '/../files/image1.jpg'));
4547

4648
$this->entityManager->persist($file);
49+
50+
self::assertFalse(
51+
$this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1')
52+
);
4753
$this->entityManager->flush();
4854

4955
self::assertTrue(
@@ -191,16 +197,64 @@ public function testSimpleDelete(): void
191197
self::assertFalse($this->flysystem->fileExists($pathname));
192198
}
193199

200+
public function testFileUploadedWithClear(): void
201+
{
202+
$file = $this->manager->upload(new SplFileObject(__DIR__ . '/../files/image1.jpg'));
203+
204+
$this->entityManager->beginTransaction();
205+
206+
$this->entityManager->persist($file);
207+
208+
self::assertFalse(
209+
$this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1')
210+
);
211+
$this->entityManager->flush();
212+
213+
self::assertFalse(
214+
$this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1')
215+
);
216+
217+
$this->entityManager->clear();
218+
219+
$this->entityManager->commit();
220+
221+
self::assertTrue(
222+
$this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1')
223+
);
224+
}
225+
226+
public function testFileNotUploadedWithRollBack(): void
227+
{
228+
$file = $this->manager->upload(new SplFileObject(__DIR__ . '/../files/image1.jpg'));
229+
230+
$this->entityManager->beginTransaction();
231+
232+
$this->entityManager->persist($file);
233+
234+
self::assertFalse(
235+
$this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1')
236+
);
237+
$this->entityManager->flush();
238+
239+
self::assertFalse(
240+
$this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1')
241+
);
242+
243+
$this->entityManager->rollback();
244+
245+
self::assertFalse(
246+
$this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1')
247+
);
248+
}
249+
194250
/**
195251
* @depends testSimpleUpload
196252
*/
197253
public function testFileNotDeletedWithRollback(): void
198254
{
199-
self::markTestSkipped('Not implemented yet. Waiting DBAL 3.2.X Release');
200-
201255
$file = $this->testSimpleUpload();
202256

203-
$filepath = '9aa1c5fc/7c938816/6d7ce7fd/46648dd1/9aa1c5fc7c9388166d7ce7fd46648dd1';
257+
$filepath = '9aa1c5fc7c9388166d7ce7fd46648dd1';
204258
self::assertTrue($this->flysystem->fileExists($filepath));
205259

206260
$this->entityManager->beginTransaction();
@@ -223,11 +277,9 @@ public function testFileNotDeletedWithRollback(): void
223277
*/
224278
public function testFileDeletedWithCommit(): void
225279
{
226-
self::markTestSkipped('Not implemented yet. Waiting DBAL 3.2.X Release');
227-
228280
$file = $this->testSimpleUpload();
229281

230-
$filepath = '9aa1c5fc/7c938816/6d7ce7fd/46648dd1/9aa1c5fc7c9388166d7ce7fd46648dd1';
282+
$filepath = '9aa1c5fc7c9388166d7ce7fd46648dd1';
231283

232284
self::assertTrue($this->flysystem->fileExists($filepath));
233285

0 commit comments

Comments
 (0)