diff --git a/baseline.xml b/baseline.xml index 2e51abc62..3e15b6e95 100644 --- a/baseline.xml +++ b/baseline.xml @@ -159,11 +159,6 @@ convertToPHPValue($data['recorded_on'], $platform)]]> - - - messages]]> - - diff --git a/deptrac-baseline.yaml b/deptrac-baseline.yaml index fe42e99fd..83ffb4da8 100644 --- a/deptrac-baseline.yaml +++ b/deptrac-baseline.yaml @@ -14,3 +14,12 @@ deptrac: - Patchlevel\EventSourcing\Aggregate\AggregateRoot Patchlevel\EventSourcing\Attribute\Subscriber: - Patchlevel\EventSourcing\Subscription\RunMode + Patchlevel\EventSourcing\Store\DoctrineDbalStore: + - Patchlevel\EventSourcing\Aggregate\AggregateHeader + Patchlevel\EventSourcing\Store\DoctrineDbalStoreStream: + - Patchlevel\EventSourcing\Aggregate\AggregateHeader + Patchlevel\EventSourcing\Store\InMemoryStore: + - Patchlevel\EventSourcing\Aggregate\AggregateHeader + - Patchlevel\EventSourcing\Metadata\Event\EventRegistry + Patchlevel\EventSourcing\Store\MissingEventRegistry: + - Patchlevel\EventSourcing\Metadata\Event\EventRegistry diff --git a/deptrac.yaml b/deptrac.yaml index ded380b15..9d7c64eb8 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -186,8 +186,6 @@ deptrac: - Cryptography - MetadataAggregate Store: - - Aggregate - - Attribute - Clock - Message - Metadata diff --git a/src/Store/Header/IndexHeader.php b/src/Store/Header/IndexHeader.php index e5baa9816..8e659b6ec 100644 --- a/src/Store/Header/IndexHeader.php +++ b/src/Store/Header/IndexHeader.php @@ -4,10 +4,7 @@ namespace Patchlevel\EventSourcing\Store\Header; -/** - * @psalm-immutable - * @experimental - */ +/** @psalm-immutable */ final class IndexHeader { /** @param positive-int $index */ diff --git a/src/Store/InMemoryStore.php b/src/Store/InMemoryStore.php index e2833430c..55b88ccb1 100644 --- a/src/Store/InMemoryStore.php +++ b/src/Store/InMemoryStore.php @@ -6,21 +6,30 @@ use Closure; use Patchlevel\EventSourcing\Aggregate\AggregateHeader; +use Patchlevel\EventSourcing\Clock\SystemClock; use Patchlevel\EventSourcing\Message\HeaderNotFound; use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; use Patchlevel\EventSourcing\Store\Criteria\AggregateIdCriterion; use Patchlevel\EventSourcing\Store\Criteria\AggregateNameCriterion; use Patchlevel\EventSourcing\Store\Criteria\ArchivedCriterion; use Patchlevel\EventSourcing\Store\Criteria\Criteria; +use Patchlevel\EventSourcing\Store\Criteria\EventsCriterion; use Patchlevel\EventSourcing\Store\Criteria\FromIndexCriterion; use Patchlevel\EventSourcing\Store\Criteria\FromPlayheadCriterion; use Patchlevel\EventSourcing\Store\Criteria\StreamCriterion; +use Patchlevel\EventSourcing\Store\Criteria\ToIndexCriterion; +use Patchlevel\EventSourcing\Store\Header\EventIdHeader; +use Patchlevel\EventSourcing\Store\Header\IndexHeader; use Patchlevel\EventSourcing\Store\Header\PlayheadHeader; +use Patchlevel\EventSourcing\Store\Header\RecordedOnHeader; use Patchlevel\EventSourcing\Store\Header\StreamNameHeader; +use Psr\Clock\ClockInterface; +use Ramsey\Uuid\Uuid; +use Throwable; use function array_filter; use function array_map; -use function array_push; use function array_reverse; use function array_slice; use function array_unique; @@ -35,10 +44,16 @@ final class InMemoryStore implements StreamStore { - /** @param array $messages */ + /** @var array<0|positive-int, Message> */ + private array $messages = []; + + /** @param list $messages */ public function __construct( - private array $messages = [], + array $messages = [], + private readonly EventRegistry|null $eventRegistry = null, + private readonly ClockInterface $clock = new SystemClock(), ) { + $this->save(...$messages); } public function load( @@ -71,7 +86,27 @@ public function count(Criteria|null $criteria = null): int public function save(Message ...$messages): void { - array_push($this->messages, ...$messages); + $this->transactional(function () use ($messages): void { + $count = count($this->messages); + + foreach ($messages as $message) { + $count++; + + if (!$message->hasHeader(IndexHeader::class)) { + $message = $message->withHeader(new IndexHeader($count)); + } + + if (!$message->hasHeader(EventIdHeader::class)) { + $message = $message->withHeader(new EventIdHeader(Uuid::uuid7()->toString())); + } + + if (!$message->hasHeader(RecordedOnHeader::class)) { + $message = $message->withHeader(new RecordedOnHeader($this->clock->now())); + } + + $this->messages[] = $message; + } + }); } /** @@ -81,7 +116,14 @@ public function save(Message ...$messages): void */ public function transactional(Closure $function): void { - $function(); + $messages = $this->messages; + try { + $function(); + } catch (Throwable $e) { + $this->messages = $messages; + + throw $e; + } } /** @return list */ @@ -134,9 +176,11 @@ private function filter(Criteria|null $criteria): array return $this->messages; } + $eventRegistry = $this->eventRegistry; + return array_filter( $this->messages, - static function (Message $message, int $index) use ($criteria): bool { + static function (Message $message) use ($criteria, $eventRegistry): bool { foreach ($criteria->all() as $criterion) { switch ($criterion::class) { case AggregateIdCriterion::class: @@ -222,7 +266,35 @@ static function (Message $message, int $index) use ($criteria): bool { break; case FromIndexCriterion::class: - if ($index < $criterion->fromIndex) { + try { + $index = $message->header(IndexHeader::class)->index; + } catch (HeaderNotFound) { + return false; + } + + if ($index <= $criterion->fromIndex) { + return false; + } + + break; + case ToIndexCriterion::class: + try { + $index = $message->header(IndexHeader::class)->index; + } catch (HeaderNotFound) { + return false; + } + + if ($index >= $criterion->toIndex) { + return false; + } + + break; + case EventsCriterion::class: + if ($eventRegistry === null) { + throw new MissingEventRegistry($criterion::class); + } + + if (!in_array($eventRegistry->eventName($message->event()::class), $criterion->events)) { return false; } diff --git a/src/Store/MissingEventRegistry.php b/src/Store/MissingEventRegistry.php new file mode 100644 index 000000000..0987425f0 --- /dev/null +++ b/src/Store/MissingEventRegistry.php @@ -0,0 +1,19 @@ +withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)), + (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)), ]; $store = new InMemoryStore($expected); @@ -57,10 +73,19 @@ public function testLoadMessages(): void public function testLoadByAggregateId(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new AggregateHeader('profile', '1', 1, new DateTimeImmutable())); + ->withHeader(new AggregateHeader('profile', '1', 1, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new AggregateHeader('profile', '2', 1, new DateTimeImmutable())); - $message3 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + ->withHeader(new AggregateHeader('profile', '2', 1, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); + $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); $store = new InMemoryStore([$message1, $message2, $message3]); @@ -74,10 +99,19 @@ public function testLoadByAggregateId(): void public function testLoadByAggregateName(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new AggregateHeader('foo', '1', 1, new DateTimeImmutable())); + ->withHeader(new AggregateHeader('foo', '1', 1, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new AggregateHeader('bar', '2', 1, new DateTimeImmutable())); - $message3 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + ->withHeader(new AggregateHeader('bar', '2', 1, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); + $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); $store = new InMemoryStore([$message1, $message2, $message3]); @@ -91,10 +125,19 @@ public function testLoadByAggregateName(): void public function testLoadByStreamName(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new StreamNameHeader('foo')); + ->withHeader(new StreamNameHeader('foo')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new StreamNameHeader('bar')); - $message3 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); + $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); $store = new InMemoryStore([$message1, $message2, $message3]); @@ -108,11 +151,20 @@ public function testLoadByStreamName(): void public function testLoadByStreamNameWithLike(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new StreamNameHeader('foo-3')); + ->withHeader(new StreamNameHeader('foo-3')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new StreamNameHeader('bar-1')); + ->withHeader(new StreamNameHeader('bar-1')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) - ->withHeader(new StreamNameHeader('bar-2')); + ->withHeader(new StreamNameHeader('bar-2')) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); $store = new InMemoryStore([$message1, $message2, $message3]); @@ -126,13 +178,25 @@ public function testLoadByStreamNameWithLike(): void public function testLoadFromPlayhead(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new AggregateHeader('foo', '1', 1, new DateTimeImmutable())); + ->withHeader(new AggregateHeader('foo', '1', 1, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new AggregateHeader('foo', '1', 2, new DateTimeImmutable())); + ->withHeader(new AggregateHeader('foo', '1', 2, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) ->withHeader(new StreamNameHeader('foo-1')) - ->withHeader(new PlayheadHeader(3)); - $message4 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + ->withHeader(new PlayheadHeader(3)) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); + $message4 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa607-3a33-7f47-bb66-4223f2390a30')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(4)); $store = new InMemoryStore([$message1, $message2, $message3, $message4]); @@ -146,13 +210,25 @@ public function testLoadFromPlayhead(): void public function testLoadFromIndex(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new AggregateHeader('foo', '1', 1, new DateTimeImmutable())); + ->withHeader(new AggregateHeader('foo', '1', 1, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new AggregateHeader('foo', '1', 2, new DateTimeImmutable())); + ->withHeader(new AggregateHeader('foo', '1', 2, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) ->withHeader(new StreamNameHeader('foo-1')) - ->withHeader(new PlayheadHeader(3)); - $message4 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + ->withHeader(new PlayheadHeader(3)) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); + $message4 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa607-3a33-7f47-bb66-4223f2390a30')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(4)); $store = new InMemoryStore([$message1, $message2, $message3, $message4]); @@ -160,17 +236,87 @@ public function testLoadFromIndex(): void $messages = iterator_to_array($stream); - self::assertSame([$message3, $message4], $messages); + self::assertCount(2, $messages); + self::assertSame( + $message3->header(PlayheadHeader::class)->playhead, + $messages[0]->header(PlayheadHeader::class)->playhead, + ); + self::assertSame( + 3, + $messages[0]->header(IndexHeader::class)->index, + ); + self::assertFalse($message4->hasHeader(PlayheadHeader::class)); + self::assertSame( + 4, + $messages[1]->header(IndexHeader::class)->index, + ); + } + + public function testLoadToIndex(): void + { + $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) + ->withHeader(new AggregateHeader('foo', '1', 1, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new AggregateHeader('foo', '1', 2, new DateTimeImmutable())) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); + $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new StreamNameHeader('foo-1')) + ->withHeader(new PlayheadHeader(3)) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); + $message4 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa607-3a33-7f47-bb66-4223f2390a30')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(4)); + + $store = new InMemoryStore([$message1, $message2, $message3, $message4]); + + $stream = $store->load(new Criteria(new ToIndexCriterion(3))); + + $messages = iterator_to_array($stream); + + self::assertCount(2, $messages); + self::assertSame( + $message1->header(AggregateHeader::class)->playhead, + $messages[0]->header(AggregateHeader::class)->playhead, + ); + self::assertSame( + 1, + $messages[0]->header(IndexHeader::class)->index, + ); + self::assertSame( + $message2->header(AggregateHeader::class)->playhead, + $messages[1]->header(AggregateHeader::class)->playhead, + ); + self::assertSame( + 2, + $messages[1]->header(IndexHeader::class)->index, + ); } public function testLoadByStreamNameWithLikeAll(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new StreamNameHeader('foo-3')); + ->withHeader(new StreamNameHeader('foo-3')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new StreamNameHeader('bar-1')); + ->withHeader(new StreamNameHeader('bar-1')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) - ->withHeader(new StreamNameHeader('bar-2')); + ->withHeader(new StreamNameHeader('bar-2')) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); $store = new InMemoryStore([$message1, $message2, $message3]); @@ -184,8 +330,14 @@ public function testLoadByStreamNameWithLikeAll(): void public function testLoadArchived(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new ArchivedHeader()); - $message2 = new Message(new ProfileVisited(ProfileId::fromString('2'))); + ->withHeader(new ArchivedHeader()) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $store = new InMemoryStore([$message1, $message2]); @@ -196,11 +348,77 @@ public function testLoadArchived(): void self::assertSame([$message1], $messages); } + public function testLoadByEventName(): void + { + $message1 = (new Message(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('s@b.de')))) + ->withHeader(new StreamNameHeader('foo')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); + $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); + + $store = new InMemoryStore( + [$message1, $message2, $message3], + new EventRegistry([ + 'profile_created' => ProfileCreated::class, + 'profile_visited' => ProfileVisited::class, + ]), + ); + + $stream = $store->load(new Criteria(new EventsCriterion(['profile_created']))); + $messages = iterator_to_array($stream); + + self::assertSame([$message1], $messages); + + $stream = $store->load(new Criteria(new EventsCriterion(['profile_visited']))); + $messages = iterator_to_array($stream); + + self::assertSame([$message2, $message3], $messages); + self::assertSame([], iterator_to_array($store->load(new Criteria(new EventsCriterion(['profile_deleted']))))); + } + + public function testLoadByEventNameWithoutRegistry(): void + { + $message1 = (new Message(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('s@b.de')))) + ->withHeader(new StreamNameHeader('foo')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); + $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); + + $store = new InMemoryStore([$message1, $message2, $message3]); + + $this->expectException(MissingEventRegistry::class); + $store->load(new Criteria(new EventsCriterion(['profile_created']))); + } + public function testLoadUnsupportedCriterion(): void { $store = new InMemoryStore([ - new Message(new ProfileVisited(ProfileId::fromString('1'))), - new Message(new ProfileVisited(ProfileId::fromString('2'))), + (new Message(new ProfileVisited(ProfileId::fromString('1')))) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)), + (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)), ]); $this->expectException(UnsupportedCriterion::class); @@ -210,8 +428,14 @@ public function testLoadUnsupportedCriterion(): void public function testLoadLimit(): void { - $message1 = new Message(new ProfileVisited(ProfileId::fromString('1'))); - $message2 = new Message(new ProfileVisited(ProfileId::fromString('2'))); + $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $store = new InMemoryStore([$message1, $message2]); @@ -224,8 +448,14 @@ public function testLoadLimit(): void public function testLoadOffset(): void { - $message1 = new Message(new ProfileVisited(ProfileId::fromString('1'))); - $message2 = new Message(new ProfileVisited(ProfileId::fromString('2'))); + $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $store = new InMemoryStore([$message1, $message2]); @@ -238,8 +468,14 @@ public function testLoadOffset(): void public function testLoadBackwards(): void { - $message1 = new Message(new ProfileVisited(ProfileId::fromString('1'))); - $message2 = new Message(new ProfileVisited(ProfileId::fromString('2'))); + $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $store = new InMemoryStore([$message1, $message2]); @@ -253,8 +489,14 @@ public function testLoadBackwards(): void public function testCount(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new ArchivedHeader()); - $message2 = new Message(new ProfileVisited(ProfileId::fromString('2'))); + ->withHeader(new ArchivedHeader()) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $store = new InMemoryStore([$message1, $message2]); @@ -264,8 +506,14 @@ public function testCount(): void public function testSaveEmpty(): void { $expected = [ - new Message(new ProfileVisited(ProfileId::fromString('1'))), - new Message(new ProfileVisited(ProfileId::fromString('2'))), + (new Message(new ProfileVisited(ProfileId::fromString('1')))) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)), + (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)), ]; $store = new InMemoryStore([]); @@ -282,11 +530,20 @@ public function testSaveEmpty(): void public function testSaveWithExistingMessages(): void { $startMessages = [ - new Message(new ProfileVisited(ProfileId::fromString('1'))), - new Message(new ProfileVisited(ProfileId::fromString('2'))), + (new Message(new ProfileVisited(ProfileId::fromString('1')))) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)), + (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)), ]; - $message1 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + $message1 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); $store = new InMemoryStore($startMessages); @@ -299,15 +556,44 @@ public function testSaveWithExistingMessages(): void self::assertSame([...$startMessages, $message1], $messages); } + public function testSaveWithoutHeaders(): void + { + $store = new InMemoryStore([new Message(new ProfileVisited(ProfileId::fromString('3')))]); + $store->save(new Message(new ProfileVisited(ProfileId::fromString('1')))); + + $stream = $store->load(); + $messages = iterator_to_array($stream); + + self::assertCount(2, $messages); + + foreach ($messages as $message) { + self::assertTrue($message->hasHeader(EventIdHeader::class)); + self::assertTrue($message->hasHeader(RecordedOnHeader::class)); + self::assertTrue($message->hasHeader(IndexHeader::class)); + } + } + public function testStreams(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new StreamNameHeader('foo')); + ->withHeader(new StreamNameHeader('foo')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new StreamNameHeader('bar')); + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) - ->withHeader(new StreamNameHeader('bar')); - $message4 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); + $message4 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa607-3a33-7f47-bb66-4223f2390a30')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(4)); $store = new InMemoryStore([$message1, $message2, $message3, $message4]); @@ -317,12 +603,24 @@ public function testStreams(): void public function testRemove(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new StreamNameHeader('foo')); + ->withHeader(new StreamNameHeader('foo')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new StreamNameHeader('bar')); + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) - ->withHeader(new StreamNameHeader('bar')); - $message4 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); + $message4 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa607-3a33-7f47-bb66-4223f2390a30')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(4)); $store = new InMemoryStore([$message1, $message2, $message3, $message4]); @@ -349,15 +647,63 @@ static function () use (&$called): void { self::assertTrue($called); } + public function testTransactionalThrowAndResetting(): void + { + $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) + ->withHeader(new StreamNameHeader('foo')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); + $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); + + $called = false; + $catched = false; + + $store = new InMemoryStore([$message1]); + + try { + $store->transactional( + static function () use (&$called, $message2, $store): void { + $called = true; + $store->save($message2); + + throw new RuntimeException('test'); + }, + ); + } catch (RuntimeException) { + $catched = true; + } + + self::assertTrue($catched); + self::assertTrue($called); + self::assertCount(1, iterator_to_array($store->load())); + } + public function testClear(): void { $message1 = (new Message(new ProfileVisited(ProfileId::fromString('1')))) - ->withHeader(new StreamNameHeader('foo')); + ->withHeader(new StreamNameHeader('foo')) + ->withHeader(new EventIdHeader('019aa600-56ef-7ca3-b92a-37c53851e2c2')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(1)); $message2 = (new Message(new ProfileVisited(ProfileId::fromString('2')))) - ->withHeader(new StreamNameHeader('bar')); + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa600-8834-752a-ae2e-d8650e84f403')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(2)); $message3 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) - ->withHeader(new StreamNameHeader('bar')); - $message4 = new Message(new ProfileVisited(ProfileId::fromString('3'))); + ->withHeader(new StreamNameHeader('bar')) + ->withHeader(new EventIdHeader('019aa604-94b8-7182-b1dc-f5d4aa9652ca')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(3)); + $message4 = (new Message(new ProfileVisited(ProfileId::fromString('3')))) + ->withHeader(new EventIdHeader('019aa607-3a33-7f47-bb66-4223f2390a30')) + ->withHeader(new RecordedOnHeader(new DateTimeImmutable())) + ->withHeader(new IndexHeader(4)); $store = new InMemoryStore([$message1, $message2, $message3, $message4]);