Skip to content

Commit 744da49

Browse files
committed
Handle Sagas
Added the implementation for `RdbmsSagaStoreRepository`
1 parent 318805d commit 744da49

8 files changed

+389
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gember\RdbmsEventStoreDoctrineDbal\Saga;
6+
7+
use Gember\DependencyContracts\EventStore\Saga\RdbmsSaga;
8+
use DateTimeImmutable;
9+
use DateMalformedStringException;
10+
11+
/**
12+
* @phpstan-import-type SagaRow from DoctrineRdbmsSagaStoreRepository
13+
*/
14+
final readonly class DoctrineDbalRdbmsSagaFactory
15+
{
16+
/**
17+
* @param SagaRow $row
18+
*
19+
* @throws DateMalformedStringException
20+
*/
21+
public function createFromRow(array $row): RdbmsSaga
22+
{
23+
return new RdbmsSaga(
24+
$row['sagaName'],
25+
$row['sagaId'],
26+
$row['payload'],
27+
new DateTimeImmutable($row['createdAt']),
28+
$row['updatedAt'] !== null ? new DateTimeImmutable($row['updatedAt']) : null,
29+
);
30+
}
31+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gember\RdbmsEventStoreDoctrineDbal\Saga;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Gember\DependencyContracts\EventStore\Saga\RdbmsSaga;
9+
use Gember\DependencyContracts\EventStore\Saga\RdbmsSagaStoreRepository;
10+
use Gember\DependencyContracts\EventStore\Saga\RdbmsSagaNotFoundException;
11+
use Gember\RdbmsEventStoreDoctrineDbal\Saga\TableSchema\SagaStoreTableSchema;
12+
use Override;
13+
use Stringable;
14+
use DateTimeImmutable;
15+
16+
/**
17+
* @phpstan-type SagaRow array{
18+
* sagaId: string,
19+
* sagaName: string,
20+
* payload: string,
21+
* createdAt: string,
22+
* updatedAt: null|string
23+
* }
24+
*/
25+
final readonly class DoctrineRdbmsSagaStoreRepository implements RdbmsSagaStoreRepository
26+
{
27+
public function __construct(
28+
private Connection $connection,
29+
private SagaStoreTableSchema $sagaStoreTableSchema,
30+
private DoctrineDbalRdbmsSagaFactory $sagaFactory,
31+
) {}
32+
33+
#[Override]
34+
public function get(string $sagaName, Stringable|string $sagaId): RdbmsSaga
35+
{
36+
$sagaStoreSchema = $this->sagaStoreTableSchema;
37+
38+
/** @var false|SagaRow $row */
39+
$row = $this->connection->createQueryBuilder()
40+
->select(
41+
<<<DQL
42+
ss.{$sagaStoreSchema->sagaIdFieldName} as sagaId,
43+
ss.{$sagaStoreSchema->sagaNameFieldName} as sagaName,
44+
ss.{$sagaStoreSchema->payloadFieldName} as payload,
45+
ss.{$sagaStoreSchema->createdAtFieldName} as createdAt,
46+
ss.{$sagaStoreSchema->updatedAtFieldName} as updatedAt
47+
DQL
48+
)
49+
->from($sagaStoreSchema->tableName, 'ss')
50+
->where(sprintf('ss.%s = :sagaId', $sagaStoreSchema->sagaIdFieldName))
51+
->andWhere(sprintf('ss.%s = :sagaName', $sagaStoreSchema->sagaNameFieldName))
52+
->setParameter('sagaId', (string) $sagaId)
53+
->setParameter('sagaName', $sagaName)
54+
->executeQuery()
55+
->fetchAssociative();
56+
57+
if (!$row) {
58+
throw RdbmsSagaNotFoundException::withSagaId($sagaName, $sagaId);
59+
}
60+
61+
return $this->sagaFactory->createFromRow($row);
62+
}
63+
64+
#[Override]
65+
public function save(
66+
string $sagaName,
67+
Stringable|string $sagaId,
68+
string $payload,
69+
DateTimeImmutable $now,
70+
): RdbmsSaga {
71+
$sagaStoreSchema = $this->sagaStoreTableSchema;
72+
73+
try {
74+
$previous = $this->get($sagaName, $sagaId);
75+
} catch (RdbmsSagaNotFoundException) {
76+
$this->connection->createQueryBuilder()
77+
->insert($sagaStoreSchema->tableName)
78+
->setValue($sagaStoreSchema->sagaIdFieldName, ':sagaId')
79+
->setValue($sagaStoreSchema->sagaNameFieldName, ':sagaName')
80+
->setValue($sagaStoreSchema->payloadFieldName, ':payload')
81+
->setValue($sagaStoreSchema->createdAtFieldName, ':createdAt')
82+
->setParameters([
83+
'sagaId' => $sagaId,
84+
'sagaName' => $sagaName,
85+
'payload' => $payload,
86+
'createdAt' => $now->format($sagaStoreSchema->createdAtFieldFormat),
87+
])
88+
->executeStatement();
89+
90+
return new RdbmsSaga(
91+
$sagaName,
92+
$sagaId,
93+
$payload,
94+
$now,
95+
null,
96+
);
97+
}
98+
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();
112+
113+
return new RdbmsSaga(
114+
$sagaName,
115+
$sagaId,
116+
$payload,
117+
$previous->createdAt,
118+
$now,
119+
);
120+
}
121+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gember\RdbmsEventStoreDoctrineDbal\Saga\TableSchema;
6+
7+
final readonly class SagaStoreTableSchema
8+
{
9+
public function __construct(
10+
public string $tableName,
11+
public string $sagaIdFieldName,
12+
public string $sagaNameFieldName,
13+
public string $payloadFieldName,
14+
public string $createdAtFieldName,
15+
public string $createdAtFieldFormat,
16+
public string $updatedAtFieldName,
17+
public string $updatedAtFieldFormat,
18+
) {}
19+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gember\RdbmsEventStoreDoctrineDbal\Saga\TableSchema;
6+
7+
final readonly class SagaTableSchemaFactory
8+
{
9+
public static function createDefaultSagaStore(
10+
string $tableName = 'saga_store',
11+
string $sagaIdFieldName = 'saga_id',
12+
string $sagaNameFieldName = 'saga_name',
13+
string $payloadFieldName = 'payload',
14+
string $createdAtFieldName = 'created_at',
15+
string $createdAtFieldFormat = 'Y-m-d H:i:s.u',
16+
string $updatedAtFieldName = 'updated_at',
17+
string $updatedAtFieldFormat = 'Y-m-d H:i:s.u',
18+
): SagaStoreTableSchema {
19+
return new SagaStoreTableSchema(
20+
$tableName,
21+
$sagaIdFieldName,
22+
$sagaNameFieldName,
23+
$payloadFieldName,
24+
$createdAtFieldName,
25+
$createdAtFieldFormat,
26+
$updatedAtFieldName,
27+
$updatedAtFieldFormat,
28+
);
29+
}
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gember\RdbmsEventStoreDoctrineDbal\Test\Saga;
6+
7+
use Gember\RdbmsEventStoreDoctrineDbal\Saga\DoctrineDbalRdbmsSagaFactory;
8+
use PHPUnit\Framework\TestCase;
9+
use PHPUnit\Framework\Attributes\Test;
10+
use DateTimeImmutable;
11+
12+
/**
13+
* @internal
14+
*/
15+
final class DoctrineDbalRdbmsSagaFactoryTest extends TestCase
16+
{
17+
#[Test]
18+
public function itShouldCreateRdbmsSaga(): void
19+
{
20+
$saga = (new DoctrineDbalRdbmsSagaFactory())->createFromRow([
21+
'sagaName' => 'some.saga',
22+
'sagaId' => '01K76G1PGKPZ047KDN25PFPEEV',
23+
'payload' => '{"some":"data"}',
24+
'createdAt' => '2018-12-01 12:05:08.234543',
25+
'updatedAt' => null,
26+
]);
27+
28+
self::assertSame('some.saga', $saga->sagaName);
29+
self::assertSame('01K76G1PGKPZ047KDN25PFPEEV', $saga->sagaId);
30+
self::assertSame('{"some":"data"}', $saga->payload);
31+
self::assertEquals(new DateTimeImmutable('2018-12-01 12:05:08.234543'), $saga->createdAt);
32+
self::assertNull($saga->updatedAt);
33+
}
34+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gember\RdbmsEventStoreDoctrineDbal\Test\Saga;
6+
7+
use Doctrine\DBAL\DriverManager;
8+
use Doctrine\DBAL\Tools\DsnParser;
9+
use Gember\DependencyContracts\EventStore\Saga\RdbmsSagaNotFoundException;
10+
use Gember\RdbmsEventStoreDoctrineDbal\Saga\DoctrineDbalRdbmsSagaFactory;
11+
use Gember\RdbmsEventStoreDoctrineDbal\Saga\DoctrineRdbmsSagaStoreRepository;
12+
use Gember\RdbmsEventStoreDoctrineDbal\Saga\TableSchema\SagaTableSchemaFactory;
13+
use PHPUnit\Framework\Attributes\Test;
14+
use PHPUnit\Framework\TestCase;
15+
use Override;
16+
use DateTimeImmutable;
17+
18+
/**
19+
* @internal
20+
*/
21+
final class DoctrineRdbmsSagaStoreRepositoryTest extends TestCase
22+
{
23+
private DoctrineRdbmsSagaStoreRepository $repository;
24+
25+
#[Override]
26+
protected function setUp(): void
27+
{
28+
parent::setUp();
29+
30+
$connection = DriverManager::getConnection((new DsnParser())->parse('pdo-sqlite:///:memory:'));
31+
$connection->executeStatement((string) file_get_contents(__DIR__ . '/../schema.sql'));
32+
33+
$this->repository = new DoctrineRdbmsSagaStoreRepository(
34+
$connection,
35+
SagaTableSchemaFactory::createDefaultSagaStore(),
36+
new DoctrineDbalRdbmsSagaFactory(),
37+
);
38+
}
39+
40+
#[Test]
41+
public function itShouldThrowExceptionWhenSagaNotFound(): void
42+
{
43+
self::expectException(RdbmsSagaNotFoundException::class);
44+
45+
$this->repository->get('some.saga', '01K76GDQ5RT71G7HQVNR264KD4');
46+
}
47+
48+
#[Test]
49+
public function itShouldSaveAndGetSaga(): void
50+
{
51+
$this->repository->save(
52+
'some.saga',
53+
'01K76GDQ5RT71G7HQVNR264KD4',
54+
'{"some":"data"}',
55+
new DateTimeImmutable('2025-10-10 12:00:34'),
56+
);
57+
58+
$saga = $this->repository->get('some.saga', '01K76GDQ5RT71G7HQVNR264KD4');
59+
60+
self::assertSame('some.saga', $saga->sagaName);
61+
self::assertSame('01K76GDQ5RT71G7HQVNR264KD4', $saga->sagaId);
62+
self::assertSame('{"some":"data"}', $saga->payload);
63+
self::assertEquals(new DateTimeImmutable('2025-10-10 12:00:34'), $saga->createdAt);
64+
self::assertNull($saga->updatedAt);
65+
}
66+
67+
#[Test]
68+
public function itShouldSaveExistingSaga(): void
69+
{
70+
$this->repository->save(
71+
'some.saga',
72+
'01K76GDQ5RT71G7HQVNR264KD4',
73+
'{"some":"data"}',
74+
new DateTimeImmutable('2025-10-10 12:00:34'),
75+
);
76+
77+
$this->repository->save(
78+
'some.saga',
79+
'01K76GDQ5RT71G7HQVNR264KD4',
80+
'{"some":"updated"}',
81+
new DateTimeImmutable('2025-10-10 13:30:12'),
82+
);
83+
84+
$saga = $this->repository->get('some.saga', '01K76GDQ5RT71G7HQVNR264KD4');
85+
86+
self::assertSame('some.saga', $saga->sagaName);
87+
self::assertSame('01K76GDQ5RT71G7HQVNR264KD4', $saga->sagaId);
88+
self::assertSame('{"some":"updated"}', $saga->payload);
89+
self::assertEquals(new DateTimeImmutable('2025-10-10 12:00:34'), $saga->createdAt);
90+
self::assertEquals(new DateTimeImmutable('2025-10-10 13:30:12'), $saga->updatedAt);
91+
}
92+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gember\RdbmsEventStoreDoctrineDbal\Test\Saga\TableSchema;
6+
7+
use Gember\RdbmsEventStoreDoctrineDbal\Saga\TableSchema\SagaTableSchemaFactory;
8+
use PHPUnit\Framework\TestCase;
9+
use PHPUnit\Framework\Attributes\Test;
10+
11+
/**
12+
* @internal
13+
*/
14+
final class SagaTableSchemaFactoryTest extends TestCase
15+
{
16+
#[Test]
17+
public function itShouldCreateDefaultEventStoreTableSchema(): void
18+
{
19+
$schema = SagaTableSchemaFactory::createDefaultSagaStore();
20+
21+
self::assertSame('saga_store', $schema->tableName);
22+
self::assertSame('saga_id', $schema->sagaIdFieldName);
23+
self::assertSame('saga_name', $schema->sagaNameFieldName);
24+
self::assertSame('payload', $schema->payloadFieldName);
25+
self::assertSame('created_at', $schema->createdAtFieldName);
26+
self::assertSame('Y-m-d H:i:s.u', $schema->createdAtFieldFormat);
27+
self::assertSame('updated_at', $schema->updatedAtFieldName);
28+
self::assertSame('Y-m-d H:i:s.u', $schema->updatedAtFieldFormat);
29+
}
30+
31+
#[Test]
32+
public function itShouldCreateCustomEventStoreTableSchema(): void
33+
{
34+
$schema = SagaTableSchemaFactory::createDefaultSagaStore(
35+
'custom_saga_store',
36+
'custom_saga_id',
37+
'custom_saga_name',
38+
'custom_payload',
39+
'custom_created_at',
40+
'custom_created_at_format',
41+
'custom_updated_at',
42+
'custom_updated_at_format',
43+
);
44+
45+
self::assertSame('custom_saga_store', $schema->tableName);
46+
self::assertSame('custom_saga_id', $schema->sagaIdFieldName);
47+
self::assertSame('custom_saga_name', $schema->sagaNameFieldName);
48+
self::assertSame('custom_payload', $schema->payloadFieldName);
49+
self::assertSame('custom_created_at', $schema->createdAtFieldName);
50+
self::assertSame('custom_created_at_format', $schema->createdAtFieldFormat);
51+
self::assertSame('custom_updated_at', $schema->updatedAtFieldName);
52+
self::assertSame('custom_updated_at_format', $schema->updatedAtFieldFormat);
53+
}
54+
}

0 commit comments

Comments
 (0)