diff --git a/baseline.xml b/baseline.xml index 7e58717d8..1fcbb2984 100644 --- a/baseline.xml +++ b/baseline.xml @@ -64,18 +64,18 @@ errorContext]]> - - - projectors]]> - - - + $method $method $method + + + projectors]]> + + new WeakMap() diff --git a/docs/pages/projection.md b/docs/pages/projection.md index 9c89d8bd7..06a6f514b 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -118,12 +118,7 @@ Otherwise the projectionist will not recognize that the projection has changed a To do this, you can add a version to the `projectorId`: ```php -use Doctrine\DBAL\Connection; -use Patchlevel\EventSourcing\Attribute\Setup; -use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Attribute\Handle; use Patchlevel\EventSourcing\Attribute\Projector; -use Patchlevel\EventSourcing\EventBus\Message; #[Projector('profile_1')] final class ProfileProjector @@ -140,16 +135,20 @@ final class ProfileProjector You can also use the `ProjectorUtil` to build the table/collection name. -## Projector Repository +## From Now -The projector repository is responsible for managing the projectors. +Certain projectors operate exclusively on post-release events, disregarding historical data. +Consider, for instance, the scenario of launching a fresh email service. +Its primary function is to dispatch welcome emails to newly registered users triggered by a `ProfileCreated` event. ```php -use Patchlevel\EventSourcing\Projection\Projector\InMemoryProjectorRepository; +use Patchlevel\EventSourcing\Attribute\Projector; -$projectorRepository = new InMemoryProjectorRepository([ - new ProfileProjection($connection) -]); +#[Projector('profile_1', fromNow: true)] +final class WelcomeEmailProjector +{ + // ... +} ``` ## Projectionist @@ -305,7 +304,7 @@ use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; $projectionist = new DefaultProjectionist( $eventStore, $projectionStore, - $projectorRepository + [$projector1, $projector2, $projector3] ); ``` diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index dcf8c680d..8d4101dc7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -15,11 +15,6 @@ parameters: count: 1 path: src/Projection/Projection/Store/DoctrineStore.php - - - message: "#^Method Patchlevel\\\\EventSourcing\\\\Projection\\\\Projector\\\\InMemoryProjectorRepository\\:\\:projectors\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: src/Projection/Projector/InMemoryProjectorRepository.php - - message: "#^Parameter \\#2 \\$data of method Patchlevel\\\\Hydrator\\\\Hydrator\\:\\:hydrate\\(\\) expects array\\, mixed given\\.$#" count: 1 diff --git a/src/Attribute/Projector.php b/src/Attribute/Projector.php index c4ed7011d..2661aced8 100644 --- a/src/Attribute/Projector.php +++ b/src/Attribute/Projector.php @@ -11,6 +11,7 @@ final class Projector { public function __construct( public readonly string $id, + public readonly bool $fromNow = false, ) { } } diff --git a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php b/src/Metadata/Projector/AttributeProjectorMetadataFactory.php index 06d2072f0..5cd10bd69 100644 --- a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php +++ b/src/Metadata/Projector/AttributeProjectorMetadataFactory.php @@ -79,6 +79,7 @@ public function metadata(string $projector): ProjectorMetadata $metadata = new ProjectorMetadata( $projectorInfo->id, + $projectorInfo->fromNow, $subscribeMethods, $createMethod, $dropMethod, diff --git a/src/Metadata/Projector/ProjectorMetadata.php b/src/Metadata/Projector/ProjectorMetadata.php index 251ea0155..265f4cf89 100644 --- a/src/Metadata/Projector/ProjectorMetadata.php +++ b/src/Metadata/Projector/ProjectorMetadata.php @@ -8,6 +8,7 @@ final class ProjectorMetadata { public function __construct( public readonly string $id, + public readonly bool $fromNow = false, /** @var array> */ public readonly array $subscribeMethods = [], public readonly string|null $setupMethod = null, diff --git a/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php index 915b297de..1367bb7d5 100644 --- a/src/Projection/Projectionist/DefaultProjectionist.php +++ b/src/Projection/Projectionist/DefaultProjectionist.php @@ -4,21 +4,24 @@ 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\ProjectorMetadataFactory; use Patchlevel\EventSourcing\Projection\Projection\Projection; use Patchlevel\EventSourcing\Projection\Projection\ProjectionCollection; use Patchlevel\EventSourcing\Projection\Projection\ProjectionCriteria; use Patchlevel\EventSourcing\Projection\Projection\ProjectionError; use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus; use Patchlevel\EventSourcing\Projection\Projection\Store\ProjectionStore; -use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorResolver; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorRepository; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorResolver; use Patchlevel\EventSourcing\Store\Criteria; use Patchlevel\EventSourcing\Store\Store; use Psr\Log\LoggerInterface; use Throwable; +use function array_map; +use function array_merge; use function sprintf; final class DefaultProjectionist implements Projectionist @@ -26,13 +29,14 @@ final class DefaultProjectionist implements Projectionist private const RETRY_LIMIT = 5; /** @var array|null */ - private array|null $projectors = null; + private array|null $projectorIndex = null; + /** @param iterable $projectors */ public function __construct( private readonly Store $streamableMessageStore, private readonly ProjectionStore $projectionStore, - private readonly ProjectorRepository $projectorRepository, - private readonly ProjectorResolver $projectorResolver = new MetadataProjectorResolver(), + private readonly iterable $projectors, + private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(), private readonly LoggerInterface|null $logger = null, ) { } @@ -66,7 +70,7 @@ public function boot( $projection->id(), )); - $setupMethod = $this->projectorResolver->resolveSetupMethod($projector); + $setupMethod = $this->resolveSetupMethod($projector); if (!$setupMethod) { $this->logger?->debug(sprintf( @@ -107,6 +111,8 @@ public function boot( } } + $this->handleFromNowProjections($projections); + $projections = $projections->filterByProjectionStatus(ProjectionStatus::Booting); if ($projections->count() === 0) { @@ -306,7 +312,7 @@ public function teardown(ProjectionCriteria $criteria = new ProjectionCriteria() continue; } - $teardownMethod = $this->projectorResolver->resolveTeardownMethod($projector); + $teardownMethod = $this->resolveTeardownMethod($projector); if (!$teardownMethod) { $this->projectionStore->remove($projection->id()); @@ -372,7 +378,7 @@ public function remove(ProjectionCriteria $criteria = new ProjectionCriteria()): continue; } - $teardownMethod = $this->projectorResolver->resolveTeardownMethod($projector); + $teardownMethod = $this->resolveTeardownMethod($projector); if (!$teardownMethod) { $this->projectionStore->remove($projection->id()); @@ -436,10 +442,9 @@ public function reactivate(ProjectionCriteria $criteria = new ProjectionCriteria public function projections(): ProjectionCollection { $projections = $this->projectionStore->all(); - $projectors = $this->projectors(); - foreach ($projectors as $projector) { - $projectorId = $this->projectorResolver->projectorId($projector); + foreach ($this->projectors as $projector) { + $projectorId = $this->projectorId($projector); if ($projections->has($projectorId)) { continue; @@ -459,7 +464,7 @@ private function handleMessage(int $index, Message $message, Projection $project throw ProjectorNotFound::forProjectionId($projection->id()); } - $subscribeMethods = $this->projectorResolver->resolveSubscribeMethods($projector, $message); + $subscribeMethods = $this->resolveSubscribeMethods($projector, $message); if ($subscribeMethods === []) { $projection->changePosition($index); @@ -523,25 +528,17 @@ private function handleMessage(int $index, Message $message, Projection $project private function projector(string $projectorId): object|null { - $projectors = $this->projectors(); - - return $projectors[$projectorId] ?? null; - } - - /** @return array */ - private function projectors(): array - { - if ($this->projectors === null) { - $this->projectors = []; + if ($this->projectorIndex === null) { + $this->projectorIndex = []; - foreach ($this->projectorRepository->projectors() as $projector) { - $projectorId = $this->projectorResolver->projectorId($projector); + foreach ($this->projectors as $projector) { + $projectorId = $this->projectorId($projector); - $this->projectors[$projectorId] = $projector; + $this->projectorIndex[$projectorId] = $projector; } } - return $this->projectors; + return $this->projectorIndex[$projectorId] ?? null; } private function handleOutdatedProjections(ProjectionCollection $projections): void @@ -593,4 +590,92 @@ private function handleRetryProjections(ProjectionCollection $projections): void ); } } + + private function handleFromNowProjections(ProjectionCollection $projections): void + { + $latestIndex = null; + + foreach ($projections->filterByProjectionStatus(ProjectionStatus::Booting) as $projection) { + $projector = $this->projector($projection->id()); + + if (!$projector) { + continue; + } + + $metadata = $this->metadataFactory->metadata($projector::class); + + if (!$metadata->fromNow) { + continue; + } + + if ($latestIndex === null) { + $latestIndex = $this->latestIndex(); + } + + $projection->changePosition($latestIndex); + $projection->active(); + $this->projectionStore->save($projection); + + $this->logger?->info( + sprintf( + 'Projectionist: Projector "%s" for "%s" is in "from now" mode: skip past messages and set to active.', + $projector::class, + $projection->id(), + ), + ); + } + } + + 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 projectorId(object $projector): string + { + return $this->metadataFactory->metadata($projector::class)->id; + } + + private function latestIndex(): int + { + $stream = $this->streamableMessageStore->load(null, 1, null, true); + + return $stream->index() ?: 1; + } } diff --git a/src/Projection/Projector/InMemoryProjectorRepository.php b/src/Projection/Projector/InMemoryProjectorRepository.php deleted file mode 100644 index ef2aac7df..000000000 --- a/src/Projection/Projector/InMemoryProjectorRepository.php +++ /dev/null @@ -1,20 +0,0 @@ - $projectors */ - public function __construct( - private readonly iterable $projectors = [], - ) { - } - - /** @return list */ - public function projectors(): array - { - return [...$this->projectors]; - } -} diff --git a/src/Projection/Projector/MetadataProjectorResolver.php b/src/Projection/Projector/MetadataProjectorResolver.php deleted file mode 100644 index f19b8c2ff..000000000 --- a/src/Projection/Projector/MetadataProjectorResolver.php +++ /dev/null @@ -1,68 +0,0 @@ -metadataFactory->metadata($projector::class); - $method = $metadata->setupMethod; - - if ($method === null) { - return null; - } - - return $projector->$method(...); - } - - public 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 */ - public 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, - ); - } - - public function projectorId(object $projector): string - { - return $this->metadataFactory->metadata($projector::class)->id; - } -} diff --git a/src/Projection/Projector/ProjectorRepository.php b/src/Projection/Projector/ProjectorRepository.php deleted file mode 100644 index f6c4c4e45..000000000 --- a/src/Projection/Projector/ProjectorRepository.php +++ /dev/null @@ -1,11 +0,0 @@ - */ - public function projectors(): array; -} diff --git a/src/Projection/Projector/ProjectorResolver.php b/src/Projection/Projector/ProjectorResolver.php deleted file mode 100644 index a21ea6d3a..000000000 --- a/src/Projection/Projector/ProjectorResolver.php +++ /dev/null @@ -1,20 +0,0 @@ - */ - public function resolveSubscribeMethods(object $projector, Message $message): iterable; - - public function projectorId(object $projector): string; -} diff --git a/tests/Integration/BankAccountSplitStream/IntegrationTest.php b/tests/Integration/BankAccountSplitStream/IntegrationTest.php index ee33ed898..382b36a1d 100644 --- a/tests/Integration/BankAccountSplitStream/IntegrationTest.php +++ b/tests/Integration/BankAccountSplitStream/IntegrationTest.php @@ -11,7 +11,6 @@ use Patchlevel\EventSourcing\Projection\Projection\ProjectionCriteria; use Patchlevel\EventSourcing\Projection\Projection\Store\InMemoryStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projector\InMemoryProjectorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Repository\MessageDecorator\ChainMessageDecorator; use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator; @@ -52,12 +51,11 @@ public function testSuccessful(): void ); $bankAccountProjector = new BankAccountProjector($this->connection); - $projectionRepository = new InMemoryProjectorRepository([$bankAccountProjector]); $projectionist = new DefaultProjectionist( $store, new InMemoryStore(), - $projectionRepository, + [$bankAccountProjector], ); $eventBus = DefaultEventBus::create([$bankAccountProjector]); diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php index ef67f8ba0..6fcf4a0f6 100644 --- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php +++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php @@ -10,7 +10,6 @@ use Patchlevel\EventSourcing\Projection\Projection\ProjectionCriteria; use Patchlevel\EventSourcing\Projection\Projection\Store\InMemoryStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projector\InMemoryProjectorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; @@ -49,14 +48,11 @@ public function testSuccessful(): void ); $profileProjector = new ProfileProjector($this->connection); - $projectorRepository = new InMemoryProjectorRepository( - [$profileProjector], - ); $projectionist = new DefaultProjectionist( $store, new InMemoryStore(), - $projectorRepository, + [$profileProjector], ); $eventBus = DefaultEventBus::create([ @@ -116,14 +112,11 @@ public function testSnapshot(): void ); $profileProjection = new ProfileProjector($this->connection); - $projectorRepository = new InMemoryProjectorRepository( - [$profileProjection], - ); $projectionist = new DefaultProjectionist( $store, new InMemoryStore(), - $projectorRepository, + [$profileProjection], ); $eventBus = DefaultEventBus::create([ diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index 78a3b14b9..6ce0deec9 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -9,7 +9,6 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projector\InMemoryProjectorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\ChainSchemaConfigurator; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; @@ -69,9 +68,7 @@ public function testRun(): void $projectionist = new DefaultProjectionist( $store, $projectionStore, - new InMemoryProjectorRepository( - [new ProfileProjector($this->connection)], - ), + [new ProfileProjector($this->connection)], ); $projectionist->boot(); diff --git a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php index f447002ab..1ef01a780 100644 --- a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php +++ b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php @@ -5,6 +5,9 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projectionist; use Patchlevel\EventSourcing\Attribute\Projector as ProjectionAttribute; +use Patchlevel\EventSourcing\Attribute\Setup; +use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Teardown; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projection\Projection; use Patchlevel\EventSourcing\Projection\Projection\ProjectionCollection; @@ -14,8 +17,6 @@ use Patchlevel\EventSourcing\Projection\Projection\Store\ErrorContext; use Patchlevel\EventSourcing\Projection\Projection\Store\ProjectionStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorRepository; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorResolver; use Patchlevel\EventSourcing\Store\ArrayStream; use Patchlevel\EventSourcing\Store\Criteria; use Patchlevel\EventSourcing\Store\Store; @@ -41,16 +42,10 @@ public function testNothingToBoot(): void $projectionStore = $this->prophesize(ProjectionStore::class); $projectionStore->all()->willReturn($projectionCollection)->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore->reveal(), - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [], ); $projectionist->boot(); @@ -59,7 +54,6 @@ public function testNothingToBoot(): void public function testBootWithoutCreateMethod(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { }; @@ -73,19 +67,10 @@ class { $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectorResolver->resolveSetupMethod($projector)->willReturn(null); - $projectorResolver->resolveSubscribeMethods($projector, $message)->willReturn([]); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->boot(); @@ -100,17 +85,18 @@ class { public function testBootWithMethods(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public Message|null $message = null; public bool $created = false; + #[Setup] public function create(): void { $this->created = true; } + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->message = $message; @@ -124,19 +110,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSetupMethod($projector)->willReturn($projector->create(...)); - $projectorResolver->resolveSubscribeMethods($projector, $message)->willReturn([$projector->handle(...)]); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->boot(); @@ -154,17 +131,18 @@ public function handle(Message $message): void public function testBootWithLimit(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public Message|null $message = null; public bool $created = false; + #[Setup] public function create(): void { $this->created = true; } + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->message = $message; @@ -178,19 +156,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSetupMethod($projector)->willReturn($projector->create(...)); - $projectorResolver->resolveSubscribeMethods($projector, $message)->willReturn([$projector->handle(...)]); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->boot(new ProjectionCriteria(), 1); @@ -207,11 +176,11 @@ public function handle(Message $message): void public function testBootingWithSkip(): void { $projectionId1 = 'test1'; - $projectorId1 = 'test1'; $projector1 = new #[ProjectionAttribute('test1')] class { public Message|null $message = null; + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->message = $message; @@ -219,11 +188,11 @@ public function handle(Message $message): void }; $projectionId2 = 'test2'; - $projectorId2 = 'test2'; - $projector2 = new #[ProjectionAttribute('test1')] + $projector2 = new #[ProjectionAttribute('test2')] class { public Message|null $message = null; + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->message = $message; @@ -240,19 +209,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector1, $projector2])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSubscribeMethods($projector1, $message)->willReturn([$projector1->handle(...)]); - $projectorResolver->projectorId($projector1)->willReturn($projectorId1); - $projectorResolver->projectorId($projector2)->willReturn($projectorId2); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector1, $projector2], ); $projectionist->boot(); @@ -270,7 +230,6 @@ public function handle(Message $message): void public function testBootWithCreateError(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public function __construct( @@ -278,6 +237,7 @@ public function __construct( ) { } + #[Setup] public function create(): void { throw $this->exception; @@ -291,18 +251,10 @@ public function create(): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->shouldNotBeCalled(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSetupMethod($projector)->willReturn($projector->create(...)); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->boot(); @@ -328,12 +280,12 @@ public function create(): void public function testBootingWithGabInIndex(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { /** @var list */ public array $messages = []; + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->messages[] = $message; @@ -348,19 +300,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([1 => $message1, 3 => $message2]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSubscribeMethods($projector, $message1)->willReturn([$projector->handle(...)]); - $projectorResolver->resolveSubscribeMethods($projector, $message2)->willReturn([$projector->handle(...)]); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->boot(); @@ -374,14 +317,50 @@ public function handle(Message $message): void self::assertSame([$message1, $message2], $projector->messages); } + public function testBootingWithFromNow(): void + { + $projectionId = 'test'; + $projector = new #[ProjectionAttribute('test', fromNow: true)] + class { + public Message|null $message = null; + + #[Subscribe(ProfileVisited::class)] + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Booting)]); + + $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $streamableStore = $this->prophesize(Store::class); + $streamableStore->load(null, 1, null, true)->willReturn(new ArrayStream([$message1]))->shouldBeCalledOnce(); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectionStore, + [$projector], + ); + + $projectionist->boot(); + + self::assertEquals([ + new Projection($projectionId, ProjectionStatus::Active, 1), + ], $projectionStore->savedProjections); + + self::assertNull($projector->message); + } + public function testRunning(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public Message|null $message = null; + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->message = $message; @@ -395,18 +374,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSubscribeMethods($projector, $message)->willReturn([$projector->handle(...)]); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->run(); @@ -421,11 +392,11 @@ public function handle(Message $message): void public function testRunningWithLimit(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public Message|null $message = null; + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->message = $message; @@ -443,18 +414,10 @@ public function handle(Message $message): void ->willReturn(new ArrayStream([$message1, $message2])) ->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSubscribeMethods($projector, $message1)->willReturn([$projector->handle(...)]); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->run(new ProjectionCriteria(), 1); @@ -469,11 +432,11 @@ public function handle(Message $message): void public function testRunningWithSkip(): void { $projectionId1 = 'test1'; - $projectorId1 = 'test1'; $projector1 = new #[ProjectionAttribute('test1')] class { public Message|null $message = null; + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->message = $message; @@ -481,11 +444,11 @@ public function handle(Message $message): void }; $projectionId2 = 'test2'; - $projectorId2 = 'test2'; - $projector2 = new #[ProjectionAttribute('test1')] + $projector2 = new #[ProjectionAttribute('test2')] class { public Message|null $message = null; + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->message = $message; @@ -502,19 +465,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector1, $projector2])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSubscribeMethods($projector1, $message)->willReturn([$projector1->handle(...)]); - $projectorResolver->projectorId($projector1)->willReturn($projectorId1); - $projectorResolver->projectorId($projector2)->willReturn($projectorId2); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector1, $projector2], ); $projectionist->run(); @@ -530,7 +484,6 @@ public function handle(Message $message): void public function testRunningWithError(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public function __construct( @@ -538,6 +491,7 @@ public function __construct( ) { } + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { throw $this->exception; @@ -551,18 +505,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSubscribeMethods($projector, $message)->willReturn([$projector->handle(...)]); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->run(); @@ -590,16 +536,10 @@ public function testRunningMarkOutdated(): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->shouldNotBeCalled(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [], ); $projectionist->run(); @@ -618,16 +558,10 @@ public function testRunningWithoutActiveProjectors(): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->shouldNotBeCalled(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [], ); $projectionist->run(); @@ -638,12 +572,12 @@ public function testRunningWithoutActiveProjectors(): void public function testRunningWithGabInIndex(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { /** @var list */ public array $messages = []; + #[Subscribe(ProfileVisited::class)] public function handle(Message $message): void { $this->messages[] = $message; @@ -658,19 +592,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([1 => $message1, 3 => $message2]))->shouldBeCalledOnce(); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveSubscribeMethods($projector, $message1)->willReturn([$projector->handle(...)]); - $projectorResolver->resolveSubscribeMethods($projector, $message2)->willReturn([$projector->handle(...)]); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->run(); @@ -686,12 +611,12 @@ public function handle(Message $message): void public function testTeardownWithProjector(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public Message|null $message = null; public bool $dropped = false; + #[Teardown] public function drop(): void { $this->dropped = true; @@ -702,18 +627,10 @@ public function drop(): void $streamableStore = $this->prophesize(Store::class); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveTeardownMethod($projector)->willReturn($projector->drop(...)); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->teardown(); @@ -726,12 +643,12 @@ public function drop(): void public function testTeardownWithProjectorAndError(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public Message|null $message = null; public bool $dropped = false; + #[Teardown] public function drop(): void { throw new RuntimeException('ERROR'); @@ -742,18 +659,10 @@ public function drop(): void $streamableStore = $this->prophesize(Store::class); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveTeardownMethod($projector)->willReturn($projector->drop(...)); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->teardown(); @@ -770,16 +679,10 @@ public function testTeardownWithoutProjector(): void $streamableStore = $this->prophesize(Store::class); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [], ); $projectionist->teardown(); @@ -791,11 +694,11 @@ public function testTeardownWithoutProjector(): void public function testRemoveWithProjector(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public bool $dropped = false; + #[Teardown] public function drop(): void { $this->dropped = true; @@ -806,18 +709,10 @@ public function drop(): void $streamableStore = $this->prophesize(Store::class); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveTeardownMethod($projector)->willReturn($projector->drop(...)); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->remove(); @@ -830,7 +725,6 @@ public function drop(): void public function testRemoveWithoutDropMethod(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { }; @@ -839,18 +733,10 @@ class { $streamableStore = $this->prophesize(Store::class); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveTeardownMethod($projector)->willReturn(null); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->remove(); @@ -862,11 +748,11 @@ class { public function testRemoveWithProjectorAndError(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { public bool $dropped = false; + #[Teardown] public function drop(): void { throw new RuntimeException('ERROR'); @@ -877,18 +763,10 @@ public function drop(): void $streamableStore = $this->prophesize(Store::class); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->resolveTeardownMethod($projector)->willReturn($projector->drop(...)); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->remove(); @@ -905,16 +783,10 @@ public function testRemoveWithoutProjector(): void $streamableStore = $this->prophesize(Store::class); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [], ); $projectionist->remove(); @@ -926,7 +798,6 @@ public function testRemoveWithoutProjector(): void public function testReactivate(): void { $projectionId = 'test'; - $projectorId = 'test'; $projector = new #[ProjectionAttribute('test')] class { }; @@ -935,17 +806,10 @@ class { $streamableStore = $this->prophesize(Store::class); - $projectorRepository = $this->prophesize(ProjectorRepository::class); - $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); - - $projectorResolver = $this->prophesize(ProjectorResolver::class); - $projectorResolver->projectorId($projector)->willReturn($projectorId); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, - $projectorRepository->reveal(), - $projectorResolver->reveal(), + [$projector], ); $projectionist->reactivate(); diff --git a/tests/Unit/Projection/Projector/InMemoryProjectorRepositoryTest.php b/tests/Unit/Projection/Projector/InMemoryProjectorRepositoryTest.php deleted file mode 100644 index 746408b9b..000000000 --- a/tests/Unit/Projection/Projector/InMemoryProjectorRepositoryTest.php +++ /dev/null @@ -1,27 +0,0 @@ -projectors()); - } - - public function testGetAllProjectors(): void - { - $projector = new class { - }; - $repository = new InMemoryProjectorRepository([$projector]); - - self::assertEquals([$projector], $repository->projectors()); - } -} diff --git a/tests/Unit/Projection/Projector/MetadataProjectorResolverTest.php b/tests/Unit/Projection/Projector/MetadataProjectorResolverTest.php deleted file mode 100644 index 9d3eed507..000000000 --- a/tests/Unit/Projection/Projector/MetadataProjectorResolverTest.php +++ /dev/null @@ -1,176 +0,0 @@ -resolveSubscribeMethods($projection, $message); - - self::assertEquals( - [ - $projection->handleProfileCreated(...), - ], - $result, - ); - } - - public function testResolveHandleAll(): void - { - $projection = new #[Projector('dummy')] - class { - #[Subscribe(Subscribe::ALL)] - public function handleProfileCreated(Message $message): void - { - } - }; - - $message = new Message( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('profile@test.com'), - ), - ); - - $resolver = new MetadataProjectorResolver(); - $result = $resolver->resolveSubscribeMethods($projection, $message); - - self::assertEquals( - [ - $projection->handleProfileCreated(...), - ], - $result, - ); - } - - public function testNotResolveHandleMethod(): void - { - $projection = new #[Projector('dummy')] - class { - }; - - $message = new Message( - new ProfileVisited( - ProfileId::fromString('1'), - ), - ); - - $resolver = new MetadataProjectorResolver(); - $result = $resolver->resolveSubscribeMethods($projection, $message); - - self::assertEmpty($result); - } - - public function testResolveCreateMethod(): void - { - $projection = new #[Projector('dummy')] - class { - public static bool $called = false; - - #[Setup] - public function method(): void - { - self::$called = true; - } - }; - - $resolver = new MetadataProjectorResolver(); - $result = $resolver->resolveSetupMethod($projection); - - self::assertIsCallable($result); - - $result(); - - self::assertTrue($projection::$called); - } - - public function testNotResolveCreateMethod(): void - { - $projection = new #[Projector('dummy')] - class { - }; - - $resolver = new MetadataProjectorResolver(); - $result = $resolver->resolveSetupMethod($projection); - - self::assertNull($result); - } - - public function testResolveDropMethod(): void - { - $projection = new #[Projector('dummy')] - class { - public static bool $called = false; - - #[Teardown] - public function method(): void - { - self::$called = true; - } - }; - - $resolver = new MetadataProjectorResolver(); - $result = $resolver->resolveTeardownMethod($projection); - - self::assertIsCallable($result); - - $result(); - - self::assertTrue($projection::$called); - } - - public function testNotResolveDropMethod(): void - { - $projection = new #[Projector('dummy')] - class { - }; - - $resolver = new MetadataProjectorResolver(); - $result = $resolver->resolveTeardownMethod($projection); - - self::assertNull($result); - } - - public function testProjectionId(): void - { - $projector = new #[Projector('dummy')] - class { - }; - - $resolver = new MetadataProjectorResolver(); - - self::assertEquals('dummy', $resolver->projectorId($projector)); - } -}