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/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/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/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php index 6e566fda..0f3e905c 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,25 @@ 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 +817,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..543c406f --- /dev/null +++ b/src/Projection/Projector/MetadataProjectorAccessor.php @@ -0,0 +1,89 @@ +> */ + 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 list + */ + public function subscribeMethods(string $eventClass): array + { + 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( + /** @return Closure(Message):void */ + fn (string $method) => $this->projector->$method(...), + $methods, + ); + + return $this->subscribeCache[$eventClass]; + } +} diff --git a/src/Projection/Projector/MetadataProjectorAccessorRepository.php b/src/Projection/Projector/MetadataProjectorAccessorRepository.php new file mode 100644 index 00000000..81c4714a --- /dev/null +++ b/src/Projection/Projector/MetadataProjectorAccessorRepository.php @@ -0,0 +1,51 @@ + */ + private array $projectorsMap = []; + + /** @param iterable $projectors */ + public function __construct( + private readonly iterable $projectors, + private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(), + ) { + } + + /** @return iterable */ + public function all(): iterable + { + return array_values($this->projectorAccessorMap()); + } + + public function get(string $id): ProjectorAccessor|null + { + $map = $this->projectorAccessorMap(); + + return $map[$id] ?? null; + } + + /** @return array */ + private function projectorAccessorMap(): array + { + 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; + } +} diff --git a/src/Projection/Projector/ProjectorAccessor.php b/src/Projection/Projector/ProjectorAccessor.php new file mode 100644 index 00000000..00e378da --- /dev/null +++ b/src/Projection/Projector/ProjectorAccessor.php @@ -0,0 +1,29 @@ + + */ + public function subscribeMethods(string $eventClass): array; +} diff --git a/src/Projection/Projector/ProjectorAccessorRepository.php b/src/Projection/Projector/ProjectorAccessorRepository.php new file mode 100644 index 00000000..267a97e5 --- /dev/null +++ b/src/Projection/Projector/ProjectorAccessorRepository.php @@ -0,0 +1,13 @@ + */ + public function all(): iterable; + + public function get(string $id): ProjectorAccessor|null; +} diff --git a/src/Projection/Projector/TraceableProjectorAccessor.php b/src/Projection/Projector/TraceableProjectorAccessor.php new file mode 100644 index 00000000..1ba70b22 --- /dev/null +++ b/src/Projection/Projector/TraceableProjectorAccessor.php @@ -0,0 +1,79 @@ +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 list + */ + public function subscribeMethods(string $eventClass): array + { + return array_map( + /** + * @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), + ); + } +} diff --git a/src/Projection/Projector/TraceableProjectorAccessorRepository.php b/src/Projection/Projector/TraceableProjectorAccessorRepository.php new file mode 100644 index 00000000..cb5beb78 --- /dev/null +++ b/src/Projection/Projector/TraceableProjectorAccessorRepository.php @@ -0,0 +1,52 @@ + */ + private array $projectorsMap = []; + + public function __construct( + private readonly ProjectorAccessorRepository $parent, + private readonly TraceStack $traceStack, + ) { + } + + /** @return iterable */ + public function all(): iterable + { + return array_values($this->projectorAccessorMap()); + } + + public function get(string $id): TraceableProjectorAccessor|null + { + $map = $this->projectorAccessorMap(); + + return $map[$id] ?? null; + } + + /** @return array */ + private function projectorAccessorMap(): array + { + if ($this->projectorsMap !== []) { + return $this->projectorsMap; + } + + foreach ($this->parent->all() as $projectorAccessor) { + $this->projectorsMap[$projectorAccessor->id()] = new TraceableProjectorAccessor( + $projectorAccessor, + $this->traceStack, + ); + } + + return $this->projectorsMap; + } +} diff --git a/src/Repository/MessageDecorator/Trace.php b/src/Repository/MessageDecorator/Trace.php new file mode 100644 index 00000000..bcc2357b --- /dev/null +++ b/src/Repository/MessageDecorator/Trace.php @@ -0,0 +1,15 @@ +traceStack->get(); + + if ($traces === []) { + return $message; + } + + return $message->withHeader(new TraceHeader( + array_map( + static fn (Trace $trace) => [ + 'name' => $trace->name, + 'category' => $trace->category, + ], + $traces, + ), + )); + } +} 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 new file mode 100644 index 00000000..24b71b9e --- /dev/null +++ b/src/Repository/MessageDecorator/TraceStack.php @@ -0,0 +1,35 @@ + */ + private array $traces = []; + + public function add(Trace $trace): void + { + $this->traces[self::key($trace)] = $trace; + } + + /** @return list */ + public function get(): array + { + return array_values($this->traces); + } + + public function remove(Trace $trace): void + { + unset($this->traces[self::key($trace)]); + } + + private static function key(Trace $trace): string + { + return $trace->category . '#' . $trace->name; + } +} 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..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; @@ -16,8 +17,13 @@ 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\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; @@ -25,9 +31,12 @@ use Patchlevel\EventSourcing\Tests\DbalManager; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Aggregate\Profile; 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 { @@ -86,7 +95,7 @@ public function testHappyPath(): void $projectionist = new DefaultProjectionist( $store, $projectionStore, - [new ProfileProjector($this->projectionConnection)], + new MetadataProjectorAccessorRepository([new ProfileProjector($this->projectionConnection)]), ); self::assertEquals( @@ -128,7 +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); @@ -195,7 +207,7 @@ public function testErrorHandling(): void $projectionist = new DefaultProjectionist( $store, $projectionStore, - [$projector], + new MetadataProjectorAccessorRepository([$projector]), new ClockBasedRetryStrategy( $clock, ClockBasedRetryStrategy::DEFAULT_BASE_DELAY, @@ -289,6 +301,111 @@ 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', + ); + + $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 ProfileProcessor($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', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], + $projectionist->projections(), + ); + + $projectionist->boot(); + + self::assertEquals( + [ + new Projection( + 'profile', + 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(); + + $projections = $projectionist->projections(); + + self::assertCount(1, $projections); + self::assertArrayHasKey(0, $projections); + + $projection = $projections[0]; + + 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 */ private static function findProjection(array $projections, string $id): Projection { 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}(); 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()); + } +}