Skip to content

Commit d3b1d55

Browse files
committed
Handle RdbmsSaga with multiple SagaIds
1 parent 4bbec53 commit d3b1d55

File tree

6 files changed

+198
-43
lines changed

6 files changed

+198
-43
lines changed

src/Saga/DoctrineDbalRdbmsSagaFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
public function createFromRow(array $row): RdbmsSaga
2222
{
2323
return new RdbmsSaga(
24+
$row['id'],
2425
$row['sagaName'],
25-
$row['sagaId'],
26+
$row['sagaIds'],
2627
$row['payload'],
2728
new DateTimeImmutable($row['createdAt']),
2829
$row['updatedAt'] !== null ? new DateTimeImmutable($row['updatedAt']) : null,

src/Saga/DoctrineRdbmsSagaStoreRepository.php

Lines changed: 146 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44

55
namespace Gember\RdbmsEventStoreDoctrineDbal\Saga;
66

7+
use Doctrine\DBAL\ArrayParameterType;
78
use Doctrine\DBAL\Connection;
89
use Gember\DependencyContracts\EventStore\Saga\RdbmsSaga;
910
use Gember\DependencyContracts\EventStore\Saga\RdbmsSagaStoreRepository;
1011
use Gember\DependencyContracts\EventStore\Saga\RdbmsSagaNotFoundException;
12+
use Gember\DependencyContracts\Util\Generator\Identity\IdentityGenerator;
13+
use Gember\RdbmsEventStoreDoctrineDbal\Saga\TableSchema\SagaStoreRelationTableSchema;
1114
use Gember\RdbmsEventStoreDoctrineDbal\Saga\TableSchema\SagaStoreTableSchema;
1215
use Override;
1316
use Stringable;
1417
use DateTimeImmutable;
18+
use Throwable;
1519

1620
/**
1721
* @phpstan-type SagaRow array{
18-
* sagaId: string,
22+
* id: string,
23+
* sagaIds: list<string>,
1924
* sagaName: string,
2025
* payload: string,
2126
* createdAt: string,
@@ -27,92 +32,199 @@
2732
public function __construct(
2833
private Connection $connection,
2934
private SagaStoreTableSchema $sagaStoreTableSchema,
35+
private SagaStoreRelationTableSchema $sagaStoreRelationTableSchema,
3036
private DoctrineDbalRdbmsSagaFactory $sagaFactory,
37+
private IdentityGenerator $identityGenerator,
3138
) {}
3239

3340
#[Override]
34-
public function get(string $sagaName, Stringable|string $sagaId): RdbmsSaga
41+
public function get(string $sagaName, Stringable|string ...$sagaIds): RdbmsSaga
3542
{
3643
$sagaStoreSchema = $this->sagaStoreTableSchema;
44+
$sagaStoreRelationSchema = $this->sagaStoreRelationTableSchema;
3745

38-
/** @var false|SagaRow $row */
46+
/** @var list<array{
47+
* id: string,
48+
* sagaId: string,
49+
* sagaName: string,
50+
* payload: string,
51+
* createdAt: string,
52+
* updatedAt: string|null
53+
* }> $row */
3954
$row = $this->connection->createQueryBuilder()
4055
->select(
4156
<<<DQL
42-
ss.{$sagaStoreSchema->sagaIdFieldName} as sagaId,
57+
ss.{$sagaStoreSchema->idFieldName} as id,
4358
ss.{$sagaStoreSchema->sagaNameFieldName} as sagaName,
4459
ss.{$sagaStoreSchema->payloadFieldName} as payload,
4560
ss.{$sagaStoreSchema->createdAtFieldName} as createdAt,
4661
ss.{$sagaStoreSchema->updatedAtFieldName} as updatedAt
4762
DQL
4863
)
4964
->from($sagaStoreSchema->tableName, 'ss')
50-
->where(sprintf('ss.%s = :sagaId', $sagaStoreSchema->sagaIdFieldName))
65+
->join('ss', $sagaStoreRelationSchema->tableName, 'ssr', sprintf(
66+
'ss.%s = ssr.%s',
67+
$sagaStoreSchema->idFieldName,
68+
$sagaStoreRelationSchema->idFieldName,
69+
))
70+
->where(sprintf('ssr.%s IN (:sagaIds)', $sagaStoreRelationSchema->sagaIdFieldName))
5171
->andWhere(sprintf('ss.%s = :sagaName', $sagaStoreSchema->sagaNameFieldName))
52-
->setParameter('sagaId', (string) $sagaId)
72+
->setParameter(
73+
'sagaIds',
74+
array_map(fn($sagaId) => (string) $sagaId, $sagaIds),
75+
ArrayParameterType::STRING,
76+
)
5377
->setParameter('sagaName', $sagaName)
78+
->setMaxResults(1)
5479
->executeQuery()
5580
->fetchAssociative();
5681

5782
if (!$row) {
58-
throw RdbmsSagaNotFoundException::withSagaId($sagaName, $sagaId);
83+
throw RdbmsSagaNotFoundException::create($sagaName, ...$sagaIds);
5984
}
6085

61-
return $this->sagaFactory->createFromRow($row);
86+
$sagaIdRows = $this->connection->createQueryBuilder()
87+
->select(
88+
<<<DQL
89+
ssr.{$sagaStoreRelationSchema->sagaIdFieldName} as sagaId
90+
DQL
91+
)
92+
->from($sagaStoreRelationSchema->tableName, 'ssr')
93+
->executeQuery()
94+
->fetchAllAssociative();
95+
96+
/** @var list<SagaRow> $payload */
97+
$payload = $row;
98+
$payload['sagaIds'] = array_map(fn($sagaIdRow) => $sagaIdRow['sagaId'], $sagaIdRows);
99+
100+
return $this->sagaFactory->createFromRow($payload);
62101
}
63102

64103
#[Override]
65104
public function save(
66105
string $sagaName,
67-
Stringable|string $sagaId,
68106
string $payload,
69107
DateTimeImmutable $now,
108+
Stringable|string ...$sagaIds,
109+
): RdbmsSaga {
110+
try {
111+
$previous = $this->get($sagaName, ...$sagaIds);
112+
} catch (RdbmsSagaNotFoundException) {
113+
return $this->create($sagaName, $payload, $now, ...$sagaIds);
114+
}
115+
116+
return $this->update($previous, $sagaName, $payload, $now, ...$sagaIds);
117+
}
118+
119+
private function create(
120+
string $sagaName,
121+
string $payload,
122+
DateTimeImmutable $now,
123+
Stringable|string ...$sagaIds,
70124
): RdbmsSaga {
125+
$id = $this->identityGenerator->generate();
126+
71127
$sagaStoreSchema = $this->sagaStoreTableSchema;
128+
$sagaStoreRelationSchema = $this->sagaStoreRelationTableSchema;
129+
130+
$this->connection->beginTransaction();
72131

73132
try {
74-
$previous = $this->get($sagaName, $sagaId);
75-
} catch (RdbmsSagaNotFoundException) {
76133
$this->connection->createQueryBuilder()
77134
->insert($sagaStoreSchema->tableName)
78-
->setValue($sagaStoreSchema->sagaIdFieldName, ':sagaId')
135+
->setValue($sagaStoreSchema->idFieldName, ':id')
79136
->setValue($sagaStoreSchema->sagaNameFieldName, ':sagaName')
80137
->setValue($sagaStoreSchema->payloadFieldName, ':payload')
81138
->setValue($sagaStoreSchema->createdAtFieldName, ':createdAt')
82139
->setParameters([
83-
'sagaId' => $sagaId,
140+
'id' => $id,
84141
'sagaName' => $sagaName,
85142
'payload' => $payload,
86143
'createdAt' => $now->format($sagaStoreSchema->createdAtFieldFormat),
87144
])
88145
->executeStatement();
89146

90-
return new RdbmsSaga(
91-
$sagaName,
92-
$sagaId,
93-
$payload,
94-
$now,
95-
null,
96-
);
147+
foreach ($sagaIds as $sagaId) {
148+
$this->connection->createQueryBuilder()
149+
->insert($sagaStoreRelationSchema->tableName)
150+
->setValue($sagaStoreRelationSchema->idFieldName, ':id')
151+
->setValue($sagaStoreRelationSchema->sagaIdFieldName, ':sagaId')
152+
->setParameters([
153+
'id' => $id,
154+
'sagaId' => $sagaId,
155+
])
156+
->executeStatement();
157+
}
158+
159+
$this->connection->commit();
160+
} catch (Throwable $exception) {
161+
$this->connection->rollBack();
162+
163+
throw $exception;
97164
}
98165

99-
$this->connection->createQueryBuilder()
100-
->update($sagaStoreSchema->tableName)
101-
->where(sprintf('%s = :sagaId', $sagaStoreSchema->sagaIdFieldName))
102-
->andWhere(sprintf('%s = :sagaName', $sagaStoreSchema->sagaNameFieldName))
103-
->set($sagaStoreSchema->payloadFieldName, ':payload')
104-
->set($sagaStoreSchema->updatedAtFieldName, ':updatedAt')
105-
->setParameters([
106-
'sagaId' => $sagaId,
107-
'sagaName' => $sagaName,
108-
'payload' => $payload,
109-
'updatedAt' => $now->format($sagaStoreSchema->updatedAtFieldFormat),
110-
])
111-
->executeStatement();
166+
return new RdbmsSaga(
167+
$id,
168+
$sagaName,
169+
array_values($sagaIds),
170+
$payload,
171+
$now,
172+
null,
173+
);
174+
}
175+
176+
private function update(
177+
RdbmsSaga $previous,
178+
string $sagaName,
179+
string $payload,
180+
DateTimeImmutable $now,
181+
Stringable|string ...$sagaIds,
182+
): RdbmsSaga {
183+
$sagaStoreSchema = $this->sagaStoreTableSchema;
184+
$sagaStoreRelationSchema = $this->sagaStoreRelationTableSchema;
185+
186+
$this->connection->beginTransaction();
187+
188+
try {
189+
$this->connection->createQueryBuilder()
190+
->update($sagaStoreSchema->tableName)
191+
->where(sprintf('%s = :id', $sagaStoreSchema->idFieldName))
192+
->set($sagaStoreSchema->payloadFieldName, ':payload')
193+
->set($sagaStoreSchema->updatedAtFieldName, ':updatedAt')
194+
->setParameter('id', $previous->id)
195+
->setParameter('payload', $payload)
196+
->setParameter('updatedAt', $now->format($sagaStoreSchema->updatedAtFieldFormat))
197+
->executeStatement();
198+
199+
$this->connection->createQueryBuilder()
200+
->delete($sagaStoreRelationSchema->tableName)
201+
->where(sprintf('%s = :id', $sagaStoreRelationSchema->idFieldName))
202+
->setParameter('id', $previous->id)
203+
->executeStatement();
204+
205+
foreach ($sagaIds as $sagaId) {
206+
$this->connection->createQueryBuilder()
207+
->insert($sagaStoreRelationSchema->tableName)
208+
->setValue($sagaStoreRelationSchema->idFieldName, ':id')
209+
->setValue($sagaStoreRelationSchema->sagaIdFieldName, ':sagaId')
210+
->setParameters([
211+
'id' => $previous->id,
212+
'sagaId' => $sagaId,
213+
])
214+
->executeStatement();
215+
}
216+
217+
$this->connection->commit();
218+
} catch (Throwable $exception) {
219+
$this->connection->rollBack();
220+
221+
throw $exception;
222+
}
112223

113224
return new RdbmsSaga(
225+
$previous->id,
114226
$sagaName,
115-
$sagaId,
227+
array_values($sagaIds),
116228
$payload,
117229
$previous->createdAt,
118230
$now,

tests/Saga/DoctrineDbalRdbmsSagaFactoryTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ final class DoctrineDbalRdbmsSagaFactoryTest extends TestCase
1818
public function itShouldCreateRdbmsSaga(): void
1919
{
2020
$saga = (new DoctrineDbalRdbmsSagaFactory())->createFromRow([
21+
'id' => '01K7Q14MMW2FQP2JS1RHQS7QXP',
2122
'sagaName' => 'some.saga',
22-
'sagaId' => '01K76G1PGKPZ047KDN25PFPEEV',
23+
'sagaIds' => ['01K76G1PGKPZ047KDN25PFPEEV', '01K7Q13CG3A3PQCC98XYSE67K1'],
2324
'payload' => '{"some":"data"}',
2425
'createdAt' => '2018-12-01 12:05:08.234543',
2526
'updatedAt' => null,
2627
]);
2728

29+
self::assertSame('01K7Q14MMW2FQP2JS1RHQS7QXP', $saga->id);
2830
self::assertSame('some.saga', $saga->sagaName);
29-
self::assertSame('01K76G1PGKPZ047KDN25PFPEEV', $saga->sagaId);
31+
self::assertSame(['01K76G1PGKPZ047KDN25PFPEEV', '01K7Q13CG3A3PQCC98XYSE67K1'], $saga->sagaIds);
3032
self::assertSame('{"some":"data"}', $saga->payload);
3133
self::assertEquals(new DateTimeImmutable('2018-12-01 12:05:08.234543'), $saga->createdAt);
3234
self::assertNull($saga->updatedAt);

tests/Saga/DoctrineRdbmsSagaStoreRepositoryTest.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Gember\RdbmsEventStoreDoctrineDbal\Saga\DoctrineDbalRdbmsSagaFactory;
1111
use Gember\RdbmsEventStoreDoctrineDbal\Saga\DoctrineRdbmsSagaStoreRepository;
1212
use Gember\RdbmsEventStoreDoctrineDbal\Saga\TableSchema\SagaTableSchemaFactory;
13+
use Gember\RdbmsEventStoreDoctrineDbal\Test\TestDoubles\TestIdentityGenerator;
1314
use PHPUnit\Framework\Attributes\Test;
1415
use PHPUnit\Framework\TestCase;
1516
use Override;
@@ -21,6 +22,7 @@
2122
final class DoctrineRdbmsSagaStoreRepositoryTest extends TestCase
2223
{
2324
private DoctrineRdbmsSagaStoreRepository $repository;
25+
private TestIdentityGenerator $identityGenerator;
2426

2527
#[Override]
2628
protected function setUp(): void
@@ -33,7 +35,9 @@ protected function setUp(): void
3335
$this->repository = new DoctrineRdbmsSagaStoreRepository(
3436
$connection,
3537
SagaTableSchemaFactory::createDefaultSagaStore(),
38+
SagaTableSchemaFactory::createDefaultSagaStoreRelation(),
3639
new DoctrineDbalRdbmsSagaFactory(),
40+
$this->identityGenerator = new TestIdentityGenerator(),
3741
);
3842
}
3943

@@ -48,17 +52,21 @@ public function itShouldThrowExceptionWhenSagaNotFound(): void
4852
#[Test]
4953
public function itShouldSaveAndGetSaga(): void
5054
{
55+
$this->identityGenerator->ids[] = '01K7Q083CX4T7Z0NT5CKEX8NEJ';
56+
5157
$this->repository->save(
5258
'some.saga',
53-
'01K76GDQ5RT71G7HQVNR264KD4',
5459
'{"some":"data"}',
5560
new DateTimeImmutable('2025-10-10 12:00:34'),
61+
'01K76GDQ5RT71G7HQVNR264KD4',
62+
'01K7Q033P5174AXA054FFAHW2F',
5663
);
5764

5865
$saga = $this->repository->get('some.saga', '01K76GDQ5RT71G7HQVNR264KD4');
5966

67+
self::assertSame('01K7Q083CX4T7Z0NT5CKEX8NEJ', $saga->id);
6068
self::assertSame('some.saga', $saga->sagaName);
61-
self::assertSame('01K76GDQ5RT71G7HQVNR264KD4', $saga->sagaId);
69+
self::assertSame(['01K76GDQ5RT71G7HQVNR264KD4', '01K7Q033P5174AXA054FFAHW2F'], $saga->sagaIds);
6270
self::assertSame('{"some":"data"}', $saga->payload);
6371
self::assertEquals(new DateTimeImmutable('2025-10-10 12:00:34'), $saga->createdAt);
6472
self::assertNull($saga->updatedAt);
@@ -67,24 +75,29 @@ public function itShouldSaveAndGetSaga(): void
6775
#[Test]
6876
public function itShouldSaveExistingSaga(): void
6977
{
78+
$this->identityGenerator->ids[] = '01K7Q083CX4T7Z0NT5CKEX8NEJ';
79+
7080
$this->repository->save(
7181
'some.saga',
72-
'01K76GDQ5RT71G7HQVNR264KD4',
7382
'{"some":"data"}',
7483
new DateTimeImmutable('2025-10-10 12:00:34'),
84+
'01K76GDQ5RT71G7HQVNR264KD4',
85+
'01K7Q0GR8ABHBZG8QCGTXJXJ7T',
7586
);
7687

7788
$this->repository->save(
7889
'some.saga',
79-
'01K76GDQ5RT71G7HQVNR264KD4',
8090
'{"some":"updated"}',
8191
new DateTimeImmutable('2025-10-10 13:30:12'),
92+
'01K76GDQ5RT71G7HQVNR264KD4',
93+
'01K7Q0JGY9ZMX11K75AAY5J78R',
8294
);
8395

8496
$saga = $this->repository->get('some.saga', '01K76GDQ5RT71G7HQVNR264KD4');
8597

98+
self::assertSame('01K7Q083CX4T7Z0NT5CKEX8NEJ', $saga->id);
8699
self::assertSame('some.saga', $saga->sagaName);
87-
self::assertSame('01K76GDQ5RT71G7HQVNR264KD4', $saga->sagaId);
100+
self::assertSame(['01K76GDQ5RT71G7HQVNR264KD4', '01K7Q0JGY9ZMX11K75AAY5J78R'], $saga->sagaIds);
88101
self::assertSame('{"some":"updated"}', $saga->payload);
89102
self::assertEquals(new DateTimeImmutable('2025-10-10 12:00:34'), $saga->createdAt);
90103
self::assertEquals(new DateTimeImmutable('2025-10-10 13:30:12'), $saga->updatedAt);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gember\RdbmsEventStoreDoctrineDbal\Test\TestDoubles;
6+
7+
use Gember\DependencyContracts\Util\Generator\Identity\IdentityGenerator;
8+
use Override;
9+
10+
final class TestIdentityGenerator implements IdentityGenerator
11+
{
12+
/**
13+
* @var list<string>
14+
*/
15+
public array $ids = [];
16+
17+
#[Override]
18+
public function generate(): string
19+
{
20+
return (string) array_shift($this->ids);
21+
}
22+
}

0 commit comments

Comments
 (0)