Skip to content

Commit ffc8db1

Browse files
committed
Add transactions support
1 parent 49766a1 commit ffc8db1

File tree

7 files changed

+112
-49
lines changed

7 files changed

+112
-49
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: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,37 @@
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;
1314
use ReflectionObject;
1415

1516
final class DoctrineORMListener
1617
{
1718
private ManagerInterface $manager;
19+
/** @var class-string<File> */
1820
private string $class;
19-
private Closure $move;
20-
private Closure $remove;
21+
private array $pendingMove = [];
22+
private array $pendingRemove = [];
2123

2224
public function __construct(ManagerInterface $manager)
2325
{
2426
$this->class = $manager->getClass();
2527
$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-
};
3328
}
3429

3530
public function postPersist(LifecycleEventArgs $eventArgs): void
3631
{
3732
$entity = $eventArgs->getEntity();
3833
$entityManager = $eventArgs->getEntityManager();
3934
if ($this->supports($entity)) {
40-
($this->move)($entity);
35+
$this->pendingMove[] = $entity;
36+
}
37+
foreach ($this->handleEmbeddable($entityManager, $entity) as $file) {
38+
$this->pendingMove[] = $file;
4139
}
42-
$this->handleEmbeddable($entityManager, $entity, $this->move);
4340
}
4441

4542
public function postRemove(LifecycleEventArgs $eventArgs): void
@@ -48,26 +45,57 @@ public function postRemove(LifecycleEventArgs $eventArgs): void
4845
$entityManager = $eventArgs->getEntityManager();
4946

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

5685
public function onClear(): void
5786
{
5887
$this->manager->clear();
88+
$this->pendingMove = [];
89+
$this->pendingRemove = [];
5990
}
6091

6192
private function supports(object $entity): bool
6293
{
6394
return $entity instanceof $this->class;
6495
}
6596

66-
private function handleEmbeddable(
67-
EntityManagerInterface $entityManager,
68-
object $entity,
69-
Closure $action
70-
): void {
97+
private function handleEmbeddable(EntityManagerInterface $entityManager, object $entity): iterable
98+
{
7199
$classMetadata = $entityManager->getClassMetadata(ClassUtils::getClass($entity));
72100

73101
foreach ($classMetadata->embeddedClasses as $property => $embeddedClass) {
@@ -84,7 +112,7 @@ private function handleEmbeddable(
84112
if ($file === null) {
85113
continue;
86114
}
87-
$action($file);
115+
yield $file;
88116
}
89117
}
90118
}

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: 32 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,38 @@ public function testSimpleDelete(): void
191197
self::assertFalse($this->flysystem->fileExists($pathname));
192198
}
193199

200+
public function testFileNotUploadedWithRollBack(): 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->commit();
218+
219+
self::assertTrue(
220+
$this->flysystem->fileExists('9aa1c5fc7c9388166d7ce7fd46648dd1')
221+
);
222+
}
223+
194224
/**
195225
* @depends testSimpleUpload
196226
*/
197227
public function testFileNotDeletedWithRollback(): void
198228
{
199-
self::markTestSkipped('Not implemented yet. Waiting DBAL 3.2.X Release');
200-
201229
$file = $this->testSimpleUpload();
202230

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

206234
$this->entityManager->beginTransaction();
@@ -223,11 +251,9 @@ public function testFileNotDeletedWithRollback(): void
223251
*/
224252
public function testFileDeletedWithCommit(): void
225253
{
226-
self::markTestSkipped('Not implemented yet. Waiting DBAL 3.2.X Release');
227-
228254
$file = $this->testSimpleUpload();
229255

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

232258
self::assertTrue($this->flysystem->fileExists($filepath));
233259

0 commit comments

Comments
 (0)