Skip to content

Commit 183e754

Browse files
committed
add inline projection feature
1 parent 30220c6 commit 183e754

7 files changed

Lines changed: 295 additions & 16 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
3535
"doctrine/dbal": "^4.4.0",
3636
"doctrine/migrations": "^3.3.2",
37-
"patchlevel/hydrator": "^1.8.0",
37+
"patchlevel/hydrator": "^1.19.0",
3838
"patchlevel/worker": "^1.4.0",
3939
"psr/cache": "^2.0.0 || ^3.0.0",
4040
"psr/clock": "^1.0",

composer.lock

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Subscription\Projection;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Doctrine\DBAL\Schema\Schema;
9+
use Doctrine\DBAL\Types\Types;
10+
use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory;
11+
use Patchlevel\EventSourcing\Metadata\Subscriber\SubscriberMetadataFactory;
12+
use Patchlevel\EventSourcing\Schema\DoctrineHelper;
13+
use Patchlevel\EventSourcing\Schema\DoctrineSchemaConfigurator;
14+
use Patchlevel\Hydrator\Hydrator;
15+
use Patchlevel\Hydrator\HydratorWithContext;
16+
use Patchlevel\Hydrator\MetadataHydrator;
17+
18+
use function json_decode;
19+
use function json_encode;
20+
21+
final class DoctrineProjectionStateStore implements ProjectionStateStore, DoctrineSchemaConfigurator
22+
{
23+
public function __construct(
24+
private readonly Connection $connection,
25+
private readonly Hydrator $hydrator = new MetadataHydrator(),
26+
private readonly SubscriberMetadataFactory $subscriberMetadataFactory = new AttributeSubscriberMetadataFactory(),
27+
private readonly string $tableName = 'projection_state',
28+
) {
29+
}
30+
31+
public function store(Projection $projection): void
32+
{
33+
$subscriberId = $this->subscriberId($projection);
34+
$data = $this->hydrator->extract($projection);
35+
36+
$this->connection->insert($this->tableName, [
37+
'id' => $subscriberId,
38+
'state' => json_encode($data),
39+
]);
40+
}
41+
42+
public function load(Projection $projection): void
43+
{
44+
$subscriberId = $this->subscriberId($projection);
45+
46+
$data = $this->connection->fetchAssociative(
47+
'SELECT * FROM ' . $this->tableName . ' WHERE id = ?',
48+
[$subscriberId],
49+
);
50+
51+
if (!$data) {
52+
return;
53+
}
54+
55+
$this->hydrator->hydrate($projection::class, json_decode($data['state'], true), [HydratorWithContext::OBJECT_TO_POPULATE => $projection]);
56+
}
57+
58+
public function configureSchema(Schema $schema, Connection $connection): void
59+
{
60+
if (!DoctrineHelper::sameDatabase($this->connection, $connection)) {
61+
return;
62+
}
63+
64+
$table = $schema->createTable($this->tableName);
65+
66+
$table->addColumn('id', Types::STRING)
67+
->setLength(255)
68+
->setNotnull(true);
69+
$table->addColumn('state', Types::JSON)
70+
->setNotnull(true);
71+
72+
$table->setPrimaryKey(['id']);
73+
}
74+
75+
public function subscriberId(Projection $projection): string
76+
{
77+
return $this->subscriberMetadataFactory->metadata($projection::class)->id;
78+
}
79+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Subscription\Projection;
6+
7+
use Patchlevel\EventSourcing\Subscription\Subscriber\BatchableSubscriber;
8+
use Patchlevel\Hydrator\Attribute\Ignore;
9+
10+
abstract class Projection implements BatchableSubscriber
11+
{
12+
public function __construct(
13+
#[Ignore]
14+
private readonly ProjectionStateStore $store,
15+
) {
16+
$this->store->load($this);
17+
}
18+
19+
public function beginBatch(): void
20+
{
21+
// do nothing
22+
}
23+
24+
public function commitBatch(): void
25+
{
26+
$this->store->store($this);
27+
}
28+
29+
public function rollbackBatch(): void
30+
{
31+
$this->store->load($this);
32+
}
33+
34+
public function forceCommit(): bool
35+
{
36+
return false;
37+
}
38+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Subscription\Projection;
6+
7+
interface ProjectionStateStore
8+
{
9+
public function store(Projection $projection): void;
10+
11+
public function load(Projection $projection): void;
12+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Integration\Subscription\Subscriber;
6+
7+
use Patchlevel\EventSourcing\Attribute\Projector;
8+
use Patchlevel\EventSourcing\Attribute\Subscribe;
9+
use Patchlevel\EventSourcing\Subscription\Projection\Projection;
10+
use Patchlevel\EventSourcing\Tests\Integration\Subscription\Events\ProfileCreated;
11+
use Patchlevel\EventSourcing\Tests\Integration\Subscription\ProfileId;
12+
13+
#[Projector('profile_inline')]
14+
final class ProfileInlineProjection extends Projection
15+
{
16+
/** @var array<string, string> */
17+
public array $profiles = [];
18+
19+
#[Subscribe(ProfileCreated::class)]
20+
public function handleProfileCreated(ProfileCreated $profileCreated): void
21+
{
22+
$this->profiles[$profileCreated->profileId->toString()] = $profileCreated->name;
23+
}
24+
25+
public function findById(ProfileId $id): string|null
26+
{
27+
return $this->profiles[$id->toString()] ?? null;
28+
}
29+
}

0 commit comments

Comments
 (0)