Skip to content

Commit f3737f0

Browse files
committed
Major rewrite
1 parent 317d8a1 commit f3737f0

19 files changed

+423
-201
lines changed

src/EventStore.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use Wwwision\DCBEventStore\Exceptions\ConditionalAppendFailed;
88
use Wwwision\DCBEventStore\Types\AppendCondition;
99
use Wwwision\DCBEventStore\Types\Events;
10-
use Wwwision\DCBEventStore\Types\SequenceNumber;
10+
use Wwwision\DCBEventStore\Types\ReadOptions;
1111
use Wwwision\DCBEventStore\Types\StreamQuery\StreamQuery;
1212

1313
/**
@@ -19,17 +19,16 @@ interface EventStore
1919
* Returns an event stream that contains events matching the specified {@see StreamQuery} in the order they occurred
2020
*
2121
* @param StreamQuery $query The StreamQuery filter every event has to match
22-
* @param SequenceNumber|null $from If specified, only events with the given {@see SequenceNumber} or a higher one will be returned
22+
* @param ReadOptions|null $options optional configuration for this interaction ({@see ReadOptions})
2323
*/
24-
public function read(StreamQuery $query, ?SequenceNumber $from = null): EventStream;
24+
public function read(StreamQuery $query, ?ReadOptions $options = null): EventStream;
2525

2626
/**
27-
* Returns an event stream that contains events matching the specified {@see StreamQuery} in descending order
27+
* Returns an event stream that contains all events
2828
*
29-
* @param StreamQuery $query The StreamQuery filter every event has to match
30-
* @param SequenceNumber|null $from If specified, only events with the given {@see SequenceNumber} or a lower one will be returned
29+
* @param ReadOptions|null $options optional configuration for this interaction ({@see ReadOptions})
3130
*/
32-
public function readBackwards(StreamQuery $query, ?SequenceNumber $from = null): EventStream;
31+
public function readAll(?ReadOptions $options = null): EventStream;
3332

3433
/**
3534
* Commits the specified $events if the specified {@see AppendCondition} is satisfied

src/Exceptions/ConditionalAppendFailed.php

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
use RuntimeException;
88
use Wwwision\DCBEventStore\EventStore;
9-
use Wwwision\DCBEventStore\Types\EventId;
109
use Wwwision\DCBEventStore\Types\ExpectedHighestSequenceNumber;
11-
use Wwwision\DCBEventStore\Types\SequenceNumber;
1210

1311
/**
1412
* An exception that is thrown when a {@see EventStore::conditionalAppend()} call has failed
@@ -19,18 +17,14 @@ private function __construct(string $message)
1917
{
2018
parent::__construct($message);
2119
}
22-
public static function becauseNoEventMatchedTheQuery(ExpectedHighestSequenceNumber $expectedHighestSequenceNumber): self
23-
{
24-
return new self("Expected highest sequence number \"$expectedHighestSequenceNumber\" but no events in the event store match the specified query");
25-
}
2620

2721
public static function becauseNoEventWhereExpected(): self
2822
{
2923
return new self('The event store contained events matching the specified query but none were expected');
3024
}
3125

32-
public static function becauseHighestExpectedSequenceNumberDoesNotMatch(ExpectedHighestSequenceNumber $expectedHighestSequenceNumber, SequenceNumber $actualSequenceNumber): self
26+
public static function becauseHighestExpectedSequenceNumberDoesNotMatch(ExpectedHighestSequenceNumber $expectedHighestSequenceNumber): self
3327
{
34-
return new self("Expected highest sequence number \"$expectedHighestSequenceNumber\" does not match the actual id of \"$actualSequenceNumber->value\"");
28+
return new self("Expected highest sequence number \"$expectedHighestSequenceNumber\" does not match the actual sequence number");
3529
}
3630
}

src/Helpers/InMemoryEventStore.php

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@
55
namespace Wwwision\DCBEventStore\Helpers;
66

77
use DateTimeImmutable;
8+
use RuntimeException;
89
use Wwwision\DCBEventStore\EventStore;
10+
use Wwwision\DCBEventStore\EventStream;
911
use Wwwision\DCBEventStore\Exceptions\ConditionalAppendFailed;
1012
use Wwwision\DCBEventStore\Types\AppendCondition;
13+
use Wwwision\DCBEventStore\Types\Event;
1114
use Wwwision\DCBEventStore\Types\EventEnvelope;
1215
use Wwwision\DCBEventStore\Types\Events;
16+
use Wwwision\DCBEventStore\Types\ReadOptions;
1317
use Wwwision\DCBEventStore\Types\SequenceNumber;
18+
use Wwwision\DCBEventStore\Types\StreamQuery\Criteria\EventTypesAndTagsCriterion;
19+
use Wwwision\DCBEventStore\Types\StreamQuery\Criteria\EventTypesCriterion;
20+
use Wwwision\DCBEventStore\Types\StreamQuery\Criteria\TagsCriterion;
21+
use Wwwision\DCBEventStore\Types\StreamQuery\Criterion;
22+
use Wwwision\DCBEventStore\Types\StreamQuery\CriterionHashes;
1423
use Wwwision\DCBEventStore\Types\StreamQuery\StreamQuery;
15-
1624
use function count;
1725

1826
/**
@@ -29,9 +37,9 @@
2937
final class InMemoryEventStore implements EventStore
3038
{
3139
/**
32-
* @var EventEnvelope[]
40+
* @var array<array{sequenceNumber:int,recordedAt:DateTimeImmutable,event:Event}>
3341
*/
34-
private array $eventEnvelopes = [];
42+
private array $events = [];
3543

