Skip to content

Commit 43cffe1

Browse files
committed
refactor projection & query api
1 parent 4c1a4c9 commit 43cffe1

7 files changed

Lines changed: 116 additions & 55 deletions

src/DCB/CompositeProjection.php

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,9 @@
55
namespace Patchlevel\EventSourcing\DCB;
66

77
use Patchlevel\EventSourcing\Message\Message;
8-
use Patchlevel\EventSourcing\Store\Header\TagsHeader;
8+
use Patchlevel\EventSourcing\Store\Query;
99

10-
use function array_diff;
1110
use function array_map;
12-
use function array_merge;
13-
use function array_unique;
14-
use function array_values;
15-
use function in_array;
16-
use function sort;
1711

1812
/**
1913
* @experimental
@@ -27,36 +21,15 @@ public function __construct(
2721
) {
2822
}
2923

30-
/** @return list<string> */
31-
public function tagFilter(): array
24+
public function query(): Query
3225
{
33-
$tags = [];
26+
$query = new Query();
3427

3528
foreach ($this->projections as $projection) {
36-
$tags = array_merge($tags, $projection->tagFilter());
29+
$query = $query->add($projection->queryComponent());
3730
}
3831

39-
return array_values(array_unique($tags));
40-
}
41-
42-
/** @return list<list<string>> */
43-
public function groupedTagFilter(): array
44-
{
45-
$result = [];
46-
47-
foreach ($this->projections as $projection) {
48-
$tags = $projection->tagFilter();
49-
50-
sort($tags);
51-
52-
if (in_array($tags, $result, true)) {
53-
continue;
54-
}
55-
56-
$result[] = $tags;
57-
}
58-
59-
return $result;
32+
return $query;
6033
}
6134

6235
/** @return array<string, mixed> */
@@ -69,12 +42,8 @@ public function initialState(): array
6942

7043
public function apply(mixed $state, Message $message): mixed
7144
{
72-
$tags = $message->header(TagsHeader::class)->tags;
73-
7445
foreach ($this->projections as $name => $projection) {
75-
$neededTags = $projection->tagFilter();
76-
77-
if (!$this->isSubset($neededTags, $tags)) {
46+
if (!$projection->queryComponent()->match($message)) {
7847
continue;
7948
}
8049

@@ -83,13 +52,4 @@ public function apply(mixed $state, Message $message): mixed
8352

8453
return $state;
8554
}
86-
87-
/**
88-
* @param list<string> $needle
89-
* @param list<string> $haystack
90-
*/
91-
private function isSubset(array $needle, array $haystack): bool
92-
{
93-
return empty(array_diff($needle, $haystack));
94-
}
9555
}

src/DCB/Projection.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Closure;
88
use Patchlevel\EventSourcing\Attribute\Apply;
99
use Patchlevel\EventSourcing\Message\Message;
10+
use Patchlevel\EventSourcing\Store\QueryComponent;
1011
use ReflectionClass;
1112
use RuntimeException;
1213

@@ -26,6 +27,11 @@ abstract public function tagFilter(): array;
2627
/** @return S */
2728
abstract public function initialState(): mixed;
2829

30+
public function queryComponent(): QueryComponent
31+
{
32+
return new QueryComponent($this->tagFilter());
33+
}
34+
2935
/**
3036
* @param S $state
3137
*

src/DCB/ProjectionBuilder.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\DCB;
6+
7+
/** @experimental */
8+
interface ProjectionBuilder
9+
{
10+
/**
11+
* @param array<string, Projection> $projections
12+
*
13+
* @return array<string, mixed>
14+
*/
15+
public function build(
16+
array $projections,
17+
): array;
18+
}

src/DCB/StoreDecisionModelBuilder.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66

77
use Patchlevel\EventSourcing\Store\AppendCondition;
88
use Patchlevel\EventSourcing\Store\AppendStore;
9-
use Patchlevel\EventSourcing\Store\Query;
10-
use Patchlevel\EventSourcing\Store\QueryComponent;
11-
12-
use function array_map;
139

1410
/** @experimental */
1511
final class StoreDecisionModelBuilder implements DecisionModelBuilder
@@ -25,11 +21,7 @@ public function build(
2521
): DecisionModel {
2622
$projection = new CompositeProjection($projections);
2723

28-
$query = new Query(...array_map(
29-
static fn (array $tags) => new QueryComponent($tags),
30-
$projection->groupedTagFilter(),
31-
));
32-
24+
$query = $projection->query();
3325
$stream = $this->store->query($query);
3426

3527
$state = $projection->initialState();

src/DCB/StoreProjectionBuilder.php

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\DCB;
6+
7+
use Patchlevel\EventSourcing\Store\AppendStore;
8+
9+
/** @experimental */
10+
final class StoreProjectionBuilder implements ProjectionBuilder
11+
{
12+
public function __construct(
13+
private AppendStore $store,
14+
) {
15+
}
16+
17+
/**
18+
* @param array<string, Projection> $projections
19+
*
20+
* @return array<string, mixed>
21+
*/
22+
public function build(
23+
array $projections,
24+
): array {
25+
$projection = new CompositeProjection($projections);
26+
27+
$query = $projection->query();
28+
$stream = $this->store->query($query);
29+
30+
$state = $projection->initialState();
31+
32+
foreach ($stream as $message) {
33+
$state = $projection->apply($state, $message);
34+
}
35+
36+
return $state;
37+
}
38+
}

src/Store/Query.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,17 @@ public function __construct(
1414
) {
1515
$this->components = $components;
1616
}
17+
18+
public function add(QueryComponent $component): self
19+
{
20+
foreach ($this->components as $c) {
21+
if ($c->equals($component)) {
22+
return $this;
23+
}
24+
}
25+
26+
$components = [...$this->components, $component];
27+
28+
return new self(...$components);
29+
}
1730
}

src/Store/QueryComponent.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,45 @@
44

55
namespace Patchlevel\EventSourcing\Store;
66

7+
use Patchlevel\EventSourcing\Message\Message;
8+
use Patchlevel\EventSourcing\Store\Header\TagsHeader;
9+
10+
use function array_diff;
11+
use function sort;
12+
713
final class QueryComponent
814
{
915
/** @param list<string> $tags */
1016
public function __construct(
1117
public readonly array $tags,
1218
) {
19+
sort($tags);
20+
}
21+
22+
public function match(Message $message): bool
23+
{
24+
if ($this->tags === []) {
25+
return true;
26+
}
27+
28+
if (!$message->hasHeader(TagsHeader::class)) {
29+
return false;
30+
}
31+
32+
return $this->isSubset($this->tags, $message->header(TagsHeader::class)->tags);
33+
}
34+
35+
public function equals(self $queryComponent): bool
36+
{
37+
return $this->tags === $queryComponent->tags;
38+
}
39+
40+
/**
41+
* @param list<string> $needle
42+
* @param list<string> $haystack
43+
*/
44+
private function isSubset(array $needle, array $haystack): bool
45+
{
46+
return empty(array_diff($needle, $haystack));
1347
}
1448
}

0 commit comments

Comments
 (0)