From 677f607b0ed15e49dfb79fc7bdc51fcd2dd3cb77 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 29 Feb 2024 16:08:28 +0100 Subject: [PATCH 1/4] poc trace --- Makefile | 11 +- .../Projectionist/DefaultProjectionist.php | 102 ++++------------- .../Projector/MetadataProjectorAccessor.php | 83 ++++++++++++++ .../MetadataProjectorAccessorRepository.php | 53 +++++++++ .../Projector/ProjectorAccessor.php | 25 ++++ .../Projector/ProjectorAccessorRepository.php | 13 +++ .../Projector/TraceableProjectorAccessor.php | 53 +++++++++ .../TraceableProjectorAccessorRepository.php | 74 ++++++++++++ src/Repository/MessageDecorator/Trace.php | 13 +++ .../MessageDecorator/TraceDecorator.php | 30 +++++ .../MessageDecorator/TraceStack.php | 33 ++++++ tests/Benchmark/ProjectionistBench.php | 11 +- .../IntegrationTest.php | 3 +- .../BasicIntegrationTest.php | 5 +- .../Projectionist/Aggregate/Profile.php | 14 ++- .../Projectionist/Events/NameChanged.php | 16 +++ .../Projection/ChangeNameProcessor.php | 39 +++++++ .../Projectionist/ProjectionistTest.php | 107 +++++++++++++++++- 18 files changed, 589 insertions(+), 96 deletions(-) create mode 100644 src/Projection/Projector/MetadataProjectorAccessor.php create mode 100644 src/Projection/Projector/MetadataProjectorAccessorRepository.php create mode 100644 src/Projection/Projector/ProjectorAccessor.php create mode 100644 src/Projection/Projector/ProjectorAccessorRepository.php create mode 100644 src/Projection/Projector/TraceableProjectorAccessor.php create mode 100644 src/Projection/Projector/TraceableProjectorAccessorRepository.php create mode 100644 src/Repository/MessageDecorator/Trace.php create mode 100644 src/Repository/MessageDecorator/TraceDecorator.php create mode 100644 src/Repository/MessageDecorator/TraceStack.php create mode 100644 tests/Integration/Projectionist/Events/NameChanged.php create mode 100644 tests/Integration/Projectionist/Projection/ChangeNameProcessor.php diff --git a/Makefile b/Makefile index 07f36723..1fc5334d 100644 --- a/Makefile +++ b/Makefile @@ -30,8 +30,15 @@ psalm-baseline: vendor vendor/bin/psalm --update-baseline --set-baseline=baseline.xml .PHONY: phpunit -phpunit: vendor ## run phpunit tests - XDEBUG_MODE=coverage vendor/bin/phpunit +phpunit: vendor phpunit-unit phpunit-integration ## run phpunit tests + +.PHONY: phpunit-integration +phpunit-integration: vendor ## run phpunit integration tests + vendor/bin/phpunit --testsuite=integration + +.PHONY: phpunit-unit +phpunit-unit: vendor ## run phpunit unit tests + XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit .PHONY: infection infection: vendor ## run infection diff --git a/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php index 6e566fda..6c6652f3 100644 --- a/src/Projection/Projectionist/DefaultProjectionist.php +++ b/src/Projection/Projectionist/DefaultProjectionist.php @@ -5,17 +5,15 @@ namespace Patchlevel\EventSourcing\Projection\Projectionist; use Closure; -use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory; -use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadata; -use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadataFactory; use Patchlevel\EventSourcing\Projection\Projection\Projection; use Patchlevel\EventSourcing\Projection\Projection\ProjectionCriteria; use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus; use Patchlevel\EventSourcing\Projection\Projection\RunMode; use Patchlevel\EventSourcing\Projection\Projection\Store\LockableProjectionStore; use Patchlevel\EventSourcing\Projection\Projection\Store\ProjectionStore; +use Patchlevel\EventSourcing\Projection\Projector\ProjectorAccessor; +use Patchlevel\EventSourcing\Projection\Projector\ProjectorAccessorRepository; use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy; use Patchlevel\EventSourcing\Projection\RetryStrategy\RetryStrategy; use Patchlevel\EventSourcing\Store\Criteria; @@ -23,24 +21,17 @@ use Psr\Log\LoggerInterface; use Throwable; -use function array_map; -use function array_merge; use function count; use function in_array; use function sprintf; final class DefaultProjectionist implements Projectionist { - /** @var array|null */ - private array|null $projectorIndex = null; - - /** @param iterable $projectors */ public function __construct( private readonly Store $messageStore, private readonly ProjectionStore $projectionStore, - private readonly iterable $projectors, + private readonly ProjectorAccessorRepository $projectorRepository, private readonly RetryStrategy $retryStrategy = new ClockBasedRetryStrategy(), - private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(), private readonly LoggerInterface|null $logger = null, ) { } @@ -323,7 +314,7 @@ function (array $projections): void { continue; } - $teardownMethod = $this->resolveTeardownMethod($projector); + $teardownMethod = $projector->teardownMethod(); if (!$teardownMethod) { $this->projectionStore->remove($projection); @@ -402,7 +393,7 @@ function (array $projections): void { continue; } - $teardownMethod = $this->resolveTeardownMethod($projector); + $teardownMethod = $projector->teardownMethod(); if (!$teardownMethod) { $this->projectionStore->remove($projection); @@ -561,7 +552,7 @@ private function handleMessage(int $index, Message $message, Projection $project throw ProjectorNotFound::forProjectionId($projection->id()); } - $subscribeMethods = $this->resolveSubscribeMethods($projector, $message); + $subscribeMethods = $projector->subscribeMethods($message->event()::class); if ($subscribeMethods === []) { $projection->changePosition($index); @@ -611,19 +602,9 @@ private function handleMessage(int $index, Message $message, Projection $project ); } - private function projector(string $projectionId): object|null + private function projector(string $projectionId): ProjectorAccessor|null { - if ($this->projectorIndex === null) { - $this->projectorIndex = []; - - foreach ($this->projectors as $projector) { - $projectorId = $this->projectorMetadata($projector)->id; - - $this->projectorIndex[$projectorId] = $projector; - } - } - - return $this->projectorIndex[$projectionId] ?? null; + return $this->projectorRepository->get($projectionId); } private function handleOutdatedProjections(ProjectionistCriteria $criteria): void @@ -764,7 +745,7 @@ function (array $projections): void { throw ProjectorNotFound::forProjectionId($projection->id()); } - $setupMethod = $this->resolveSetupMethod($projector); + $setupMethod = $projector->setupMethod(); if (!$setupMethod) { $projection->booting(); @@ -810,25 +791,26 @@ private function discoverNewProjections(): void $this->findForUpdate( new ProjectionCriteria(), function (array $projections): void { - foreach ($this->projectors as $projector) { - $metadata = $this->projectorMetadata($projector); + foreach ($this->projectorRepository->all() as $projector) { foreach ($projections as $projection) { - if ($projection->id() === $metadata->id) { + if ($projection->id() === $projector->id()) { continue 2; } } - $this->projectionStore->add(new Projection( - $metadata->id, - $metadata->group, - $metadata->runMode, - )); + $this->projectionStore->add( + new Projection( + $projector->id(), + $projector->group(), + $projector->runMode(), + ) + ); $this->logger?->info( sprintf( 'Projectionist: New Projector "%s" was found and added to the projection store.', - $metadata->id, + $projector->id(), ), ); } @@ -836,52 +818,6 @@ function (array $projections): void { ); } - private function resolveSetupMethod(object $projector): Closure|null - { - $metadata = $this->metadataFactory->metadata($projector::class); - $method = $metadata->setupMethod; - - if ($method === null) { - return null; - } - - return $projector->$method(...); - } - - private function resolveTeardownMethod(object $projector): Closure|null - { - $metadata = $this->metadataFactory->metadata($projector::class); - $method = $metadata->teardownMethod; - - if ($method === null) { - return null; - } - - return $projector->$method(...); - } - - /** @return iterable */ - private function resolveSubscribeMethods(object $projector, Message $message): iterable - { - $event = $message->event(); - $metadata = $this->metadataFactory->metadata($projector::class); - - $methods = array_merge( - $metadata->subscribeMethods[$event::class] ?? [], - $metadata->subscribeMethods[Subscribe::ALL] ?? [], - ); - - return array_map( - static fn (string $method) => $projector->$method(...), - $methods, - ); - } - - private function projectorMetadata(object $projector): ProjectorMetadata - { - return $this->metadataFactory->metadata($projector::class); - } - private function latestIndex(): int { $stream = $this->messageStore->load(null, 1, null, true); diff --git a/src/Projection/Projector/MetadataProjectorAccessor.php b/src/Projection/Projector/MetadataProjectorAccessor.php new file mode 100644 index 00000000..a9bb0ac4 --- /dev/null +++ b/src/Projection/Projector/MetadataProjectorAccessor.php @@ -0,0 +1,83 @@ +> + */ + private array $subscribeCache = []; + + public function __construct( + private readonly object $projector, + private readonly ProjectorMetadata $metadata, + ) { + } + + public function id(): string + { + return $this->metadata->id; + } + + public function group(): string + { + return $this->metadata->group; + } + + public function runMode(): RunMode + { + return $this->metadata->runMode; + } + + public function setupMethod(): Closure|null + { + $method = $this->metadata->setupMethod; + + if ($method === null) { + return null; + } + + return $this->projector->$method(...); + } + + public function teardownMethod(): Closure|null + { + $method = $this->metadata->teardownMethod; + + if ($method === null) { + return null; + } + + return $this->projector->$method(...); + } + + /** + * @param class-string $eventClass + * + * @return iterable + */ + public function subscribeMethods(string $eventClass): iterable + { + if (array_key_exists($eventClass, $this->subscribeCache)) { + return $this->subscribeCache[$eventClass]; + } + + $methods = array_merge( + $this->metadata->subscribeMethods[$eventClass] ?? [], + $this->metadata->subscribeMethods[Subscribe::ALL] ?? [], + ); + + $this->subscribeCache[$eventClass] = array_map( + fn(string $method) => $this->projector->$method(...), + $methods, + ); + + return $this->subscribeCache[$eventClass]; + } +} \ No newline at end of file diff --git a/src/Projection/Projector/MetadataProjectorAccessorRepository.php b/src/Projection/Projector/MetadataProjectorAccessorRepository.php new file mode 100644 index 00000000..0734ec67 --- /dev/null +++ b/src/Projection/Projector/MetadataProjectorAccessorRepository.php @@ -0,0 +1,53 @@ + + */ + private array $projectorsMap = []; + + public function __construct( + private readonly iterable $projectors, + private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory() + ) { + } + + /** + * @return iterable + */ + public function all(): iterable + { + if ($this->init === false) { + $this->init(); + } + + return array_values($this->projectorsMap); + } + + public function get(string $id): ProjectorAccessor|null + { + if ($this->init === false) { + $this->init(); + } + + return $this->projectorsMap[$id] ?? null; + } + + private function init(): void + { + $this->init = true; + + foreach ($this->projectors as $projector) { + $metadata = $this->metadataFactory->metadata($projector::class); + $this->projectorsMap[$metadata->id] = new MetadataProjectorAccessor($projector, $metadata); + } + } +} \ No newline at end of file diff --git a/src/Projection/Projector/ProjectorAccessor.php b/src/Projection/Projector/ProjectorAccessor.php new file mode 100644 index 00000000..481d8651 --- /dev/null +++ b/src/Projection/Projector/ProjectorAccessor.php @@ -0,0 +1,25 @@ + + */ + public function subscribeMethods(string $eventClass): iterable; +} \ No newline at end of file diff --git a/src/Projection/Projector/ProjectorAccessorRepository.php b/src/Projection/Projector/ProjectorAccessorRepository.php new file mode 100644 index 00000000..4a7ecc1b --- /dev/null +++ b/src/Projection/Projector/ProjectorAccessorRepository.php @@ -0,0 +1,13 @@ + + */ + public function all(): iterable; + + public function get(string $id): ProjectorAccessor|null; +} \ No newline at end of file diff --git a/src/Projection/Projector/TraceableProjectorAccessor.php b/src/Projection/Projector/TraceableProjectorAccessor.php new file mode 100644 index 00000000..925026d1 --- /dev/null +++ b/src/Projection/Projector/TraceableProjectorAccessor.php @@ -0,0 +1,53 @@ +parent->id(); + } + + public function group(): string + { + return $this->parent->group(); + } + + public function runMode(): RunMode + { + return $this->parent->runMode(); + } + + public function setupMethod(): Closure|null + { + return $this->parent->setupMethod(); + } + + public function teardownMethod(): Closure|null + { + return $this->parent->teardownMethod(); + } + + /** + * @param class-string $eventClass + * + * @return iterable + */ + public function subscribeMethods(string $eventClass): iterable + { + return array_map( + fn(Closure $closure) => ($this->wrapper)($this, $closure), + $this->parent->subscribeMethods($eventClass) + ); + } +} \ No newline at end of file diff --git a/src/Projection/Projector/TraceableProjectorAccessorRepository.php b/src/Projection/Projector/TraceableProjectorAccessorRepository.php new file mode 100644 index 00000000..a0eec472 --- /dev/null +++ b/src/Projection/Projector/TraceableProjectorAccessorRepository.php @@ -0,0 +1,74 @@ + + */ + private array $projectorsMap = []; + + public function __construct( + private readonly ProjectorAccessorRepository $parent, + private readonly TraceStack $traceStack, + ) { + } + + /** + * @return iterable + */ + public function all(): iterable + { + if ($this->init === false) { + $this->init(); + } + + return array_values($this->projectorsMap); + } + + public function get(string $id): ProjectorAccessor|null + { + if ($this->init === false) { + $this->init(); + } + + return $this->projectorsMap[$id] ?? null; + } + + private function init(): void + { + $this->init = true; + + foreach ($this->parent->all() as $projectorAccessor) { + $this->projectorsMap[$projectorAccessor->id()] = new TraceableProjectorAccessor( + $projectorAccessor, + $this->wrapper(...) + ); + } + } + + public function wrapper($projectorAccessor, Closure $closure): Closure + { + return function (Message $message) use ($projectorAccessor, $closure) { + $trace = new Trace( + $projectorAccessor->id(), + 'event_sourcing:' . $projectorAccessor->group() + ); + + $this->traceStack->add($trace); + try { + return $closure($message); + } finally { + $this->traceStack->remove($trace); + } + }; + } +} \ No newline at end of file diff --git a/src/Repository/MessageDecorator/Trace.php b/src/Repository/MessageDecorator/Trace.php new file mode 100644 index 00000000..9de76719 --- /dev/null +++ b/src/Repository/MessageDecorator/Trace.php @@ -0,0 +1,13 @@ +traceStack->get(); + + if ($traces === []) { + return $message; + } + + return $message->withHeader('trace', array_map( + fn (Trace $trace) => [ + 'id' => $trace->name, + 'type' => $trace->category, + ], + $traces + )); + } +} \ No newline at end of file diff --git a/src/Repository/MessageDecorator/TraceStack.php b/src/Repository/MessageDecorator/TraceStack.php new file mode 100644 index 00000000..1fd0400d --- /dev/null +++ b/src/Repository/MessageDecorator/TraceStack.php @@ -0,0 +1,33 @@ + + */ + private array $traces = []; + + public function add(Trace $trace): void + { + $this->traces[$trace->name] = $trace; + } + + /** + * @return iterable + */ + public function get(): array + { + return array_values($this->traces); + } + + public function remove(Trace $trace): void + { + unset($this->traces[$trace->name]); + } + + private static function key(Trace $trace): string + { + return $trace->category . '#' . $trace->name; + } +} \ No newline at end of file diff --git a/tests/Benchmark/ProjectionistBench.php b/tests/Benchmark/ProjectionistBench.php index fd5050e4..fa8ff604 100644 --- a/tests/Benchmark/ProjectionistBench.php +++ b/tests/Benchmark/ProjectionistBench.php @@ -11,6 +11,7 @@ use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; use Patchlevel\EventSourcing\Projection\Projectionist\Projectionist; +use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepository; use Patchlevel\EventSourcing\Repository\Repository; use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; @@ -83,10 +84,12 @@ public function setUp(): void $this->projectionist = new DefaultProjectionist( $this->store, $projectionStore, - [ - new ProfileProjector($connection), - new SendEmailProcessor(), - ], + new MetadataProjectorAccessorRepository( + [ + new ProfileProjector($connection), + new SendEmailProcessor(), + ], + ), ); } diff --git a/tests/Integration/BankAccountSplitStream/IntegrationTest.php b/tests/Integration/BankAccountSplitStream/IntegrationTest.php index 7fff903b..9df66124 100644 --- a/tests/Integration/BankAccountSplitStream/IntegrationTest.php +++ b/tests/Integration/BankAccountSplitStream/IntegrationTest.php @@ -12,6 +12,7 @@ use Patchlevel\EventSourcing\Projection\Projection\Store\InMemoryStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; +use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Repository\MessageDecorator\ChainMessageDecorator; use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator; @@ -60,7 +61,7 @@ public function testSuccessful(): void $projectionist = new DefaultProjectionist( $store, new InMemoryStore(), - [$bankAccountProjector], + new MetadataProjectorAccessorRepository([$bankAccountProjector]), ); $eventBus = DefaultEventBus::create([$bankAccountProjector]); diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php index 0b095150..f8f3fe47 100644 --- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php +++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php @@ -11,6 +11,7 @@ use Patchlevel\EventSourcing\Projection\Projection\Store\InMemoryStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; +use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; @@ -57,7 +58,7 @@ public function testSuccessful(): void $projectionist = new DefaultProjectionist( $store, new InMemoryStore(), - [$profileProjector], + new MetadataProjectorAccessorRepository([$profileProjector]), ); $eventBus = DefaultEventBus::create([ @@ -125,7 +126,7 @@ public function testSnapshot(): void $projectionist = new DefaultProjectionist( $store, new InMemoryStore(), - [$profileProjection], + new MetadataProjectorAccessorRepository([$profileProjection]), ); $eventBus = DefaultEventBus::create([ diff --git a/tests/Integration/Projectionist/Aggregate/Profile.php b/tests/Integration/Projectionist/Aggregate/Profile.php index 0d827bad..64a4378a 100644 --- a/tests/Integration/Projectionist/Aggregate/Profile.php +++ b/tests/Integration/Projectionist/Aggregate/Profile.php @@ -9,6 +9,7 @@ use Patchlevel\EventSourcing\Attribute\Apply; use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; +use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\NameChanged; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\ProfileId; @@ -28,13 +29,24 @@ public static function create(ProfileId $id, string $name): self return $self; } - #[Apply(ProfileCreated::class)] + public function changeName(string $name): void + { + $this->recordThat(new NameChanged($name)); + } + + #[Apply] protected function applyProfileCreated(ProfileCreated $event): void { $this->id = $event->profileId; $this->name = $event->name; } + #[Apply] + protected function applyNameChanged(NameChanged $event): void + { + $this->name = $event->name; + } + public function name(): string { return $this->name; diff --git a/tests/Integration/Projectionist/Events/NameChanged.php b/tests/Integration/Projectionist/Events/NameChanged.php new file mode 100644 index 00000000..3ffd6e5d --- /dev/null +++ b/tests/Integration/Projectionist/Events/NameChanged.php @@ -0,0 +1,16 @@ +event(); + + assert($profileCreated instanceof ProfileCreated); + + $repository = $this->repositoryManager->get(Profile::class); + + $profile = $repository->load($profileCreated->profileId); + + $profile->changeName('new name'); + + $repository->save($profile); + } +} diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index a80ac1e1..14929e37 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -16,14 +16,19 @@ use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; +use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; +use Patchlevel\EventSourcing\Projection\Projector\TraceableProjectorAccessorRepository; use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; +use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceDecorator; +use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceStack; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; use Patchlevel\EventSourcing\Tests\DbalManager; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Aggregate\Profile; +use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ChangeNameProcessor; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ErrorProducerProjector; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ProfileProjector; use PHPUnit\Framework\TestCase; @@ -86,7 +91,7 @@ public function testHappyPath(): void $projectionist = new DefaultProjectionist( $store, $projectionStore, - [new ProfileProjector($this->projectionConnection)], + new MetadataProjectorAccessorRepository([new ProfileProjector($this->projectionConnection)]), ); self::assertEquals( @@ -128,7 +133,8 @@ public function testHappyPath(): void $projectionist->projections(), ); - $result = $this->projectionConnection->fetchAssociative('SELECT * FROM projection_profile_1 WHERE id = ?', ['1']); + $result = $this->projectionConnection->fetchAssociative('SELECT * FROM projection_profile_1 WHERE id = ?', + ['1']); self::assertIsArray($result); self::assertArrayHasKey('id', $result); @@ -195,7 +201,7 @@ public function testErrorHandling(): void $projectionist = new DefaultProjectionist( $store, $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), new ClockBasedRetryStrategy( $clock, ClockBasedRetryStrategy::DEFAULT_BASE_DELAY, @@ -289,6 +295,101 @@ public function testErrorHandling(): void self::assertEquals(0, $projection->retryAttempt()); } + + public function testProcessor(): void + { + $store = new DoctrineDbalStore( + $this->connection, + DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']), + 'eventstore', + ); + + $clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00')); + + $projectionStore = new DoctrineStore( + $this->connection, + $clock, + ); + + $traceStack = new TraceStack(); + + $manager = new DefaultRepositoryManager( + new AggregateRootRegistry(['profile' => Profile::class]), + $store, + DefaultEventBus::create(), + null, + new TraceDecorator($traceStack) + ); + + $projectorAccessorRepository = new TraceableProjectorAccessorRepository( + new MetadataProjectorAccessorRepository([ + new ChangeNameProcessor($manager) + ]), + $traceStack + ); + + $repository = $manager->get(Profile::class); + + $schemaDirector = new DoctrineSchemaDirector( + $this->connection, + new ChainDoctrineSchemaConfigurator([ + $store, + $projectionStore, + ]), + ); + + $schemaDirector->create(); + + $projectionist = new DefaultProjectionist( + $store, + $projectionStore, + $projectorAccessorRepository, + ); + + self::assertEquals( + [new Projection('profile_change_name', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], + $projectionist->projections(), + ); + + $projectionist->boot(); + + self::assertEquals( + [ + new Projection( + 'profile_change_name', + Projection::DEFAULT_GROUP, + RunMode::FromBeginning, + ProjectionStatus::Active, + lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), + ), + ], + $projectionist->projections(), + ); + + $profile = Profile::create(ProfileId::fromString('1'), 'John'); + $repository->save($profile); + + $projectionist->run(); + + self::assertEquals( + [ + new Projection( + 'profile_change_name', + Projection::DEFAULT_GROUP, + RunMode::FromBeginning, + ProjectionStatus::Active, + 2, + lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), + ), + ], + $projectionist->projections(), + ); + + $events = $store->load(); + + dd(iterator_to_array($events)); + } + /** @param list $projections */ private static function findProjection(array $projections, string $id): Projection { From 6f29f3f8e608eef70c4793c090678b4cea29c801 Mon Sep 17 00:00:00 2001 From: David Badura Date: Mon, 4 Mar 2024 12:41:05 +0100 Subject: [PATCH 2/4] imrove tests --- .../Projectionist/Aggregate/Profile.php | 12 ++++++++++++ .../Projectionist/Events/Reborned.php | 16 ++++++++++++++++ ...eNameProcessor.php => ProfileProcessor.php} | 18 +++++++++++++++++- .../Projectionist/ProjectionistTest.php | 6 +++--- 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 tests/Integration/Projectionist/Events/Reborned.php rename tests/Integration/Projectionist/Projection/{ChangeNameProcessor.php => ProfileProcessor.php} (66%) diff --git a/tests/Integration/Projectionist/Aggregate/Profile.php b/tests/Integration/Projectionist/Aggregate/Profile.php index 64a4378a..7c5599d6 100644 --- a/tests/Integration/Projectionist/Aggregate/Profile.php +++ b/tests/Integration/Projectionist/Aggregate/Profile.php @@ -11,6 +11,7 @@ use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\NameChanged; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; +use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\Reborned; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\ProfileId; #[Aggregate('profile')] @@ -34,6 +35,11 @@ public function changeName(string $name): void $this->recordThat(new NameChanged($name)); } + public function reborn(string $name): void + { + $this->recordThat(new Reborned($name)); + } + #[Apply] protected function applyProfileCreated(ProfileCreated $event): void { @@ -47,6 +53,12 @@ protected function applyNameChanged(NameChanged $event): void $this->name = $event->name; } + #[Apply] + protected function applyReborned(Reborned $event): void + { + $this->name = $event->name; + } + public function name(): string { return $this->name; diff --git a/tests/Integration/Projectionist/Events/Reborned.php b/tests/Integration/Projectionist/Events/Reborned.php new file mode 100644 index 00000000..2bbcb6ac --- /dev/null +++ b/tests/Integration/Projectionist/Events/Reborned.php @@ -0,0 +1,16 @@ +save($profile); } + + #[Subscribe(NameChanged::class)] + public function handleNameChanged(Message $message) + { + $id = ProfileId::fromString($message->aggregateId()); + + $repository = $this->repositoryManager->get(Profile::class); + + $profile = $repository->load($id); + + $profile->reborn('neo'); + + $repository->save($profile); + } } diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index 14929e37..840cf35e 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -28,7 +28,7 @@ use Patchlevel\EventSourcing\Store\DoctrineDbalStore; use Patchlevel\EventSourcing\Tests\DbalManager; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Aggregate\Profile; -use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ChangeNameProcessor; +use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ProfileProcessor; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ErrorProducerProjector; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ProfileProjector; use PHPUnit\Framework\TestCase; @@ -323,7 +323,7 @@ public function testProcessor(): void $projectorAccessorRepository = new TraceableProjectorAccessorRepository( new MetadataProjectorAccessorRepository([ - new ChangeNameProcessor($manager) + new ProfileProcessor($manager) ]), $traceStack ); @@ -378,7 +378,7 @@ public function testProcessor(): void Projection::DEFAULT_GROUP, RunMode::FromBeginning, ProjectionStatus::Active, - 2, + 3, lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), ), ], From f31829fa0b6af92ccd5bb32cfc87022c7644b673 Mon Sep 17 00:00:00 2001 From: David Badura Date: Mon, 4 Mar 2024 14:33:18 +0100 Subject: [PATCH 3/4] refactor code --- baseline.xml | 6 +- .../Projectionist/DefaultProjectionist.php | 3 +- .../Projector/MetadataProjectorAccessor.php | 20 +++-- .../MetadataProjectorAccessorRepository.php | 40 ++++----- .../Projector/ProjectorAccessor.php | 10 ++- .../Projector/ProjectorAccessorRepository.php | 8 +- .../Projector/TraceableProjectorAccessor.php | 38 ++++++-- .../TraceableProjectorAccessorRepository.php | 60 ++++--------- src/Repository/MessageDecorator/Trace.php | 8 +- .../MessageDecorator/TraceDecorator.php | 23 +++-- .../MessageDecorator/TraceHeader.php | 18 ++++ .../MessageDecorator/TraceStack.php | 20 +++-- .../Projectionist/Aggregate/Profile.php | 12 --- .../Projectionist/Events/Reborned.php | 16 ---- .../Projection/ProfileProcessor.php | 18 +--- .../Projectionist/ProjectionistTest.php | 70 +++++++++------ .../DefaultProjectionistTest.php | 87 ++++++++++--------- 17 files changed, 237 insertions(+), 220 deletions(-) create mode 100644 src/Repository/MessageDecorator/TraceHeader.php delete mode 100644 tests/Integration/Projectionist/Events/Reborned.php diff --git a/baseline.xml b/baseline.xml index 2a1ccf71..e107ec3e 100644 --- a/baseline.xml +++ b/baseline.xml @@ -56,12 +56,16 @@ errorContext]]> - + + + projector->$method(...)]]> + + diff --git a/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php index 6c6652f3..0f3e905c 100644 --- a/src/Projection/Projectionist/DefaultProjectionist.php +++ b/src/Projection/Projectionist/DefaultProjectionist.php @@ -792,7 +792,6 @@ private function discoverNewProjections(): void new ProjectionCriteria(), function (array $projections): void { foreach ($this->projectorRepository->all() as $projector) { - foreach ($projections as $projection) { if ($projection->id() === $projector->id()) { continue 2; @@ -804,7 +803,7 @@ function (array $projections): void { $projector->id(), $projector->group(), $projector->runMode(), - ) + ), ); $this->logger?->info( diff --git a/src/Projection/Projector/MetadataProjectorAccessor.php b/src/Projection/Projector/MetadataProjectorAccessor.php index a9bb0ac4..543c406f 100644 --- a/src/Projection/Projector/MetadataProjectorAccessor.php +++ b/src/Projection/Projector/MetadataProjectorAccessor.php @@ -1,17 +1,22 @@ > - */ + /** @var array> */ private array $subscribeCache = []; public function __construct( @@ -60,9 +65,9 @@ public function teardownMethod(): Closure|null /** * @param class-string $eventClass * - * @return iterable + * @return list */ - public function subscribeMethods(string $eventClass): iterable + public function subscribeMethods(string $eventClass): array { if (array_key_exists($eventClass, $this->subscribeCache)) { return $this->subscribeCache[$eventClass]; @@ -74,10 +79,11 @@ public function subscribeMethods(string $eventClass): iterable ); $this->subscribeCache[$eventClass] = array_map( - fn(string $method) => $this->projector->$method(...), + /** @return Closure(Message):void */ + fn (string $method) => $this->projector->$method(...), $methods, ); return $this->subscribeCache[$eventClass]; } -} \ No newline at end of file +} diff --git a/src/Projection/Projector/MetadataProjectorAccessorRepository.php b/src/Projection/Projector/MetadataProjectorAccessorRepository.php index 0734ec67..81c4714a 100644 --- a/src/Projection/Projector/MetadataProjectorAccessorRepository.php +++ b/src/Projection/Projector/MetadataProjectorAccessorRepository.php @@ -1,53 +1,51 @@ - */ + /** @var array */ private array $projectorsMap = []; + /** @param iterable $projectors */ public function __construct( private readonly iterable $projectors, - private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory() + private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(), ) { } - /** - * @return iterable - */ + /** @return iterable */ public function all(): iterable { - if ($this->init === false) { - $this->init(); - } - - return array_values($this->projectorsMap); + return array_values($this->projectorAccessorMap()); } public function get(string $id): ProjectorAccessor|null { - if ($this->init === false) { - $this->init(); - } + $map = $this->projectorAccessorMap(); - return $this->projectorsMap[$id] ?? null; + return $map[$id] ?? null; } - private function init(): void + /** @return array */ + private function projectorAccessorMap(): array { - $this->init = true; + if ($this->projectorsMap !== []) { + return $this->projectorsMap; + } foreach ($this->projectors as $projector) { $metadata = $this->metadataFactory->metadata($projector::class); $this->projectorsMap[$metadata->id] = new MetadataProjectorAccessor($projector, $metadata); } + + return $this->projectorsMap; } -} \ No newline at end of file +} diff --git a/src/Projection/Projector/ProjectorAccessor.php b/src/Projection/Projector/ProjectorAccessor.php index 481d8651..00e378da 100644 --- a/src/Projection/Projector/ProjectorAccessor.php +++ b/src/Projection/Projector/ProjectorAccessor.php @@ -1,8 +1,11 @@ + * @return list */ - public function subscribeMethods(string $eventClass): iterable; -} \ No newline at end of file + public function subscribeMethods(string $eventClass): array; +} diff --git a/src/Projection/Projector/ProjectorAccessorRepository.php b/src/Projection/Projector/ProjectorAccessorRepository.php index 4a7ecc1b..267a97e5 100644 --- a/src/Projection/Projector/ProjectorAccessorRepository.php +++ b/src/Projection/Projector/ProjectorAccessorRepository.php @@ -1,13 +1,13 @@ - */ + /** @return iterable */ public function all(): iterable; public function get(string $id): ProjectorAccessor|null; -} \ No newline at end of file +} diff --git a/src/Projection/Projector/TraceableProjectorAccessor.php b/src/Projection/Projector/TraceableProjectorAccessor.php index 925026d1..1ba70b22 100644 --- a/src/Projection/Projector/TraceableProjectorAccessor.php +++ b/src/Projection/Projector/TraceableProjectorAccessor.php @@ -1,15 +1,23 @@ + * @return list */ - public function subscribeMethods(string $eventClass): iterable + public function subscribeMethods(string $eventClass): array { return array_map( - fn(Closure $closure) => ($this->wrapper)($this, $closure), - $this->parent->subscribeMethods($eventClass) + /** + * @param Closure(Message):void $closure + * + * @return Closure(Message):void + */ + fn (Closure $closure) => function (Message $message) use ($closure): void { + $trace = new Trace( + $this->id(), + 'event_sourcing/projector/' . $this->group(), + ); + + $this->traceStack->add($trace); + + try { + $closure($message); + } finally { + $this->traceStack->remove($trace); + } + }, + $this->parent->subscribeMethods($eventClass), ); } -} \ No newline at end of file +} diff --git a/src/Projection/Projector/TraceableProjectorAccessorRepository.php b/src/Projection/Projector/TraceableProjectorAccessorRepository.php index a0eec472..cb5beb78 100644 --- a/src/Projection/Projector/TraceableProjectorAccessorRepository.php +++ b/src/Projection/Projector/TraceableProjectorAccessorRepository.php @@ -1,19 +1,17 @@ - */ + /** @var array */ private array $projectorsMap = []; public function __construct( @@ -22,53 +20,33 @@ public function __construct( ) { } - /** - * @return iterable - */ + /** @return iterable */ public function all(): iterable { - if ($this->init === false) { - $this->init(); - } - - return array_values($this->projectorsMap); + return array_values($this->projectorAccessorMap()); } - public function get(string $id): ProjectorAccessor|null + public function get(string $id): TraceableProjectorAccessor|null { - if ($this->init === false) { - $this->init(); - } + $map = $this->projectorAccessorMap(); - return $this->projectorsMap[$id] ?? null; + return $map[$id] ?? null; } - private function init(): void + /** @return array */ + private function projectorAccessorMap(): array { - $this->init = true; + if ($this->projectorsMap !== []) { + return $this->projectorsMap; + } foreach ($this->parent->all() as $projectorAccessor) { $this->projectorsMap[$projectorAccessor->id()] = new TraceableProjectorAccessor( $projectorAccessor, - $this->wrapper(...) + $this->traceStack, ); } - } - - public function wrapper($projectorAccessor, Closure $closure): Closure - { - return function (Message $message) use ($projectorAccessor, $closure) { - $trace = new Trace( - $projectorAccessor->id(), - 'event_sourcing:' . $projectorAccessor->group() - ); - $this->traceStack->add($trace); - try { - return $closure($message); - } finally { - $this->traceStack->remove($trace); - } - }; + return $this->projectorsMap; } -} \ No newline at end of file +} diff --git a/src/Repository/MessageDecorator/Trace.php b/src/Repository/MessageDecorator/Trace.php index 9de76719..bcc2357b 100644 --- a/src/Repository/MessageDecorator/Trace.php +++ b/src/Repository/MessageDecorator/Trace.php @@ -1,13 +1,15 @@ withHeader('trace', array_map( - fn (Trace $trace) => [ - 'id' => $trace->name, - 'type' => $trace->category, - ], - $traces + return $message->withHeader(new TraceHeader( + array_map( + static fn (Trace $trace) => [ + 'name' => $trace->name, + 'category' => $trace->category, + ], + $traces, + ), )); } -} \ No newline at end of file +} diff --git a/src/Repository/MessageDecorator/TraceHeader.php b/src/Repository/MessageDecorator/TraceHeader.php new file mode 100644 index 00000000..c7c10d23 --- /dev/null +++ b/src/Repository/MessageDecorator/TraceHeader.php @@ -0,0 +1,18 @@ + $traces */ + public function __construct( + public readonly array $traces, + ) { + } +} diff --git a/src/Repository/MessageDecorator/TraceStack.php b/src/Repository/MessageDecorator/TraceStack.php index 1fd0400d..24b71b9e 100644 --- a/src/Repository/MessageDecorator/TraceStack.php +++ b/src/Repository/MessageDecorator/TraceStack.php @@ -1,21 +1,23 @@ - */ + /** @var array */ private array $traces = []; public function add(Trace $trace): void { - $this->traces[$trace->name] = $trace; + $this->traces[self::key($trace)] = $trace; } - /** - * @return iterable - */ + /** @return list */ public function get(): array { return array_values($this->traces); @@ -23,11 +25,11 @@ public function get(): array public function remove(Trace $trace): void { - unset($this->traces[$trace->name]); + unset($this->traces[self::key($trace)]); } private static function key(Trace $trace): string { return $trace->category . '#' . $trace->name; } -} \ No newline at end of file +} diff --git a/tests/Integration/Projectionist/Aggregate/Profile.php b/tests/Integration/Projectionist/Aggregate/Profile.php index 7c5599d6..64a4378a 100644 --- a/tests/Integration/Projectionist/Aggregate/Profile.php +++ b/tests/Integration/Projectionist/Aggregate/Profile.php @@ -11,7 +11,6 @@ use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\NameChanged; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; -use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\Reborned; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\ProfileId; #[Aggregate('profile')] @@ -35,11 +34,6 @@ public function changeName(string $name): void $this->recordThat(new NameChanged($name)); } - public function reborn(string $name): void - { - $this->recordThat(new Reborned($name)); - } - #[Apply] protected function applyProfileCreated(ProfileCreated $event): void { @@ -53,12 +47,6 @@ protected function applyNameChanged(NameChanged $event): void $this->name = $event->name; } - #[Apply] - protected function applyReborned(Reborned $event): void - { - $this->name = $event->name; - } - public function name(): string { return $this->name; diff --git a/tests/Integration/Projectionist/Events/Reborned.php b/tests/Integration/Projectionist/Events/Reborned.php deleted file mode 100644 index 2bbcb6ac..00000000 --- a/tests/Integration/Projectionist/Events/Reborned.php +++ /dev/null @@ -1,16 +0,0 @@ -save($profile); } - - #[Subscribe(NameChanged::class)] - public function handleNameChanged(Message $message) - { - $id = ProfileId::fromString($message->aggregateId()); - - $repository = $this->repositoryManager->get(Profile::class); - - $profile = $repository->load($id); - - $profile->reborn('neo'); - - $repository->save($profile); - } } diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index 840cf35e..0b5ca0c5 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -8,6 +8,7 @@ use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Clock\FrozenClock; use Patchlevel\EventSourcing\EventBus\DefaultEventBus; +use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\EventBus\Serializer\DefaultHeadersSerializer; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Projection\Projection\Projection; @@ -20,19 +21,22 @@ use Patchlevel\EventSourcing\Projection\Projector\TraceableProjectorAccessorRepository; use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; -use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceDecorator; +use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceHeader; use Patchlevel\EventSourcing\Repository\MessageDecorator\TraceStack; +use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; use Patchlevel\EventSourcing\Tests\DbalManager; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Aggregate\Profile; -use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ProfileProcessor; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ErrorProducerProjector; +use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ProfileProcessor; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Projection\ProfileProjector; use PHPUnit\Framework\TestCase; +use function iterator_to_array; + /** @coversNothing */ final class ProjectionistTest extends TestCase { @@ -133,8 +137,10 @@ public function testHappyPath(): void $projectionist->projections(), ); - $result = $this->projectionConnection->fetchAssociative('SELECT * FROM projection_profile_1 WHERE id = ?', - ['1']); + $result = $this->projectionConnection->fetchAssociative( + 'SELECT * FROM projection_profile_1 WHERE id = ?', + ['1'], + ); self::assertIsArray($result); self::assertArrayHasKey('id', $result); @@ -295,12 +301,15 @@ public function testErrorHandling(): void self::assertEquals(0, $projection->retryAttempt()); } - public function testProcessor(): void { $store = new DoctrineDbalStore( $this->connection, DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']), + DefaultHeadersSerializer::createFromPaths([ + __DIR__ . '/../../../src', + __DIR__, + ]), 'eventstore', ); @@ -318,14 +327,12 @@ public function testProcessor(): void $store, DefaultEventBus::create(), null, - new TraceDecorator($traceStack) + new TraceDecorator($traceStack), ); $projectorAccessorRepository = new TraceableProjectorAccessorRepository( - new MetadataProjectorAccessorRepository([ - new ProfileProcessor($manager) - ]), - $traceStack + new MetadataProjectorAccessorRepository([new ProfileProcessor($manager)]), + $traceStack, ); $repository = $manager->get(Profile::class); @@ -347,7 +354,7 @@ public function testProcessor(): void ); self::assertEquals( - [new Projection('profile_change_name', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], + [new Projection('profile', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], $projectionist->projections(), ); @@ -356,7 +363,7 @@ public function testProcessor(): void self::assertEquals( [ new Projection( - 'profile_change_name', + 'profile', Projection::DEFAULT_GROUP, RunMode::FromBeginning, ProjectionStatus::Active, @@ -371,23 +378,32 @@ public function testProcessor(): void $projectionist->run(); - self::assertEquals( - [ - new Projection( - 'profile_change_name', - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Active, - 3, - lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), - ), - ], - $projectionist->projections(), - ); + $projections = $projectionist->projections(); + + self::assertCount(1, $projections); + self::assertArrayHasKey(0, $projections); - $events = $store->load(); + $projection = $projections[0]; - dd(iterator_to_array($events)); + self::assertEquals('profile', $projection->id()); + + self::assertEquals(ProjectionStatus::Active, $projection->status()); + + /** @var list $messages */ + $messages = iterator_to_array($store->load()); + + self::assertCount(2, $messages); + self::assertArrayHasKey(1, $messages); + + self::assertEquals( + new TraceHeader([ + [ + 'name' => 'profile', + 'category' => 'event_sourcing/projector/default', + ], + ]), + $messages[1]->header(TraceHeader::class), + ); } /** @param list $projections */ diff --git a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php index 33506502..b87f83e0 100644 --- a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php +++ b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php @@ -21,6 +21,7 @@ use Patchlevel\EventSourcing\Projection\Projection\ThrowableToErrorContextTransformer; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; +use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; use Patchlevel\EventSourcing\Projection\RetryStrategy\RetryStrategy; use Patchlevel\EventSourcing\Store\ArrayStream; use Patchlevel\EventSourcing\Store\Criteria; @@ -49,7 +50,7 @@ public function testNothingToBoot(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $store, - [], + new MetadataProjectorAccessorRepository([]), ); $projectionist->boot(); @@ -73,7 +74,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(); @@ -122,7 +123,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(); @@ -182,7 +183,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(); @@ -254,7 +255,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(new ProjectionistCriteria(), 1); @@ -338,7 +339,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector1, $projector2], + new MetadataProjectorAccessorRepository([$projector1, $projector2]), ); $projectionist->boot(); @@ -405,7 +406,7 @@ public function create(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(); @@ -462,7 +463,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(); @@ -518,7 +519,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(); @@ -567,7 +568,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(); @@ -616,7 +617,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->boot(); @@ -654,7 +655,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->run(); @@ -700,7 +701,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->run(); @@ -753,7 +754,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->run(new ProjectionistCriteria(), 1); @@ -821,7 +822,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector1, $projector2], + new MetadataProjectorAccessorRepository([$projector1, $projector2]), ); $projectionist->run(); @@ -881,7 +882,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->run(); @@ -924,7 +925,7 @@ public function testRunningMarkOutdated(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [], + new MetadataProjectorAccessorRepository([]), ); $projectionist->run(); @@ -959,7 +960,7 @@ public function testRunningWithoutActiveProjectors(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [], + new MetadataProjectorAccessorRepository([]), ); $projectionist->run(); @@ -1000,7 +1001,7 @@ public function handle(Message $message): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->run(); @@ -1031,7 +1032,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->teardown(); @@ -1067,7 +1068,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->teardown(); @@ -1105,7 +1106,7 @@ public function drop(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->teardown(); @@ -1144,7 +1145,7 @@ public function drop(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->teardown(); @@ -1171,7 +1172,7 @@ public function testTeardownWithoutProjector(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [], + new MetadataProjectorAccessorRepository([]), ); $projectionist->teardown(); @@ -1193,7 +1194,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->remove(); @@ -1235,7 +1236,7 @@ public function drop(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->remove(); @@ -1265,7 +1266,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->remove(); @@ -1301,7 +1302,7 @@ public function drop(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->remove(); @@ -1327,7 +1328,7 @@ public function testRemoveWithoutProjector(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [], + new MetadataProjectorAccessorRepository([]), ); $projectionist->remove(); @@ -1349,7 +1350,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->reactivate(); @@ -1387,7 +1388,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->reactivate(); @@ -1424,7 +1425,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->reactivate(); @@ -1460,7 +1461,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->reactivate(); @@ -1496,7 +1497,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->reactivate(); @@ -1524,7 +1525,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->pause(); @@ -1560,7 +1561,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->pause(); @@ -1596,7 +1597,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->pause(); @@ -1634,7 +1635,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->pause(); @@ -1664,7 +1665,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projections = $projectionist->projections(); @@ -1722,7 +1723,7 @@ public function subscribe(): void $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), $retryStrategy->reveal(), ); @@ -1774,7 +1775,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), $retryStrategy->reveal(), ); @@ -1809,7 +1810,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore->reveal(), - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionistCriteria = new ProjectionistCriteria( @@ -1849,7 +1850,7 @@ class { $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore->reveal(), - [$projector], + new MetadataProjectorAccessorRepository([$projector]), ); $projectionist->{$method}(); From bb6fcc92bfc2f44fa99ebdd4e62b4eecd02c2cb9 Mon Sep 17 00:00:00 2001 From: David Badura Date: Mon, 4 Mar 2024 16:46:41 +0100 Subject: [PATCH 4/4] update docs and add more tests --- docs/pages/getting_started.md | 3 +- docs/pages/projection.md | 13 +- ...etadataProjectorAccessorRepositoryTest.php | 45 ++++ .../MetadataProjectorAccessorTest.php | 206 ++++++++++++++++++ .../MessageDecorator/TraceDecoratorTest.php | 59 +++++ .../MessageDecorator/TraceStackTest.php | 33 +++ 6 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php create mode 100644 tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php create mode 100644 tests/Unit/Repository/MessageDecorator/TraceDecoratorTest.php create mode 100644 tests/Unit/Repository/MessageDecorator/TraceStackTest.php diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index ca32cfb0..9b8c53ea 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -281,6 +281,7 @@ use Doctrine\DBAL\DriverManager; use Patchlevel\EventSourcing\EventBus\DefaultEventBus; use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; +use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; @@ -306,7 +307,7 @@ $eventStore = new DoctrineDbalStore( $hotelProjector = new HotelProjector($projectionConnection); -$projectorRepository = new ProjectorRepository([ +$projectorRepository = new MetadataProjectorAccessorRepository([ $hotelProjector, ]); diff --git a/docs/pages/projection.md b/docs/pages/projection.md index ecfe7916..a57b3246 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -469,6 +469,17 @@ $retryStrategy = new ClockBasedRetryStrategy( You can reactivate the projection manually or remove it and rebuild it from scratch. +### Projector Accessor + +The projector accessor is responsible for providing the projectors to the projectionist. +We provide a metadata projector accessor repository by default. + +```php +use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorAccessorRepository; + +$projectorAccessorRepository = new MetadataProjectorAccessorRepository([$projector1, $projector2, $projector3]); +``` + ### Projectionist Now we can create the projectionist and plug together the necessary services. @@ -481,7 +492,7 @@ use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; $projectionist = new DefaultProjectionist( $eventStore, $projectionStore, - [$projector1, $projector2, $projector3], + $projectorAccessorRepository, $retryStrategy, ); ``` diff --git a/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php b/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php new file mode 100644 index 00000000..48615474 --- /dev/null +++ b/tests/Unit/Projection/Projector/MetadataProjectorAccessorRepositoryTest.php @@ -0,0 +1,45 @@ +all()); + self::assertNull($repository->get('foo')); + } + + public function testWithProjector(): void + { + $projector = new #[Projector('foo')] + class { + }; + $metadataFactory = new AttributeProjectorMetadataFactory(); + + $repository = new MetadataProjectorAccessorRepository( + [$projector], + $metadataFactory, + ); + + $accessor = new MetadataProjectorAccessor( + $projector, + $metadataFactory->metadata($projector::class), + ); + + self::assertEquals([$accessor], $repository->all()); + self::assertEquals($accessor, $repository->get('foo')); + } +} diff --git a/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php b/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php new file mode 100644 index 00000000..9b5d801e --- /dev/null +++ b/tests/Unit/Projection/Projector/MetadataProjectorAccessorTest.php @@ -0,0 +1,206 @@ +metadata($projector::class), + ); + + self::assertEquals('profile', $accessor->id()); + } + + public function testGroup(): void + { + $projector = new #[Projector('profile')] + class { + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + self::assertEquals('default', $accessor->group()); + } + + public function testRunMode(): void + { + $projector = new #[Projector('profile')] + class { + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + self::assertEquals(RunMode::FromBeginning, $accessor->runMode()); + } + + public function testSubscribeMethod(): void + { + $projector = new #[Projector('profile')] + class { + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(Message $message): void + { + } + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + $result = $accessor->subscribeMethods(ProfileCreated::class); + + self::assertEquals([ + $projector->onProfileCreated(...), + ], $result); + } + + public function testMultipleSubscribeMethod(): void + { + $projector = new #[Projector('profile')] + class { + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(Message $message): void + { + } + + #[Subscribe(ProfileCreated::class)] + public function onFoo(Message $message): void + { + } + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + $result = $accessor->subscribeMethods(ProfileCreated::class); + + self::assertEquals([ + $projector->onProfileCreated(...), + $projector->onFoo(...), + ], $result); + } + + public function testSubscribeAllMethod(): void + { + $projector = new #[Projector('profile')] + class { + #[Subscribe('*')] + public function onProfileCreated(Message $message): void + { + } + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + $result = $accessor->subscribeMethods(ProfileCreated::class); + + self::assertEquals([ + $projector->onProfileCreated(...), + ], $result); + } + + public function testSetupMethod(): void + { + $projector = new #[Projector('profile')] + class { + #[Setup] + public function method(): void + { + } + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + $result = $accessor->setupMethod(); + + self::assertEquals($projector->method(...), $result); + } + + public function testNotSetupMethod(): void + { + $projector = new #[Projector('profile')] + class { + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + $result = $accessor->setupMethod(); + + self::assertNull($result); + } + + public function testTeardownMethod(): void + { + $projector = new #[Projector('profile')] + class { + #[Teardown] + public function method(): void + { + } + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + $result = $accessor->teardownMethod(); + + self::assertEquals($projector->method(...), $result); + } + + public function testNotTeardownMethod(): void + { + $projector = new #[Projector('profile')] + class { + }; + + $accessor = new MetadataProjectorAccessor( + $projector, + (new AttributeProjectorMetadataFactory())->metadata($projector::class), + ); + + $result = $accessor->teardownMethod(); + + self::assertNull($result); + } +} diff --git a/tests/Unit/Repository/MessageDecorator/TraceDecoratorTest.php b/tests/Unit/Repository/MessageDecorator/TraceDecoratorTest.php new file mode 100644 index 00000000..4434d586 --- /dev/null +++ b/tests/Unit/Repository/MessageDecorator/TraceDecoratorTest.php @@ -0,0 +1,59 @@ +expectException(HeaderNotFound::class); + + $stack = new TraceStack(); + $decorator = new TraceDecorator($stack); + + $message = new Message(new stdClass()); + + $decoratedMessage = $decorator($message); + + self::assertEquals($message, $decoratedMessage); + + $decoratedMessage->header(TraceHeader::class); + } + + public function testWithTrace(): void + { + $stack = new TraceStack(); + $stack->add(new Trace('name', 'category')); + + $decorator = new TraceDecorator($stack); + + $message = new Message(new stdClass()); + + $decoratedMessage = $decorator($message); + + self::assertEquals( + new TraceHeader([ + [ + 'name' => 'name', + 'category' => 'category', + ], + ]), + $decoratedMessage->header(TraceHeader::class), + ); + } +} diff --git a/tests/Unit/Repository/MessageDecorator/TraceStackTest.php b/tests/Unit/Repository/MessageDecorator/TraceStackTest.php new file mode 100644 index 00000000..f348e353 --- /dev/null +++ b/tests/Unit/Repository/MessageDecorator/TraceStackTest.php @@ -0,0 +1,33 @@ +get()); + + $trace = new Trace('name', 'category'); + + $stack->add($trace); + + self::assertEquals([$trace], $stack->get()); + + $stack->remove($trace); + + self::assertEquals([], $stack->get()); + } +}