3644
private function __construct()
3745
{
@@ -42,52 +50,74 @@ public static function create(): self
4250
return new self();
4351
}
4452

45-
public function read(StreamQuery $query, ?SequenceNumber $from = null): InMemoryEventStream
53+
public function read(StreamQuery $query, ?ReadOptions $options = null): InMemoryEventStream
4654
{
47-
$matchingEventEnvelopes = $this->eventEnvelopes;
48-
if ($from !== null) {
49-
$matchingEventEnvelopes = array_filter($matchingEventEnvelopes, static fn (EventEnvelope $eventEnvelope) => $eventEnvelope->sequenceNumber->value >= $from->value);
55+
$options ??= ReadOptions::create();
56+
$matchingEventEnvelopes = [];
57+
$events = $this->events;
58+
if ($options->backwards) {
59+
$events = array_reverse($events);
5060
}
51-
if (!$query->isWildcard()) {
52-
$matchingEventEnvelopes = array_filter($matchingEventEnvelopes, static fn (EventEnvelope $eventEnvelope) => $query->matches($eventEnvelope->event));
61+
foreach ($events as $event) {
62+
if ($options->from !== null && (($options->backwards && $event['sequenceNumber'] > $options->from->value) || (!$options->backwards && $event['sequenceNumber'] < $options->from->value))) {
63+
continue;
64+
}
65+
$matchedCriterionHashes = [];
66+
if (!$query->isWildcard()) {
67+
foreach ($query->criteria as $criterion) {
68+
if (array_key_exists($criterion->hash()->value, $matchedCriterionHashes)) {
69+
continue;
70+
}
71+
if (self::criterionMatchesEvent($criterion, $event['event'])) {
72+
$matchedCriterionHashes[$criterion->hash()->value] = true;
73+
}
74+
}
75+
if ($matchedCriterionHashes === []) {
76+
continue;
77+
}
78+
}
79+
$matchingEventEnvelopes[] = new EventEnvelope(SequenceNumber::fromInteger($event['sequenceNumber']), $event['recordedAt'], CriterionHashes::fromArray(array_keys($matchedCriterionHashes)), $event['event']);
5380
}
5481
return InMemoryEventStream::create(...$matchingEventEnvelopes);
5582
}
5683

57-
public function readBackwards(StreamQuery $query, ?SequenceNumber $from = null): InMemoryEventStream
84+
public function readAll(?ReadOptions $options = null): EventStream
5885
{
59-
$matchingEventEnvelopes = array_reverse($this->eventEnvelopes);
60-
if ($from !== null) {
61-
$matchingEventEnvelopes = array_filter($matchingEventEnvelopes, static fn (EventEnvelope $eventEnvelope) => $eventEnvelope->sequenceNumber->value <= $from->value);
62-
}
63-
if (!$query->isWildcard()) {
64-
$matchingEventEnvelopes = array_filter($matchingEventEnvelopes, static fn (EventEnvelope $eventEnvelope) => $query->matches($eventEnvelope->event));
65-
}
66-
return InMemoryEventStream::create(...$matchingEventEnvelopes);
86+
return $this->read(StreamQuery::wildcard(), $options);
87+
}
88+
89+
private static function criterionMatchesEvent(Criterion $criterion, Event $event): bool
90+
{
91+
return match ($criterion::class) {
92+
EventTypesAndTagsCriterion::class => $event->tags->containEvery($criterion->tags) && $criterion->eventTypes->contain($event->type),
93+
EventTypesCriterion::class => $criterion->eventTypes->contain($event->type),
94+
TagsCriterion::class => $event->tags->containEvery($criterion->tags),
95+
default => throw new RuntimeException(sprintf('The criterion type "%s" is not supported by the %s', $criterion::class, self::class), 1700302540),
96+
};
6797
}
6898

6999
public function append(Events $events, AppendCondition $condition): void
70100
{
71101
if (!$condition->expectedHighestSequenceNumber->isAny()) {
72-
$lastEventEnvelope = $this->readBackwards($condition->query)->first();
102+
$lastEventEnvelope = $this->read($condition->query, ReadOptions::create(backwards: true))->first();
73103
if ($lastEventEnvelope === null) {
74104
if (!$condition->expectedHighestSequenceNumber->isNone()) {
75-
throw ConditionalAppendFailed::becauseNoEventMatchedTheQuery($condition->expectedHighestSequenceNumber);
105+
throw ConditionalAppendFailed::becauseHighestExpectedSequenceNumberDoesNotMatch($condition->expectedHighestSequenceNumber);
76106
}
77107
} elseif ($condition->expectedHighestSequenceNumber->isNone()) {
78108
throw ConditionalAppendFailed::becauseNoEventWhereExpected();
79109
} elseif (!$condition->expectedHighestSequenceNumber->matches($lastEventEnvelope->sequenceNumber)) {
80-
throw ConditionalAppendFailed::becauseHighestExpectedSequenceNumberDoesNotMatch($condition->expectedHighestSequenceNumber, $lastEventEnvelope->sequenceNumber);
110+
throw ConditionalAppendFailed::becauseHighestExpectedSequenceNumberDoesNotMatch($condition->expectedHighestSequenceNumber);
81111
}
82112
}
83-
$sequenceNumber = count($this->eventEnvelopes);
113+
$sequenceNumber = count($this->events);
84114
foreach ($events as $event) {
85115
$sequenceNumber++;
86-
$this->eventEnvelopes[] = new EventEnvelope(
87-
SequenceNumber::fromInteger($sequenceNumber),
88-
new DateTimeImmutable(),
89-
$event,
90-
);
116+
$this->events[] = [
117+
'sequenceNumber' => $sequenceNumber,
118+
'recordedAt' => new DateTimeImmutable(),
119+
'event' => $event,
120+
];
91121
}
92122
}
93123
}

src/Types/Event.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
final class Event
1111
{
1212
public function __construct(
13-
public readonly EventId $id, // required for deduplication
13+
public readonly EventId $id, // required for deduplication – TODO really? the sequenceNumber should work
1414
public readonly EventType $type,
1515
public readonly EventData $data, // opaque, no size limit?
1616
public readonly Tags $tags,

src/Types/EventEnvelope.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Wwwision\DCBEventStore\Types;
66

77
use DateTimeImmutable;
8+
use Wwwision\DCBEventStore\Types\StreamQuery\CriterionHashes;
89

910
/**
1011
* An {@see Event} with its global {@see SequenceNumber} in the Events Store
@@ -13,7 +14,8 @@ final class EventEnvelope
1314
{
1415
public function __construct(
1516
public readonly SequenceNumber $sequenceNumber,
16-
public DateTimeImmutable $recordedAt,
17+
public readonly DateTimeImmutable $recordedAt,
18+
public readonly CriterionHashes $criterionHashes,
1719
public readonly Event $event,
1820
) {
1921
}

src/Types/ReadOptions.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Wwwision\DCBEventStore\Types;
6+
7+
use Wwwision\DCBEventStore\EventStore;
8+
9+
/**
10+
* Condition for {@see EventStore::read()} and {@see EventStore::readAll()}
11+
*/
12+
final class ReadOptions
13+
{
14+
/**
15+
* @param SequenceNumber|null $from If specified, only events with the given {@see SequenceNumber} or a higher (lower if backwards) one will be returned
16+
* @param bool $backwards If true, events will be returned in descending order, otherwise in the order they were appended
17+
*/
18+
private function __construct(
19+
public readonly ?SequenceNumber $from,
20+
public bool $backwards,
21+
) {
22+
}
23+
24+
public static function create(
25+
?SequenceNumber $from = null,
26+
bool $backwards = null,
27+
): self {
28+
return new self(
29+
$from,
30+
$backwards ?? false,
31+
);
32+
}
33+
34+
public function with(
35+
?SequenceNumber $from = null,
36+
bool $backwards = null,
37+
): self {
38+
return new self(
39+
$from ?? $this->from,
40+
$backwards ?? $this->backwards,
41+
);
42+
}
43+
}

src/Types/StreamQuery/Criteria.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ public function with(Criterion $criterion): self
4747
return new self(...[...$this->criteria, $criterion]);
4848
}
4949

50+
// TODO: Deduplicate
51+
public function merge(self $other): self
52+
{
53+
return new self(...[...$this->criteria, ...$other->criteria]);
54+
}
55+
5056
public function getIterator(): Traversable
5157
{
5258
return new ArrayIterator($this->criteria);
@@ -66,6 +72,11 @@ public function map(Closure $callback): array
6672
return array_map($callback, $this->criteria);
6773
}
6874

75+
public function hashes(): CriterionHashes
76+
{
77+
return CriterionHashes::fromArray(array_map(static fn (Criterion $criterion) => $criterion->hash(), $this->criteria));
78+
}
79+
6980
/**
7081
* @return Criterion[]
7182
*/

src/Types/StreamQuery/Criteria/EventTypesAndTagsCriterion.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,28 @@
44

55
namespace Wwwision\DCBEventStore\Types\StreamQuery\Criteria;
66

7-
use Wwwision\DCBEventStore\Types\Event;
87
use Wwwision\DCBEventStore\Types\EventTypes;
98
use Wwwision\DCBEventStore\Types\StreamQuery\Criterion;
9+
use Wwwision\DCBEventStore\Types\StreamQuery\CriterionHash;
1010
use Wwwision\DCBEventStore\Types\Tags;
1111

1212
final class EventTypesAndTagsCriterion implements Criterion
1313
{
14+
private readonly CriterionHash $hash;
15+
1416
public function __construct(
1517
public readonly EventTypes $eventTypes,
1618
public readonly Tags $tags,
1719
) {
20+
$this->hash = CriterionHash::fromParts(
21+
substr(substr(self::class, 0, -9), strrpos(self::class, '\\') + 1),
22+
implode(',', $this->eventTypes->toStringArray()),
23+
implode(',', $this->tags->toSimpleArray()),
24+
);
1825
}
1926

20-
public function matches(Event $event): bool
27+
public function hash(): CriterionHash
2128
{
22-
return $this->eventTypes->contain($event->type) && $event->tags->containEvery($this->tags);
29+
return $this->hash;
2330
}
2431
}

src/Types/StreamQuery/Criteria/EventTypesCriterion.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@
44

55
namespace Wwwision\DCBEventStore\Types\StreamQuery\Criteria;
66

7-
use Wwwision\DCBEventStore\Types\Event;
87
use Wwwision\DCBEventStore\Types\EventTypes;
98
use Wwwision\DCBEventStore\Types\StreamQuery\Criterion;
9+
use Wwwision\DCBEventStore\Types\StreamQuery\CriterionHash;
1010

1111
final class EventTypesCriterion implements Criterion
1212
{
13+
private readonly CriterionHash $hash;
14+
1315
public function __construct(
1416
public readonly EventTypes $eventTypes,
1517
) {
18+
$this->hash = CriterionHash::fromParts(
19+
substr(substr(self::class, 0, -9), strrpos(self::class, '\\') + 1),
20+
implode(',', $this->eventTypes->toStringArray()),
21+
);
1622
}
1723

18-
public function matches(Event $event): bool
24+
public function hash(): CriterionHash
1925
{
20-
return $this->eventTypes->contain($event->type);
26+
return $this->hash;
2127
}
2228
}

src/Types/StreamQuery/Criteria/TagsCriterion.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@
44

55
namespace Wwwision\DCBEventStore\Types\StreamQuery\Criteria;
66

7-
use Wwwision\DCBEventStore\Types\Event;
87
use Wwwision\DCBEventStore\Types\StreamQuery\Criterion;
8+
use Wwwision\DCBEventStore\Types\StreamQuery\CriterionHash;
99
use Wwwision\DCBEventStore\Types\Tags;
1010

1111
final class TagsCriterion implements Criterion
1212
{
13+
private readonly CriterionHash $hash;
14+
1315
public function __construct(
1416
public readonly Tags $tags,
1517
) {
18+
$this->hash = CriterionHash::fromParts(
19+
substr(substr(self::class, 0, -9), strrpos(self::class, '\\') + 1),
20+
implode(',', $this->tags->toSimpleArray()),
21+
);
1622
}
1723

18-
public function matches(Event $event): bool
24+
public function hash(): CriterionHash
1925
{
20-
return $event->tags->containEvery($this->tags);
26+
return $this->hash;
2127
}
2228
}

src/Types/StreamQuery/Criterion.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44

55
namespace Wwwision\DCBEventStore\Types\StreamQuery;
66

7-
use Wwwision\DCBEventStore\Types\Event;
8-
97
/**
108
* Common marker interface for {@see StreamQuery} criteria
119
*
1210
* @internal This is not meant to be implemented by external packages!
1311
*/
1412
interface Criterion
1513
{
16-
public function matches(Event $event): bool;
14+
public function hash(): CriterionHash;
1715
}

0 commit comments

Comments
 (0)