Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 775ed64

Browse filesBrowse files
committed
add Doctrine transaction decorator
1 parent 7846475 commit 775ed64
Copy full SHA for 775ed64

File tree

5 files changed

+199
-0
lines changed
Filter options

5 files changed

+199
-0
lines changed
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Attribute;
13+
14+
use Symfony\Bridge\Doctrine\Decorator\DoctrineTransactionDecorator;
15+
use Symfony\Component\Decorator\Attribute\Decorate;
16+
17+
/**
18+
* Indicates that an object method should be wrapped within a single Doctrine transaction.
19+
*
20+
* @author Yonel Ceruto <open@yceruto.dev>
21+
*/
22+
#[\Attribute(\Attribute::TARGET_METHOD)]
23+
class Transactional extends Decorate
24+
{
25+
/**
26+
* @param string|null $name The entity manager name (null for the default one)
27+
*/
28+
public function __construct(?string $name = null)
29+
{
30+
parent::__construct(DoctrineTransactionDecorator::class, ['name' => $name]);
31+
}
32+
}

‎src/Symfony/Bridge/Doctrine/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Accept `ReadableCollection` in `CollectionToArrayTransformer`
8+
* Add `DoctrineTransactionDecorator` and `Transactional` attribute
89

910
7.1
1011
---
+56Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Decorator;
13+
14+
use Doctrine\ORM\EntityManagerInterface;
15+
use Doctrine\Persistence\ManagerRegistry;
16+
use Symfony\Component\Decorator\DecoratorInterface;
17+
18+
/**
19+
* Wraps a persistence functionality in a single Doctrine transaction.
20+
*
21+
* @author Yonel Ceruto <open@yceruto.dev>
22+
*/
23+
final readonly class DoctrineTransactionDecorator implements DecoratorInterface
24+
{
25+
public function __construct(
26+
private ManagerRegistry $managerRegistry,
27+
) {
28+
}
29+
30+
public function decorate(\Closure $func, ?string $name = null): \Closure
31+
{
32+
$entityManager = $this->managerRegistry->getManager($name);
33+
34+
if (!$entityManager instanceof EntityManagerInterface) {
35+
throw new \RuntimeException(sprintf('The manager "%s" is not an entity manager.', $name));
36+
}
37+
38+
return function (mixed ...$args) use ($func, $entityManager) {
39+
$entityManager->getConnection()->beginTransaction();
40+
41+
try {
42+
$return = $func(...$args);
43+
44+
$entityManager->flush();
45+
$entityManager->getConnection()->commit();
46+
47+
return $return;
48+
} catch (\Throwable $e) {
49+
$entityManager->close();
50+
$entityManager->getConnection()->rollBack();
51+
52+
throw $e;
53+
}
54+
};
55+
}
56+
}
+109Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Tests\Decorator;
13+
14+
use Doctrine\DBAL\Connection;
15+
use Doctrine\ORM\EntityManagerInterface;
16+
use Doctrine\Persistence\ManagerRegistry;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Bridge\Doctrine\Attribute\Transactional;
19+
use Symfony\Bridge\Doctrine\Decorator\DoctrineTransactionDecorator;
20+
use Symfony\Component\Decorator\DecoratorChain;
21+
22+
class DoctrineTransactionDecoratorTest extends TestCase
23+
{
24+
private ManagerRegistry $managerRegistry;
25+
private Connection $connection;
26+
private EntityManagerInterface $entityManager;
27+
private DecoratorChain $decorator;
28+
29+
protected function setUp(): void
30+
{
31+
$this->connection = $this->createMock(Connection::class);
32+
33+
$this->entityManager = $this->createMock(EntityManagerInterface::class);
34+
$this->entityManager->method('getConnection')->willReturn($this->connection);
35+
36+
$this->managerRegistry = $this->createMock(ManagerRegistry::class);
37+
$this->managerRegistry->method('getManager')->willReturn($this->entityManager);
38+
39+
$this->decorator = DecoratorChain::from([
40+
DoctrineTransactionDecorator::class => fn () => new DoctrineTransactionDecorator($this->managerRegistry),
41+
]);
42+
}
43+
44+
public function testDecoratorWrapsInTransactionAndFlushes()
45+
{
46+
$this->connection->expects($this->once())
47+
->method('beginTransaction')
48+
;
49+
$this->connection->expects($this->once())
50+
->method('commit')
51+
;
52+
$this->entityManager->expects($this->once())
53+
->method('flush')
54+
;
55+
56+
$handler = new TestHandler();
57+
$result = $this->decorator->call($handler->handle(...));
58+
$this->assertSame('success', $result);
59+
}
60+
61+
public function testTransactionIsRolledBackOnException()
62+
{
63+
$this->connection->expects($this->once())
64+
->method('beginTransaction')
65+
;
66+
$this->connection->expects($this->once())
67+
->method('rollBack')
68+
;
69+
70+
$this->expectException(\RuntimeException::class);
71+
$this->expectExceptionMessage('A runtime error.');
72+
73+
$handler = new TestHandler();
74+
$this->decorator->call($handler->handleWithError(...));
75+
}
76+
77+
public function testInvalidEntityManagerThrowsException()
78+
{
79+
$this->managerRegistry
80+
->method('getManager')
81+
->with('unknown_manager')
82+
->will($this->throwException(new \InvalidArgumentException()));
83+
84+
$this->expectException(\InvalidArgumentException::class);
85+
86+
$handler = new TestHandler();
87+
$this->decorator->call($handler->handleWithUnknownManager(...));
88+
}
89+
}
90+
91+
class TestHandler
92+
{
93+
#[Transactional]
94+
public function handle(): string
95+
{
96+
return 'success';
97+
}
98+
99+
#[Transactional]
100+
public function handleWithError(): void
101+
{
102+
throw new \RuntimeException('A runtime error.');
103+
}
104+
105+
#[Transactional('unknown_manager')]
106+
public function handleWithUnknownManager(): void
107+
{
108+
}
109+
}

‎src/Symfony/Bridge/Doctrine/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"require-dev": {
2828
"symfony/cache": "^6.4|^7.0",
2929
"symfony/config": "^6.4|^7.0",
30+
"symfony/decorator": "^7.2",
3031
"symfony/dependency-injection": "^6.4|^7.0",
3132
"symfony/doctrine-messenger": "^6.4|^7.0",
3233
"symfony/expression-language": "^6.4|^7.0",

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.