From 7ae2b2313e992116660c00f168025daa6a376fe3 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 22 Jul 2022 13:05:38 +0200 Subject: [PATCH 01/26] add projector and projectionist --- src/Console/Command/ProjectionRunCommand.php | 115 ++++++++++++++ .../AttributeProjectorMetadataFactory.php | 92 +++++++++++ .../Projector/DuplicateCreateMethod.php | 27 ++++ .../Projector/DuplicateDropMethod.php | 27 ++++ .../Projector/DuplicateHandleMethod.php | 29 ++++ src/Metadata/Projector/ProjectorMetadata.php | 16 ++ .../Projector/ProjectorMetadataFactory.php | 13 ++ src/Projection/DefaultProjectionist.php | 147 ++++++++++++++++++ .../MetadataAwareProjectionHandler.php | 1 + src/Projection/MetadataProjectorResolver.php | 62 ++++++++ src/Projection/Projection.php | 3 + src/Projection/ProjectionHandler.php | 1 + src/Projection/ProjectionListener.php | 3 + src/Projection/Projectionist.php | 9 ++ src/Projection/Projector/Projector.php | 17 ++ src/Projection/Projector/ProjectorId.php | 31 ++++ src/Projection/ProjectorInformation.php | 17 ++ .../ProjectorInformationCollection.php | 93 +++++++++++ .../ProjectorInformationNotFound.php | 18 +++ src/Projection/ProjectorResolver.php | 18 +++ src/Projection/ProjectorStatus.php | 13 ++ src/Projection/ProjectorStore/InMemory.php | 39 +++++ .../ProjectorStore/ProjectorData.php | 53 +++++++ .../ProjectorStore/ProjectorStore.php | 17 ++ src/Store/MultiTableStore.php | 2 +- src/Store/PipelineStore.php | 1 + src/Store/SingleTableStore.php | 2 +- src/Store/StreamableStore.php | 18 +++ .../Projectionist/Aggregate/Profile.php | 46 ++++++ .../Projectionist/Events/ProfileCreated.php | 21 +++ .../Normalizer/ProfileIdNormalizer.php | 36 +++++ tests/Integration/Projectionist/ProfileId.php | 25 +++ .../Projection/ProfileProjection.php | 72 +++++++++ .../Projectionist/ProjectionistTest.php | 78 ++++++++++ 34 files changed, 1160 insertions(+), 2 deletions(-) create mode 100644 src/Console/Command/ProjectionRunCommand.php create mode 100644 src/Metadata/Projector/AttributeProjectorMetadataFactory.php create mode 100644 src/Metadata/Projector/DuplicateCreateMethod.php create mode 100644 src/Metadata/Projector/DuplicateDropMethod.php create mode 100644 src/Metadata/Projector/DuplicateHandleMethod.php create mode 100644 src/Metadata/Projector/ProjectorMetadata.php create mode 100644 src/Metadata/Projector/ProjectorMetadataFactory.php create mode 100644 src/Projection/DefaultProjectionist.php create mode 100644 src/Projection/MetadataProjectorResolver.php create mode 100644 src/Projection/Projectionist.php create mode 100644 src/Projection/Projector/Projector.php create mode 100644 src/Projection/Projector/ProjectorId.php create mode 100644 src/Projection/ProjectorInformation.php create mode 100644 src/Projection/ProjectorInformationCollection.php create mode 100644 src/Projection/ProjectorInformationNotFound.php create mode 100644 src/Projection/ProjectorResolver.php create mode 100644 src/Projection/ProjectorStatus.php create mode 100644 src/Projection/ProjectorStore/InMemory.php create mode 100644 src/Projection/ProjectorStore/ProjectorData.php create mode 100644 src/Projection/ProjectorStore/ProjectorStore.php create mode 100644 src/Store/StreamableStore.php create mode 100644 tests/Integration/Projectionist/Aggregate/Profile.php create mode 100644 tests/Integration/Projectionist/Events/ProfileCreated.php create mode 100644 tests/Integration/Projectionist/Normalizer/ProfileIdNormalizer.php create mode 100644 tests/Integration/Projectionist/ProfileId.php create mode 100644 tests/Integration/Projectionist/Projection/ProfileProjection.php create mode 100644 tests/Integration/Projectionist/ProjectionistTest.php diff --git a/src/Console/Command/ProjectionRunCommand.php b/src/Console/Command/ProjectionRunCommand.php new file mode 100644 index 00000000..4d68756a --- /dev/null +++ b/src/Console/Command/ProjectionRunCommand.php @@ -0,0 +1,115 @@ +addOption( + 'run-limit', + null, + InputOption::VALUE_OPTIONAL, + 'The maximum number of runs this command should execute', + 1 + ) + ->addOption( + 'memory-limit', + null, + InputOption::VALUE_REQUIRED, + 'How much memory consumption should the worker be terminated' + ) + ->addOption( + 'time-limit', + null, + InputOption::VALUE_REQUIRED, + 'What is the maximum time the worker can run in seconds' + ) + ->addOption( + 'sleep', + null, + InputOption::VALUE_REQUIRED, + 'How much time should elapse before the next job is executed in microseconds', + 1000 + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $runLimit = InputHelper::nullableInt($input->getOption('run-limit')); + $memoryLimit = InputHelper::nullableString($input->getOption('memory-limit')); + $timeLimit = InputHelper::nullableInt($input->getOption('time-limit')); + $sleep = InputHelper::int($input->getOption('sleep')); + + $logger = new ConsoleLogger($output); + + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber(new StopWorkerOnSigtermSignalListener($logger)); + + if ($runLimit) { + if ($runLimit <= 0) { + throw new InvalidArgumentGiven($runLimit, 'null|positive-int'); + } + + $eventDispatcher->addSubscriber(new StopWorkerOnIterationLimitListener($runLimit, $logger)); + } + + if ($memoryLimit) { + $eventDispatcher->addSubscriber(new StopWorkerOnMemoryLimitListener(Bytes::parseFromString($memoryLimit), $logger)); + } + + if ($timeLimit) { + if ($timeLimit <= 0) { + throw new InvalidArgumentGiven($timeLimit, 'null|positive-int'); + } + + $eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($timeLimit, $logger)); + } + + $worker = new DefaultWorker( + function (): void { + $this->projectionist->run(); + }, + $eventDispatcher, + $logger + ); + + if ($sleep < 0) { + throw new InvalidArgumentGiven($sleep, '0|positive-int'); + } + + $worker->run($sleep); + + return 0; + } +} diff --git a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php b/src/Metadata/Projector/AttributeProjectorMetadataFactory.php new file mode 100644 index 00000000..6bd57a19 --- /dev/null +++ b/src/Metadata/Projector/AttributeProjectorMetadataFactory.php @@ -0,0 +1,92 @@ + */ + private array $projectorMetadata = []; + + /** + * @param class-string $projector + */ + public function metadata(string $projector): ProjectorMetadata + { + if (array_key_exists($projector, $this->projectorMetadata)) { + return $this->projectorMetadata[$projector]; + } + + $reflector = new ReflectionClass($projector); + + $methods = $reflector->getMethods(); + + $handleMethods = []; + $createMethod = null; + $dropMethod = null; + + foreach ($methods as $method) { + $attributes = $method->getAttributes(Handle::class); + + foreach ($attributes as $attribute) { + $instance = $attribute->newInstance(); + $eventClass = $instance->eventClass(); + + if (array_key_exists($eventClass, $handleMethods)) { + throw new DuplicateHandleMethod( + $projector, + $eventClass, + $handleMethods[$eventClass], + $method->getName() + ); + } + + $handleMethods[$eventClass] = $method->getName(); + } + + if ($method->getAttributes(Create::class)) { + if ($createMethod) { + throw new DuplicateCreateMethod( + $projector, + $createMethod, + $method->getName() + ); + } + + $createMethod = $method->getName(); + } + + if (!$method->getAttributes(Drop::class)) { + continue; + } + + if ($dropMethod) { + throw new DuplicateDropMethod( + $projector, + $dropMethod, + $method->getName() + ); + } + + $dropMethod = $method->getName(); + } + + $metadata = new ProjectorMetadata( + $handleMethods, + $createMethod, + $dropMethod + ); + + $this->projectorMetadata[$projector] = $metadata; + + return $metadata; + } +} diff --git a/src/Metadata/Projector/DuplicateCreateMethod.php b/src/Metadata/Projector/DuplicateCreateMethod.php new file mode 100644 index 00000000..c3761e3c --- /dev/null +++ b/src/Metadata/Projector/DuplicateCreateMethod.php @@ -0,0 +1,27 @@ + */ + public readonly array $handleMethods = [], + public readonly ?string $createMethod = null, + public readonly ?string $dropMethod = null + ) { + } +} diff --git a/src/Metadata/Projector/ProjectorMetadataFactory.php b/src/Metadata/Projector/ProjectorMetadataFactory.php new file mode 100644 index 00000000..bc819120 --- /dev/null +++ b/src/Metadata/Projector/ProjectorMetadataFactory.php @@ -0,0 +1,13 @@ + $projector + */ + public function metadata(string $projector): ProjectorMetadata; +} diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php new file mode 100644 index 00000000..e6f422fe --- /dev/null +++ b/src/Projection/DefaultProjectionist.php @@ -0,0 +1,147 @@ + $projectors + */ + public function __construct( + private readonly StreamableStore $streamableMessageStore, + private readonly ProjectorStore $positionStore, + private readonly iterable $projectors, + private readonly ProjectorResolver $resolver = new MetadataProjectorResolver() + ) { + } + + public function boot(): void + { + $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Pending); + + foreach ($informationCollection as $information) { + if (!$information->projector) { + continue; + } + + $createMethod = $this->resolver->resolveCreateMethod($information->projector); + $information->projectorData->running(); + + if (!$createMethod) { + continue; + } + + $createMethod(); + } + + $stream = $this->streamableMessageStore->stream(); + + foreach ($stream as $message) { + foreach ($informationCollection as $information) { + if (!$information->projector) { + continue; + } + + $handleMethod = $this->resolver->resolveHandleMethod($information->projector, $message); + $information->projectorData->incrementPosition(); + + if (!$handleMethod) { + continue; + } + + $handleMethod($message); + } + } + + $this->positionStore->save( + ...array_map( + static fn (ProjectorInformation $information) => $information->projectorData, + iterator_to_array($informationCollection) + ) + ); + } + + public function run(?int $limit = null): void + { + $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Running); + $position = $informationCollection->minProjectorPosition(); + $stream = $this->streamableMessageStore->stream($position); + + foreach ($stream as $message) { + $toSave = []; + + foreach ($informationCollection as $information) { + if ($information->projectorData->position() > $position) { + continue; + } + + $toSave[] = $information->projectorData; + + $this->resolver->resolveHandleMethod($information->projector, $message)($message); + $information->projectorData->incrementPosition(); + } + + $this->positionStore->save(...$toSave); + $position++; + } + } + + private function information(): ProjectorInformationCollection + { + $informationCollection = new ProjectorInformationCollection(); + $found = []; + + $projectorDataList = $this->positionStore->all(); + + foreach ($projectorDataList as $projectorData) { + $informationCollection = $informationCollection->add( + new ProjectorInformation( + $projectorData, + $this->findProjector($projectorData->id()), + ) + ); + + $found[$projectorData->id()->toString()] = true; + } + + foreach ($this->projectors as $projector) { + $projectorId = $projector->projectorId(); + + if (array_key_exists($projectorId->toString(), $found)) { + continue; + } + + $informationCollection = $informationCollection->add( + new ProjectorInformation( + new ProjectorData($projectorId), + $projector + ) + ); + } + + return $informationCollection; + } + + private function findProjector(ProjectorId $id): ?object + { + foreach ($this->projectors as $projector) { + if ($id->toString() === $projector->projectorId()->toString()) { + return $projector; + } + } + + return null; + } +} diff --git a/src/Projection/MetadataAwareProjectionHandler.php b/src/Projection/MetadataAwareProjectionHandler.php index 8d01fb02..03ce1b6c 100644 --- a/src/Projection/MetadataAwareProjectionHandler.php +++ b/src/Projection/MetadataAwareProjectionHandler.php @@ -10,6 +10,7 @@ use function array_key_exists; +/** @deprecated use DefaultProjectionist */ final class MetadataAwareProjectionHandler implements ProjectionHandler { /** @var iterable */ diff --git a/src/Projection/MetadataProjectorResolver.php b/src/Projection/MetadataProjectorResolver.php new file mode 100644 index 00000000..9a5d1a79 --- /dev/null +++ b/src/Projection/MetadataProjectorResolver.php @@ -0,0 +1,62 @@ +metadataFactory->metadata($projector::class); + $method = $metadata->createMethod; + + if (!$method) { + return null; + } + + return $projector->$method(...); + } + + public function resolveDropMethod(Projector $projector): ?Closure + { + $metadata = $this->metadataFactory->metadata($projector::class); + $method = $metadata->dropMethod; + + if (!$method) { + return null; + } + + return $projector->$method(...); + } + + /** + * @return (Closure(Message):void)|null + */ + public function resolveHandleMethod(Projector $projector, Message $message): ?Closure + { + $event = $message->event(); + $metadata = $this->metadataFactory->metadata($projector::class); + + if (!array_key_exists($event::class, $metadata->handleMethods)) { + return null; + } + + $handleMethod = $metadata->handleMethods[$event::class]; + + return $projector->$handleMethod(...); + } +} diff --git a/src/Projection/Projection.php b/src/Projection/Projection.php index 6f8a9890..bdbfbde5 100644 --- a/src/Projection/Projection.php +++ b/src/Projection/Projection.php @@ -4,6 +4,9 @@ namespace Patchlevel\EventSourcing\Projection; +/** + * @deprecated use Projector interface + */ interface Projection { } diff --git a/src/Projection/ProjectionHandler.php b/src/Projection/ProjectionHandler.php index 5f58f94d..74c2bc1f 100644 --- a/src/Projection/ProjectionHandler.php +++ b/src/Projection/ProjectionHandler.php @@ -6,6 +6,7 @@ use Patchlevel\EventSourcing\EventBus\Message; +/** @deprecated use Projectionist */ interface ProjectionHandler { public function handle(Message $message): void; diff --git a/src/Projection/ProjectionListener.php b/src/Projection/ProjectionListener.php index c3c49be0..133e3758 100644 --- a/src/Projection/ProjectionListener.php +++ b/src/Projection/ProjectionListener.php @@ -7,6 +7,9 @@ use Patchlevel\EventSourcing\EventBus\Listener; use Patchlevel\EventSourcing\EventBus\Message; +/** + * @deprecated + */ final class ProjectionListener implements Listener { private ProjectionHandler $projectionHandler; diff --git a/src/Projection/Projectionist.php b/src/Projection/Projectionist.php new file mode 100644 index 00000000..791c322b --- /dev/null +++ b/src/Projection/Projectionist.php @@ -0,0 +1,9 @@ +name(), $this->version()); + } + + abstract public function name(): string; + + abstract public function version(): int; +} diff --git a/src/Projection/Projector/ProjectorId.php b/src/Projection/Projector/ProjectorId.php new file mode 100644 index 00000000..186c623c --- /dev/null +++ b/src/Projection/Projector/ProjectorId.php @@ -0,0 +1,31 @@ +name, $this->version); + } + + public function name(): string + { + return $this->name; + } + + public function version(): int + { + return $this->version; + } +} diff --git a/src/Projection/ProjectorInformation.php b/src/Projection/ProjectorInformation.php new file mode 100644 index 00000000..992c0fa7 --- /dev/null +++ b/src/Projection/ProjectorInformation.php @@ -0,0 +1,17 @@ + + */ +final class ProjectorInformationCollection implements Countable, IteratorAggregate +{ + /** @var array */ + private readonly array $projectorInformation; + + /** + * @param list $projectorInformationList + */ + public function __construct(array $projectorInformationList = []) + { + $result = []; + + foreach ($projectorInformationList as $projectorInformation) { + $result[$projectorInformation->projectorData->id()->toString()] = $projectorInformation; + } + + $this->projectorInformation = $result; + } + + public function get(ProjectorId $projectorId): ProjectorInformation + { + if (!array_key_exists($projectorId->toString(), $this->projectorInformation)) { + throw new ProjectorInformationNotFound($projectorId); + } + + return $this->projectorInformation[$projectorId->toString()]; + } + + public function add(ProjectorInformation $information): self + { + return new self( + [ + ...array_values($this->projectorInformation), + $information, + ] + ); + } + + public function minProjectorPosition(): int + { + $min = 0; + + foreach ($this->projectorInformation as $projectorInformation) { + if ($projectorInformation->projectorData->position() >= $min) { + continue; + } + + $min = $projectorInformation->projectorData->position(); + } + + return $min; + } + + public function filterByProjectorStatus(ProjectorStatus $status): self + { + $projectors = array_filter( + $this->projectorInformation, + static fn (ProjectorInformation $information) => $information->projectorData->status() === $status + ); + + return new self(array_values($projectors)); + } + + public function count(): int + { + return count($this->projectorInformation); + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->projectorInformation); + } +} diff --git a/src/Projection/ProjectorInformationNotFound.php b/src/Projection/ProjectorInformationNotFound.php new file mode 100644 index 00000000..c539e3a9 --- /dev/null +++ b/src/Projection/ProjectorInformationNotFound.php @@ -0,0 +1,18 @@ +toString())); + } +} diff --git a/src/Projection/ProjectorResolver.php b/src/Projection/ProjectorResolver.php new file mode 100644 index 00000000..4a70ac87 --- /dev/null +++ b/src/Projection/ProjectorResolver.php @@ -0,0 +1,18 @@ + */ + private array $store = []; + + public function get(ProjectorId $projectorId): ProjectorData + { + if (array_key_exists($projectorId->toString(), $this->store)) { + return $this->store[$projectorId->toString()]; + } + + throw new RuntimeException(); // todo + } + + /** @return list */ + public function all(): array + { + return array_values($this->store); + } + + public function save(ProjectorData ...$data): void + { + foreach ($data as $item) { + $this->store[$item->id()->toString()] = $item; + } + } +} diff --git a/src/Projection/ProjectorStore/ProjectorData.php b/src/Projection/ProjectorStore/ProjectorData.php new file mode 100644 index 00000000..4c907895 --- /dev/null +++ b/src/Projection/ProjectorStore/ProjectorData.php @@ -0,0 +1,53 @@ +id; + } + + public function status(): ProjectorStatus + { + return $this->status; + } + + public function position(): int + { + return $this->position; + } + + public function incrementPosition(): void + { + $this->position++; + } + + public function error(): void + { + $this->status = ProjectorStatus::Error; + } + + public function stale(): void + { + $this->status = ProjectorStatus::Stale; + } + + public function running(): void + { + $this->status = ProjectorStatus::Running; + } +} diff --git a/src/Projection/ProjectorStore/ProjectorStore.php b/src/Projection/ProjectorStore/ProjectorStore.php new file mode 100644 index 00000000..e560b9eb --- /dev/null +++ b/src/Projection/ProjectorStore/ProjectorStore.php @@ -0,0 +1,17 @@ + */ + public function all(): array; + + public function save(ProjectorData ...$positions): void; +} diff --git a/src/Store/MultiTableStore.php b/src/Store/MultiTableStore.php index 71a9fca0..f44a9641 100644 --- a/src/Store/MultiTableStore.php +++ b/src/Store/MultiTableStore.php @@ -21,7 +21,7 @@ use function is_string; use function sprintf; -final class MultiTableStore extends DoctrineStore implements PipelineStore, SchemaConfigurator +final class MultiTableStore extends DoctrineStore implements StreamableStore, SchemaConfigurator { private string $metadataTableName; diff --git a/src/Store/PipelineStore.php b/src/Store/PipelineStore.php index 345bbe1a..c2f9c95e 100644 --- a/src/Store/PipelineStore.php +++ b/src/Store/PipelineStore.php @@ -7,6 +7,7 @@ use Generator; use Patchlevel\EventSourcing\EventBus\Message; +/** @deprecated use StreamableStore */ interface PipelineStore extends Store { /** diff --git a/src/Store/SingleTableStore.php b/src/Store/SingleTableStore.php index af30da89..ffb2d0a3 100644 --- a/src/Store/SingleTableStore.php +++ b/src/Store/SingleTableStore.php @@ -20,7 +20,7 @@ use function is_string; use function sprintf; -final class SingleTableStore extends DoctrineStore implements PipelineStore, SchemaConfigurator +final class SingleTableStore extends DoctrineStore implements StreamableStore, SchemaConfigurator { private string $storeTableName; diff --git a/src/Store/StreamableStore.php b/src/Store/StreamableStore.php new file mode 100644 index 00000000..fa096cb5 --- /dev/null +++ b/src/Store/StreamableStore.php @@ -0,0 +1,18 @@ + + */ + public function stream(int $fromIndex = 0): Generator; + + public function count(int $fromIndex = 0): int; +} diff --git a/tests/Integration/Projectionist/Aggregate/Profile.php b/tests/Integration/Projectionist/Aggregate/Profile.php new file mode 100644 index 00000000..07a0dd93 --- /dev/null +++ b/tests/Integration/Projectionist/Aggregate/Profile.php @@ -0,0 +1,46 @@ +id->toString(); + } + + public static function create(ProfileId $id, string $name): self + { + $self = new self(); + $self->recordThat(new ProfileCreated($id, $name)); + + return $self; + } + + #[Apply(ProfileCreated::class)] + protected function applyProfileCreated(ProfileCreated $event): void + { + $this->id = $event->profileId; + $this->name = $event->name; + } + + public function name(): string + { + return $this->name; + } +} diff --git a/tests/Integration/Projectionist/Events/ProfileCreated.php b/tests/Integration/Projectionist/Events/ProfileCreated.php new file mode 100644 index 00000000..beb9df8a --- /dev/null +++ b/tests/Integration/Projectionist/Events/ProfileCreated.php @@ -0,0 +1,21 @@ +toString(); + } + + public function denormalize(mixed $value): ?ProfileId + { + if ($value === null) { + return null; + } + + if (!is_string($value)) { + throw new InvalidArgumentException(); + } + + return ProfileId::fromString($value); + } +} diff --git a/tests/Integration/Projectionist/ProfileId.php b/tests/Integration/Projectionist/ProfileId.php new file mode 100644 index 00000000..65c33fd2 --- /dev/null +++ b/tests/Integration/Projectionist/ProfileId.php @@ -0,0 +1,25 @@ +id = $id; + } + + public static function fromString(string $id): self + { + return new self($id); + } + + public function toString(): string + { + return $this->id; + } +} diff --git a/tests/Integration/Projectionist/Projection/ProfileProjection.php b/tests/Integration/Projectionist/Projection/ProfileProjection.php new file mode 100644 index 00000000..9129c65e --- /dev/null +++ b/tests/Integration/Projectionist/Projection/ProfileProjection.php @@ -0,0 +1,72 @@ +connection = $connection; + } + + #[Create] + public function create(): void + { + $table = new Table($this->tableName()); + $table->addColumn('id', 'string'); + $table->addColumn('name', 'string'); + $table->setPrimaryKey(['id']); + + $this->connection->createSchemaManager()->createTable($table); + } + + #[Drop] + public function drop(): void + { + $this->connection->createSchemaManager()->dropTable($this->tableName()); + } + + #[Handle(ProfileCreated::class)] + public function handleProfileCreated(Message $message): void + { + $profileCreated = $message->event(); + + $this->connection->executeStatement( + 'INSERT INTO ' . $this->tableName() . ' (id, name) VALUES(:id, :name);', + [ + 'id' => $profileCreated->profileId->toString(), + 'name' => $profileCreated->name, + ] + ); + } + + private function tableName(): string + { + return sprintf('projection_%s_%s', $this->name(), $this->version()); + } + + public function version(): int + { + return 1; + } + + public function name(): string + { + return 'profile'; + } +} diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php new file mode 100644 index 00000000..25aa0206 --- /dev/null +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -0,0 +1,78 @@ +connection = DbalManager::createConnection(); + } + + public function tearDown(): void + { + $this->connection->close(); + } + + public function testSuccessful(): void + { + $store = new SingleTableStore( + $this->connection, + DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']), + (new AttributeAggregateRootRegistryFactory())->create([__DIR__ . '/Aggregate']), + 'eventstore' + ); + + $manager = new DefaultRepositoryManager( + new AggregateRootRegistry(['profile' => Profile::class]), + $store, + new DefaultEventBus(), + ); + + $repository = $manager->get(Profile::class); + + // create tables + (new DoctrineSchemaManager())->create($store); + + $profile = Profile::create(ProfileId::fromString('1'), 'John'); + $repository->save($profile); + + $projectionist = new DefaultProjectionist( + $store, + new InMemory(), + [new ProfileProjection($this->connection)], + ); + + $projectionist->boot(); + $projectionist->run(); + + $result = $this->connection->fetchAssociative('SELECT * FROM projection_profile_1 WHERE id = ?', ['1']); + + self::assertIsArray($result); + self::assertArrayHasKey('id', $result); + self::assertSame('1', $result['id']); + self::assertSame('John', $result['name']); + } +} From 5bd01b1affb60fae8a4f21f1669b9dc65ee6e712 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 17 Sep 2022 12:55:02 +0200 Subject: [PATCH 02/26] improve projectionist implementation --- src/Projection/DefaultProjectionist.php | 93 ++++++++++------ src/Projection/MetadataProjectorResolver.php | 1 - src/Projection/Projectionist.php | 7 ++ src/Projection/Projector.php | 10 ++ src/Projection/Projector/Projector.php | 17 --- .../{Projector => }/ProjectorId.php | 2 +- src/Projection/ProjectorInformation.php | 5 +- .../ProjectorInformationCollection.php | 16 +-- .../ProjectorInformationNotFound.php | 1 - src/Projection/ProjectorResolver.php | 1 - src/Projection/ProjectorStatus.php | 6 +- src/Projection/ProjectorStore/InMemory.php | 17 +-- .../{ProjectorData.php => ProjectorState.php} | 14 +-- .../ProjectorStore/ProjectorStateNotFound.php | 11 ++ .../ProjectorStore/ProjectorStore.php | 12 ++- src/Store/DoctrineStore.php | 101 +++++++++++++++++- src/Store/MultiTableStore.php | 1 + src/Store/SingleTableStore.php | 1 + .../Projection/ProfileProjection.php | 20 ++-- .../Projectionist/ProjectionistTest.php | 2 + 20 files changed, 245 insertions(+), 93 deletions(-) create mode 100644 src/Projection/Projector.php delete mode 100644 src/Projection/Projector/Projector.php rename src/Projection/{Projector => }/ProjectorId.php (89%) rename src/Projection/ProjectorStore/{ProjectorData.php => ProjectorState.php} (69%) create mode 100644 src/Projection/ProjectorStore/ProjectorStateNotFound.php diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index e6f422fe..85c5f47a 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -4,13 +4,10 @@ namespace Patchlevel\EventSourcing\Projection; -use Patchlevel\EventSourcing\Projection\Projector\Projector; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorId; -use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorData; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStore; use Patchlevel\EventSourcing\Store\StreamableStore; -use function array_key_exists; use function array_map; use function iterator_to_array; @@ -21,7 +18,7 @@ final class DefaultProjectionist implements Projectionist */ public function __construct( private readonly StreamableStore $streamableMessageStore, - private readonly ProjectorStore $positionStore, + private readonly ProjectorStore $projectorStore, private readonly iterable $projectors, private readonly ProjectorResolver $resolver = new MetadataProjectorResolver() ) { @@ -29,15 +26,15 @@ public function __construct( public function boot(): void { - $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Pending); + $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Booting); foreach ($informationCollection as $information) { if (!$information->projector) { - continue; + continue; // throw an exception } $createMethod = $this->resolver->resolveCreateMethod($information->projector); - $information->projectorData->running(); + $information->projectorState->active(); if (!$createMethod) { continue; @@ -51,11 +48,11 @@ public function boot(): void foreach ($stream as $message) { foreach ($informationCollection as $information) { if (!$information->projector) { - continue; + continue; // throw an exception } $handleMethod = $this->resolver->resolveHandleMethod($information->projector, $message); - $information->projectorData->incrementPosition(); + $information->projectorState->incrementPosition(); if (!$handleMethod) { continue; @@ -65,9 +62,9 @@ public function boot(): void } } - $this->positionStore->save( + $this->projectorStore->saveProjectorState( ...array_map( - static fn (ProjectorInformation $information) => $information->projectorData, + static fn (ProjectorInformation $information) => $information->projectorState, iterator_to_array($informationCollection) ) ); @@ -75,7 +72,7 @@ public function boot(): void public function run(?int $limit = null): void { - $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Running); + $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Active); $position = $informationCollection->minProjectorPosition(); $stream = $this->streamableMessageStore->stream($position); @@ -83,49 +80,85 @@ public function run(?int $limit = null): void $toSave = []; foreach ($informationCollection as $information) { - if ($information->projectorData->position() > $position) { + if ($information->projectorState->position() > $position) { continue; } - $toSave[] = $information->projectorData; + $toSave[] = $information->projectorState; + + $handleMethod = $this->resolver->resolveHandleMethod($information->projector, $message); + $handleMethod($message); - $this->resolver->resolveHandleMethod($information->projector, $message)($message); - $information->projectorData->incrementPosition(); + $information->projectorState->incrementPosition(); } - $this->positionStore->save(...$toSave); + $this->projectorStore->saveProjectorState(...$toSave); $position++; } } + public function teardown(): void + { + $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Outdated); + + foreach ($informationCollection as $information) { + if (!$information->projector) { + continue; // hmm........................ + } + + $dropMethod = $this->resolver->resolveDropMethod($information->projector); + + if (!$dropMethod) { + continue; + } + + $dropMethod(); + + $this->projectorStore->removeProjectorState($information->projectorState->id()); + } + } + + public function destroy(): void + { + $informationCollection = $this->information(); + + foreach ($informationCollection as $information) { + if ($information->projector) { + $dropMethod = $this->resolver->resolveDropMethod($information->projector); + + if (!$dropMethod) { + continue; + } + + $dropMethod(); + } + + $this->projectorStore->removeProjectorState($information->projectorState->id()); + } + } + private function information(): ProjectorInformationCollection { $informationCollection = new ProjectorInformationCollection(); - $found = []; - - $projectorDataList = $this->positionStore->all(); + $projectorsStates = $this->projectorStore->getStateFromAllProjectors(); - foreach ($projectorDataList as $projectorData) { + foreach ($projectorsStates as $projectorState) { $informationCollection = $informationCollection->add( new ProjectorInformation( - $projectorData, - $this->findProjector($projectorData->id()), + $projectorState, + $this->findProjector($projectorState->id()), ) ); - - $found[$projectorData->id()->toString()] = true; } foreach ($this->projectors as $projector) { - $projectorId = $projector->projectorId(); - - if (array_key_exists($projectorId->toString(), $found)) { + if ($informationCollection->has($projector->projectorId())) { continue; } $informationCollection = $informationCollection->add( new ProjectorInformation( - new ProjectorData($projectorId), + new ProjectorState($projector->projectorId()), $projector ) ); diff --git a/src/Projection/MetadataProjectorResolver.php b/src/Projection/MetadataProjectorResolver.php index 9a5d1a79..ccdce103 100644 --- a/src/Projection/MetadataProjectorResolver.php +++ b/src/Projection/MetadataProjectorResolver.php @@ -8,7 +8,6 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory; use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadataFactory; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use function array_key_exists; diff --git a/src/Projection/Projectionist.php b/src/Projection/Projectionist.php index 791c322b..51176495 100644 --- a/src/Projection/Projectionist.php +++ b/src/Projection/Projectionist.php @@ -6,4 +6,11 @@ interface Projectionist { + public function boot(): void; + + public function run(?int $limit = null): void; + + public function teardown(): void; + + public function destroy(): void; } diff --git a/src/Projection/Projector.php b/src/Projection/Projector.php new file mode 100644 index 00000000..75af8796 --- /dev/null +++ b/src/Projection/Projector.php @@ -0,0 +1,10 @@ +name(), $this->version()); - } - - abstract public function name(): string; - - abstract public function version(): int; -} diff --git a/src/Projection/Projector/ProjectorId.php b/src/Projection/ProjectorId.php similarity index 89% rename from src/Projection/Projector/ProjectorId.php rename to src/Projection/ProjectorId.php index 186c623c..354b8bc4 100644 --- a/src/Projection/Projector/ProjectorId.php +++ b/src/Projection/ProjectorId.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Projection\Projector; +namespace Patchlevel\EventSourcing\Projection; use function sprintf; diff --git a/src/Projection/ProjectorInformation.php b/src/Projection/ProjectorInformation.php index 992c0fa7..507ec02c 100644 --- a/src/Projection/ProjectorInformation.php +++ b/src/Projection/ProjectorInformation.php @@ -4,13 +4,12 @@ namespace Patchlevel\EventSourcing\Projection; -use Patchlevel\EventSourcing\Projection\Projector\Projector; -use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorData; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; final class ProjectorInformation { public function __construct( - public readonly ProjectorData $projectorData, + public readonly ProjectorState $projectorState, public readonly ?Projector $projector = null ) { } diff --git a/src/Projection/ProjectorInformationCollection.php b/src/Projection/ProjectorInformationCollection.php index 65f81bca..e7ed667a 100644 --- a/src/Projection/ProjectorInformationCollection.php +++ b/src/Projection/ProjectorInformationCollection.php @@ -7,7 +7,6 @@ use ArrayIterator; use Countable; use IteratorAggregate; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorId; use Traversable; use function array_filter; @@ -31,7 +30,7 @@ public function __construct(array $projectorInformationList = []) $result = []; foreach ($projectorInformationList as $projectorInformation) { - $result[$projectorInformation->projectorData->id()->toString()] = $projectorInformation; + $result[$projectorInformation->projectorState->id()->toString()] = $projectorInformation; } $this->projectorInformation = $result; @@ -39,13 +38,18 @@ public function __construct(array $projectorInformationList = []) public function get(ProjectorId $projectorId): ProjectorInformation { - if (!array_key_exists($projectorId->toString(), $this->projectorInformation)) { + if (!$this->has($projectorId)) { throw new ProjectorInformationNotFound($projectorId); } return $this->projectorInformation[$projectorId->toString()]; } + public function has(ProjectorId $projectorId): bool + { + return array_key_exists($projectorId->toString(), $this->projectorInformation); + } + public function add(ProjectorInformation $information): self { return new self( @@ -61,11 +65,11 @@ public function minProjectorPosition(): int $min = 0; foreach ($this->projectorInformation as $projectorInformation) { - if ($projectorInformation->projectorData->position() >= $min) { + if ($projectorInformation->projectorState->position() >= $min) { continue; } - $min = $projectorInformation->projectorData->position(); + $min = $projectorInformation->projectorState->position(); } return $min; @@ -75,7 +79,7 @@ public function filterByProjectorStatus(ProjectorStatus $status): self { $projectors = array_filter( $this->projectorInformation, - static fn (ProjectorInformation $information) => $information->projectorData->status() === $status + static fn (ProjectorInformation $information) => $information->projectorState->status() === $status ); return new self(array_values($projectors)); diff --git a/src/Projection/ProjectorInformationNotFound.php b/src/Projection/ProjectorInformationNotFound.php index c539e3a9..3a7d8114 100644 --- a/src/Projection/ProjectorInformationNotFound.php +++ b/src/Projection/ProjectorInformationNotFound.php @@ -4,7 +4,6 @@ namespace Patchlevel\EventSourcing\Projection; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorId; use RuntimeException; use function sprintf; diff --git a/src/Projection/ProjectorResolver.php b/src/Projection/ProjectorResolver.php index 4a70ac87..e3ca06e0 100644 --- a/src/Projection/ProjectorResolver.php +++ b/src/Projection/ProjectorResolver.php @@ -6,7 +6,6 @@ use Closure; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\Projector; interface ProjectorResolver { diff --git a/src/Projection/ProjectorStatus.php b/src/Projection/ProjectorStatus.php index ade694e0..11746ac2 100644 --- a/src/Projection/ProjectorStatus.php +++ b/src/Projection/ProjectorStatus.php @@ -6,8 +6,8 @@ enum ProjectorStatus: string { - case Pending = 'pending'; - case Running = 'running'; - case Stale = 'stale'; + case Booting = 'booting'; + case Active = 'active'; + case Outdated = 'outdated'; case Error = 'error'; } diff --git a/src/Projection/ProjectorStore/InMemory.php b/src/Projection/ProjectorStore/InMemory.php index 69d29c7f..43ebde77 100644 --- a/src/Projection/ProjectorStore/InMemory.php +++ b/src/Projection/ProjectorStore/InMemory.php @@ -4,7 +4,7 @@ namespace Patchlevel\EventSourcing\Projection\ProjectorStore; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorId; +use Patchlevel\EventSourcing\Projection\ProjectorId; use RuntimeException; use function array_key_exists; @@ -12,10 +12,10 @@ class InMemory implements ProjectorStore { - /** @var array */ + /** @var array */ private array $store = []; - public function get(ProjectorId $projectorId): ProjectorData + public function getProjectorState(ProjectorId $projectorId): ProjectorState { if (array_key_exists($projectorId->toString(), $this->store)) { return $this->store[$projectorId->toString()]; @@ -24,16 +24,21 @@ public function get(ProjectorId $projectorId): ProjectorData throw new RuntimeException(); // todo } - /** @return list */ - public function all(): array + /** @return list */ + public function getStateFromAllProjectors(): array { return array_values($this->store); } - public function save(ProjectorData ...$data): void + public function saveProjectorState(ProjectorState ...$data): void { foreach ($data as $item) { $this->store[$item->id()->toString()] = $item; } } + + public function removeProjectorState(ProjectorId $projectorId): void + { + unset($this->store[$projectorId->toString()]); + } } diff --git a/src/Projection/ProjectorStore/ProjectorData.php b/src/Projection/ProjectorStore/ProjectorState.php similarity index 69% rename from src/Projection/ProjectorStore/ProjectorData.php rename to src/Projection/ProjectorStore/ProjectorState.php index 4c907895..80be89df 100644 --- a/src/Projection/ProjectorStore/ProjectorData.php +++ b/src/Projection/ProjectorStore/ProjectorState.php @@ -4,14 +4,14 @@ namespace Patchlevel\EventSourcing\Projection\ProjectorStore; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorId; +use Patchlevel\EventSourcing\Projection\ProjectorId; use Patchlevel\EventSourcing\Projection\ProjectorStatus; -final class ProjectorData +final class ProjectorState { public function __construct( private readonly ProjectorId $id, - private ProjectorStatus $status = ProjectorStatus::Pending, + private ProjectorStatus $status = ProjectorStatus::Booting, private int $position = 0 ) { } @@ -41,13 +41,13 @@ public function error(): void $this->status = ProjectorStatus::Error; } - public function stale(): void + public function outdated(): void { - $this->status = ProjectorStatus::Stale; + $this->status = ProjectorStatus::Outdated; } - public function running(): void + public function active(): void { - $this->status = ProjectorStatus::Running; + $this->status = ProjectorStatus::Active; } } diff --git a/src/Projection/ProjectorStore/ProjectorStateNotFound.php b/src/Projection/ProjectorStore/ProjectorStateNotFound.php new file mode 100644 index 00000000..60605a09 --- /dev/null +++ b/src/Projection/ProjectorStore/ProjectorStateNotFound.php @@ -0,0 +1,11 @@ + */ - public function all(): array; + /** @return list */ + public function getStateFromAllProjectors(): array; - public function save(ProjectorData ...$positions): void; + public function saveProjectorState(ProjectorState ...$projectorStates): void; + + public function removeProjectorState(ProjectorId $projectorId): void; } diff --git a/src/Store/DoctrineStore.php b/src/Store/DoctrineStore.php index 443c25b0..b9bbb2dc 100644 --- a/src/Store/DoctrineStore.php +++ b/src/Store/DoctrineStore.php @@ -14,6 +14,11 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Schema\SchemaConfigurator; +use Patchlevel\EventSourcing\Projection\ProjectorId; +use Patchlevel\EventSourcing\Projection\ProjectorStatus; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateNotFound; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStore; use Patchlevel\EventSourcing\Serializer\EventSerializer; use Patchlevel\EventSourcing\Serializer\SerializedEvent; @@ -22,9 +27,10 @@ use function is_int; use function is_string; -abstract class DoctrineStore implements Store, TransactionStore, OutboxStore, SplitEventstreamStore +abstract class DoctrineStore implements Store, TransactionStore, OutboxStore, ProjectorStore, SplitEventstreamStore { private const OUTBOX_TABLE = 'outbox'; + private const PROJECTOR_TABLE = 'projector'; public function __construct( protected Connection $connection, @@ -171,6 +177,81 @@ public function schema(): Schema return $schema; } + public function getProjectorState(ProjectorId $projectorId): ProjectorState + { + $sql = $this->connection->createQueryBuilder() + ->select('*') + ->from(self::PROJECTOR_TABLE) + ->where('projector = :projector AND version = :version') + ->setParameters([ + 'projector' => $projectorId->name(), + 'version' => $projectorId->version(), + ]) + ->getSQL(); + + /** @var array{projector: string, version: int, position: int, status: string}|false $result */ + $result = $this->connection->fetchOne($sql); + + if ($result === false) { + throw new ProjectorStateNotFound(); + } + + return new ProjectorState( + new ProjectorId($result['projector'], $result['version']), + ProjectorStatus::from($result['status']), + $result['position'] + ); + } + + public function getStateFromAllProjectors(): array + { + $sql = $this->connection->createQueryBuilder() + ->select('*') + ->from(self::PROJECTOR_TABLE) + ->getSQL(); + + /** @var list $result */ + $result = $this->connection->fetchAllAssociative($sql); + + return array_map( + static function (array $data) { + return new ProjectorState( + new ProjectorId($data['projector'], $data['version']), + ProjectorStatus::from($data['status']), + $data['position'] + ); + }, + $result + ); + } + + public function saveProjectorState(ProjectorState ...$projectorStates): void + { + $this->connection->transactional( + static function (Connection $connection) use ($projectorStates): void { + foreach ($projectorStates as $projectorState) { + $connection->insert( + self::PROJECTOR_TABLE, + [ + 'projector' => $projectorState->id()->name(), + 'version' => $projectorState->id()->version(), + 'position' => $projectorState->position(), + 'status' => $projectorState->status(), + ] + ); + } + } + ); + } + + public function removeProjectorState(ProjectorId $projectorId): void + { + $this->connection->delete(self::PROJECTOR_TABLE, [ + 'projector' => $projectorId->name(), + 'version' => $projectorId->version(), + ]); + } + protected static function normalizeRecordedOn(string $recordedOn, AbstractPlatform $platform): DateTimeImmutable { $normalizedRecordedOn = Type::getType(Types::DATETIMETZ_IMMUTABLE)->convertToPHPValue($recordedOn, $platform); @@ -212,7 +293,7 @@ protected static function normalizeCustomHeaders(string $customHeaders, Abstract protected function addOutboxSchema(Schema $schema): void { - $table = $schema->createTable('outbox'); + $table = $schema->createTable(self::OUTBOX_TABLE); $table->addColumn('aggregate', Types::STRING) ->setNotnull(true); @@ -231,4 +312,20 @@ protected function addOutboxSchema(Schema $schema): void $table->setPrimaryKey(['aggregate', 'aggregate_id', 'playhead']); } + + protected function addProjectorSchema(Schema $schema): void + { + $table = $schema->createTable(self::PROJECTOR_TABLE); + + $table->addColumn('projector', Types::STRING) + ->setNotnull(true); + $table->addColumn('version', Types::INTEGER) + ->setNotnull(true); + $table->addColumn('position', Types::INTEGER) + ->setNotnull(true); + $table->addColumn('status', Types::STRING) + ->setNotnull(true); + + $table->setPrimaryKey(['projector', 'version']); + } } diff --git a/src/Store/MultiTableStore.php b/src/Store/MultiTableStore.php index f44a9641..051b7103 100644 --- a/src/Store/MultiTableStore.php +++ b/src/Store/MultiTableStore.php @@ -268,6 +268,7 @@ public function configureSchema(Schema $schema, Connection $connection): void } $this->addOutboxSchema($schema); + $this->addProjectorSchema($schema); } private function addMetaTableToSchema(Schema $schema): void diff --git a/src/Store/SingleTableStore.php b/src/Store/SingleTableStore.php index ffb2d0a3..a63f8cfb 100644 --- a/src/Store/SingleTableStore.php +++ b/src/Store/SingleTableStore.php @@ -260,5 +260,6 @@ public function configureSchema(Schema $schema, Connection $connection): void $table->addIndex(['aggregate', 'aggregate_id', 'playhead', 'archived']); $this->addOutboxSchema($schema); + $this->addProjectorSchema($schema); } } diff --git a/tests/Integration/Projectionist/Projection/ProfileProjection.php b/tests/Integration/Projectionist/Projection/ProfileProjection.php index 9129c65e..340afd49 100644 --- a/tests/Integration/Projectionist/Projection/ProfileProjection.php +++ b/tests/Integration/Projectionist/Projection/ProfileProjection.php @@ -10,12 +10,13 @@ use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Handle; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\Projector; +use Patchlevel\EventSourcing\Projection\Projector; +use Patchlevel\EventSourcing\Projection\ProjectorId; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; use function sprintf; -final class ProfileProjection extends Projector +final class ProfileProjection implements Projector { private Connection $connection; @@ -57,16 +58,15 @@ public function handleProfileCreated(Message $message): void private function tableName(): string { - return sprintf('projection_%s_%s', $this->name(), $this->version()); - } - - public function version(): int - { - return 1; + return sprintf( + 'projection_%s_%s', + $this->projectorId()->name(), + $this->projectorId()->version() + ); } - public function name(): string + public function projectorId(): ProjectorId { - return 'profile'; + return new ProjectorId('profile', 1); } } diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index 25aa0206..6f0e1f3a 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -74,5 +74,7 @@ public function testSuccessful(): void self::assertArrayHasKey('id', $result); self::assertSame('1', $result['id']); self::assertSame('John', $result['name']); + + $projectionist->destroy(); } } From e1ed1db2a89cbe4801270bbe1943b798428bb717 Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 20 Sep 2022 13:19:18 +0200 Subject: [PATCH 03/26] revert unnecessary changes and fix phpstan --- .../AttributeProjectorMetadataFactory.php | 92 ------------------- .../Projector/DuplicateCreateMethod.php | 27 ------ .../Projector/DuplicateDropMethod.php | 27 ------ .../Projector/DuplicateHandleMethod.php | 29 ------ src/Metadata/Projector/ProjectorMetadata.php | 16 ---- .../Projector/ProjectorMetadataFactory.php | 13 --- src/Projection/DefaultProjectionist.php | 11 ++- .../MetadataAwareProjectionHandler.php | 36 +++----- src/Projection/MetadataProjectorResolver.php | 12 +-- src/Projection/ProjectionHandler.php | 1 - src/Projection/ProjectionListener.php | 3 - src/Projection/Projector.php | 2 +- src/Projection/ProjectorHandler.php | 16 ++++ src/Projection/ProjectorResolver.php | 6 +- src/Projection/ProjectorStore/InMemory.php | 8 +- .../ProjectorStore/ProjectorStateNotFound.php | 2 +- 16 files changed, 55 insertions(+), 246 deletions(-) delete mode 100644 src/Metadata/Projector/AttributeProjectorMetadataFactory.php delete mode 100644 src/Metadata/Projector/DuplicateCreateMethod.php delete mode 100644 src/Metadata/Projector/DuplicateDropMethod.php delete mode 100644 src/Metadata/Projector/DuplicateHandleMethod.php delete mode 100644 src/Metadata/Projector/ProjectorMetadata.php delete mode 100644 src/Metadata/Projector/ProjectorMetadataFactory.php create mode 100644 src/Projection/ProjectorHandler.php diff --git a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php b/src/Metadata/Projector/AttributeProjectorMetadataFactory.php deleted file mode 100644 index 6bd57a19..00000000 --- a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php +++ /dev/null @@ -1,92 +0,0 @@ - */ - private array $projectorMetadata = []; - - /** - * @param class-string $projector - */ - public function metadata(string $projector): ProjectorMetadata - { - if (array_key_exists($projector, $this->projectorMetadata)) { - return $this->projectorMetadata[$projector]; - } - - $reflector = new ReflectionClass($projector); - - $methods = $reflector->getMethods(); - - $handleMethods = []; - $createMethod = null; - $dropMethod = null; - - foreach ($methods as $method) { - $attributes = $method->getAttributes(Handle::class); - - foreach ($attributes as $attribute) { - $instance = $attribute->newInstance(); - $eventClass = $instance->eventClass(); - - if (array_key_exists($eventClass, $handleMethods)) { - throw new DuplicateHandleMethod( - $projector, - $eventClass, - $handleMethods[$eventClass], - $method->getName() - ); - } - - $handleMethods[$eventClass] = $method->getName(); - } - - if ($method->getAttributes(Create::class)) { - if ($createMethod) { - throw new DuplicateCreateMethod( - $projector, - $createMethod, - $method->getName() - ); - } - - $createMethod = $method->getName(); - } - - if (!$method->getAttributes(Drop::class)) { - continue; - } - - if ($dropMethod) { - throw new DuplicateDropMethod( - $projector, - $dropMethod, - $method->getName() - ); - } - - $dropMethod = $method->getName(); - } - - $metadata = new ProjectorMetadata( - $handleMethods, - $createMethod, - $dropMethod - ); - - $this->projectorMetadata[$projector] = $metadata; - - return $metadata; - } -} diff --git a/src/Metadata/Projector/DuplicateCreateMethod.php b/src/Metadata/Projector/DuplicateCreateMethod.php deleted file mode 100644 index c3761e3c..00000000 --- a/src/Metadata/Projector/DuplicateCreateMethod.php +++ /dev/null @@ -1,27 +0,0 @@ - */ - public readonly array $handleMethods = [], - public readonly ?string $createMethod = null, - public readonly ?string $dropMethod = null - ) { - } -} diff --git a/src/Metadata/Projector/ProjectorMetadataFactory.php b/src/Metadata/Projector/ProjectorMetadataFactory.php deleted file mode 100644 index bc819120..00000000 --- a/src/Metadata/Projector/ProjectorMetadataFactory.php +++ /dev/null @@ -1,13 +0,0 @@ - $projector - */ - public function metadata(string $projector): ProjectorMetadata; -} diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 85c5f47a..df1e404c 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -80,6 +80,10 @@ public function run(?int $limit = null): void $toSave = []; foreach ($informationCollection as $information) { + if (!$information->projector) { + continue; + } + if ($information->projectorState->position() > $position) { continue; } @@ -87,7 +91,10 @@ public function run(?int $limit = null): void $toSave[] = $information->projectorState; $handleMethod = $this->resolver->resolveHandleMethod($information->projector, $message); - $handleMethod($message); + + if ($handleMethod) { + $handleMethod($message); + } $information->projectorState->incrementPosition(); } @@ -167,7 +174,7 @@ private function information(): ProjectorInformationCollection return $informationCollection; } - private function findProjector(ProjectorId $id): ?object + private function findProjector(ProjectorId $id): ?Projector { foreach ($this->projectors as $projector) { if ($id->toString() === $projector->projectorId()->toString()) { diff --git a/src/Projection/MetadataAwareProjectionHandler.php b/src/Projection/MetadataAwareProjectionHandler.php index 03ce1b6c..19bbf6de 100644 --- a/src/Projection/MetadataAwareProjectionHandler.php +++ b/src/Projection/MetadataAwareProjectionHandler.php @@ -8,15 +8,14 @@ use Patchlevel\EventSourcing\Metadata\Projection\AttributeProjectionMetadataFactory; use Patchlevel\EventSourcing\Metadata\Projection\ProjectionMetadataFactory; -use function array_key_exists; - -/** @deprecated use DefaultProjectionist */ final class MetadataAwareProjectionHandler implements ProjectionHandler { /** @var iterable */ private iterable $projections; - private ProjectionMetadataFactory $metadataFactor; + private ProjectionMetadataFactory $metadataFactory; + + private ProjectorResolver $resolver; /** * @param iterable $projections @@ -24,51 +23,46 @@ final class MetadataAwareProjectionHandler implements ProjectionHandler public function __construct(iterable $projections, ?ProjectionMetadataFactory $metadataFactory = null) { $this->projections = $projections; - $this->metadataFactor = $metadataFactory ?? new AttributeProjectionMetadataFactory(); + $this->metadataFactory = $metadataFactory ?? new AttributeProjectionMetadataFactory(); + $this->resolver = new MetadataProjectorResolver($this->metadataFactory); } public function handle(Message $message): void { - $event = $message->event(); - foreach ($this->projections as $projection) { - $metadata = $this->metadataFactor->metadata($projection::class); + $handleMethod = $this->resolver->resolveHandleMethod($projection, $message); - if (!array_key_exists($event::class, $metadata->handleMethods)) { + if (!$handleMethod) { continue; } - $handleMethod = $metadata->handleMethods[$event::class]; - - $projection->$handleMethod($message); + $handleMethod($message); } } public function create(): void { foreach ($this->projections as $projection) { - $metadata = $this->metadataFactor->metadata($projection::class); - $method = $metadata->createMethod; + $createMethod = $this->resolver->resolveCreateMethod($projection); - if (!$method) { + if (!$createMethod) { continue; } - $projection->$method(); + $createMethod(); } } public function drop(): void { foreach ($this->projections as $projection) { - $metadata = $this->metadataFactor->metadata($projection::class); - $method = $metadata->dropMethod; + $dropMethod = $this->resolver->resolveDropMethod($projection); - if (!$method) { + if (!$dropMethod) { continue; } - $projection->$method(); + $dropMethod(); } } @@ -82,6 +76,6 @@ public function projections(): iterable public function metadataFactory(): ProjectionMetadataFactory { - return $this->metadataFactor; + return $this->metadataFactory; } } diff --git a/src/Projection/MetadataProjectorResolver.php b/src/Projection/MetadataProjectorResolver.php index ccdce103..3572e221 100644 --- a/src/Projection/MetadataProjectorResolver.php +++ b/src/Projection/MetadataProjectorResolver.php @@ -6,19 +6,19 @@ use Closure; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory; -use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadataFactory; +use Patchlevel\EventSourcing\Metadata\Projection\AttributeProjectionMetadataFactory; +use Patchlevel\EventSourcing\Metadata\Projection\ProjectionMetadataFactory; use function array_key_exists; final class MetadataProjectorResolver implements ProjectorResolver { public function __construct( - private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory() + private readonly ProjectionMetadataFactory $metadataFactory = new AttributeProjectionMetadataFactory() ) { } - public function resolveCreateMethod(Projector $projector): ?Closure + public function resolveCreateMethod(Projection $projector): ?Closure { $metadata = $this->metadataFactory->metadata($projector::class); $method = $metadata->createMethod; @@ -30,7 +30,7 @@ public function resolveCreateMethod(Projector $projector): ?Closure return $projector->$method(...); } - public function resolveDropMethod(Projector $projector): ?Closure + public function resolveDropMethod(Projection $projector): ?Closure { $metadata = $this->metadataFactory->metadata($projector::class); $method = $metadata->dropMethod; @@ -45,7 +45,7 @@ public function resolveDropMethod(Projector $projector): ?Closure /** * @return (Closure(Message):void)|null */ - public function resolveHandleMethod(Projector $projector, Message $message): ?Closure + public function resolveHandleMethod(Projection $projector, Message $message): ?Closure { $event = $message->event(); $metadata = $this->metadataFactory->metadata($projector::class); diff --git a/src/Projection/ProjectionHandler.php b/src/Projection/ProjectionHandler.php index 74c2bc1f..5f58f94d 100644 --- a/src/Projection/ProjectionHandler.php +++ b/src/Projection/ProjectionHandler.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\EventBus\Message; -/** @deprecated use Projectionist */ interface ProjectionHandler { public function handle(Message $message): void; diff --git a/src/Projection/ProjectionListener.php b/src/Projection/ProjectionListener.php index 133e3758..c3c49be0 100644 --- a/src/Projection/ProjectionListener.php +++ b/src/Projection/ProjectionListener.php @@ -7,9 +7,6 @@ use Patchlevel\EventSourcing\EventBus\Listener; use Patchlevel\EventSourcing\EventBus\Message; -/** - * @deprecated - */ final class ProjectionListener implements Listener { private ProjectionHandler $projectionHandler; diff --git a/src/Projection/Projector.php b/src/Projection/Projector.php index 75af8796..5f9dcc6a 100644 --- a/src/Projection/Projector.php +++ b/src/Projection/Projector.php @@ -4,7 +4,7 @@ namespace Patchlevel\EventSourcing\Projection; -interface Projector +interface Projector extends Projection { public function projectorId(): ProjectorId; } diff --git a/src/Projection/ProjectorHandler.php b/src/Projection/ProjectorHandler.php new file mode 100644 index 00000000..263684d7 --- /dev/null +++ b/src/Projection/ProjectorHandler.php @@ -0,0 +1,16 @@ + */ private array $store = []; @@ -30,10 +30,10 @@ public function getStateFromAllProjectors(): array return array_values($this->store); } - public function saveProjectorState(ProjectorState ...$data): void + public function saveProjectorState(ProjectorState ...$projectorStates): void { - foreach ($data as $item) { - $this->store[$item->id()->toString()] = $item; + foreach ($projectorStates as $state) { + $this->store[$state->id()->toString()] = $state; } } diff --git a/src/Projection/ProjectorStore/ProjectorStateNotFound.php b/src/Projection/ProjectorStore/ProjectorStateNotFound.php index 60605a09..1469e7c5 100644 --- a/src/Projection/ProjectorStore/ProjectorStateNotFound.php +++ b/src/Projection/ProjectorStore/ProjectorStateNotFound.php @@ -6,6 +6,6 @@ use RuntimeException; -class ProjectorStateNotFound extends RuntimeException +final class ProjectorStateNotFound extends RuntimeException { } From 3a38a4f52c84aa65abec554f57391e80f6774ac2 Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 20 Sep 2022 14:04:43 +0200 Subject: [PATCH 04/26] add cli commands & remove information dto --- .../Command/ProjectionistBootCommand.php | 31 +++++ .../Command/ProjectionistDestroyCommand.php | 31 +++++ ...ommand.php => ProjectionistRunCommand.php} | 6 +- .../Command/ProjectionistStatusCommand.php | 52 ++++++++ .../Command/ProjectionistTeardownCommand.php | 31 +++++ src/Projection/DefaultProjectionist.php | 124 +++++++++--------- src/Projection/Projectionist.php | 7 + src/Projection/ProjectorInformation.php | 16 --- .../ProjectorInformationCollection.php | 97 -------------- .../ProjectorInformationNotFound.php | 17 --- src/Projection/ProjectorStore/InMemory.php | 5 +- .../ProjectorStateCollection.php | 99 ++++++++++++++ .../ProjectorStore/ProjectorStore.php | 3 +- src/Store/DoctrineStore.php | 23 ++-- 14 files changed, 333 insertions(+), 209 deletions(-) create mode 100644 src/Console/Command/ProjectionistBootCommand.php create mode 100644 src/Console/Command/ProjectionistDestroyCommand.php rename src/Console/Command/{ProjectionRunCommand.php => ProjectionistRunCommand.php} (96%) create mode 100644 src/Console/Command/ProjectionistStatusCommand.php create mode 100644 src/Console/Command/ProjectionistTeardownCommand.php delete mode 100644 src/Projection/ProjectorInformation.php delete mode 100644 src/Projection/ProjectorInformationCollection.php delete mode 100644 src/Projection/ProjectorInformationNotFound.php create mode 100644 src/Projection/ProjectorStore/ProjectorStateCollection.php diff --git a/src/Console/Command/ProjectionistBootCommand.php b/src/Console/Command/ProjectionistBootCommand.php new file mode 100644 index 00000000..705ad6c4 --- /dev/null +++ b/src/Console/Command/ProjectionistBootCommand.php @@ -0,0 +1,31 @@ +projectionist->boot(); + + return 0; + } +} diff --git a/src/Console/Command/ProjectionistDestroyCommand.php b/src/Console/Command/ProjectionistDestroyCommand.php new file mode 100644 index 00000000..94db6c1f --- /dev/null +++ b/src/Console/Command/ProjectionistDestroyCommand.php @@ -0,0 +1,31 @@ +projectionist->destroy(); + + return 0; + } +} diff --git a/src/Console/Command/ProjectionRunCommand.php b/src/Console/Command/ProjectionistRunCommand.php similarity index 96% rename from src/Console/Command/ProjectionRunCommand.php rename to src/Console/Command/ProjectionistRunCommand.php index 4d68756a..ef16ba87 100644 --- a/src/Console/Command/ProjectionRunCommand.php +++ b/src/Console/Command/ProjectionistRunCommand.php @@ -22,10 +22,10 @@ use Symfony\Component\EventDispatcher\EventDispatcher; #[AsCommand( - 'event-sourcing:projection:run', - 'published the messages from the outbox store' + 'event-sourcing:projectionist:run', + 'TODO' )] -final class ProjectionRunCommand extends Command +final class ProjectionistRunCommand extends Command { public function __construct( private readonly Projectionist $projectionist diff --git a/src/Console/Command/ProjectionistStatusCommand.php b/src/Console/Command/ProjectionistStatusCommand.php new file mode 100644 index 00000000..24ea4ec8 --- /dev/null +++ b/src/Console/Command/ProjectionistStatusCommand.php @@ -0,0 +1,52 @@ +projectionist->status(); + + $io->table( + [ + 'name', + 'version', + 'position', + 'status', + ], + array_map( + fn(ProjectorState $state) => [ + $state->id()->name(), + $state->id()->version(), + $state->position(), + $state->status()->value, + ], + $states + ) + ); + + return 0; + } +} diff --git a/src/Console/Command/ProjectionistTeardownCommand.php b/src/Console/Command/ProjectionistTeardownCommand.php new file mode 100644 index 00000000..f539d17f --- /dev/null +++ b/src/Console/Command/ProjectionistTeardownCommand.php @@ -0,0 +1,31 @@ +projectionist->teardown(); + + return 0; + } +} diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index df1e404c..c9e4b816 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -5,14 +5,18 @@ namespace Patchlevel\EventSourcing\Projection; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStore; use Patchlevel\EventSourcing\Store\StreamableStore; -use function array_map; +use function array_values; use function iterator_to_array; final class DefaultProjectionist implements Projectionist { + /** @var array|null */ + private ?array $projectorHashmap = null; + /** * @param iterable $projectors */ @@ -26,15 +30,17 @@ public function __construct( public function boot(): void { - $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Booting); + $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Booting); + + foreach ($projectorStates as $projectorState) { + $projector = $this->findProjector($projectorState->id()); - foreach ($informationCollection as $information) { - if (!$information->projector) { + if (!$projector) { continue; // throw an exception } - $createMethod = $this->resolver->resolveCreateMethod($information->projector); - $information->projectorState->active(); + $createMethod = $this->resolver->resolveCreateMethod($projector); + $projectorState->active(); if (!$createMethod) { continue; @@ -46,13 +52,15 @@ public function boot(): void $stream = $this->streamableMessageStore->stream(); foreach ($stream as $message) { - foreach ($informationCollection as $information) { - if (!$information->projector) { + foreach ($projectorStates as $projectorState) { + $projector = $this->findProjector($projectorState->id()); + + if (!$projector) { continue; // throw an exception } - $handleMethod = $this->resolver->resolveHandleMethod($information->projector, $message); - $information->projectorState->incrementPosition(); + $handleMethod = $this->resolver->resolveHandleMethod($projector, $message); + $projectorState->incrementPosition(); if (!$handleMethod) { continue; @@ -62,41 +70,38 @@ public function boot(): void } } - $this->projectorStore->saveProjectorState( - ...array_map( - static fn (ProjectorInformation $information) => $information->projectorState, - iterator_to_array($informationCollection) - ) - ); + $this->projectorStore->saveProjectorState(...iterator_to_array($projectorStates)); } public function run(?int $limit = null): void { - $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Active); - $position = $informationCollection->minProjectorPosition(); + $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Active); + $position = $projectorStates->minProjectorPosition(); $stream = $this->streamableMessageStore->stream($position); foreach ($stream as $message) { $toSave = []; - foreach ($informationCollection as $information) { - if (!$information->projector) { + foreach ($projectorStates as $projectorState) { + if ($projectorState->position() > $position) { continue; } - if ($information->projectorState->position() > $position) { + $projector = $this->findProjector($projectorState->id()); + + if (!$projector) { continue; } - $toSave[] = $information->projectorState; + $toSave[] = $projectorState; - $handleMethod = $this->resolver->resolveHandleMethod($information->projector, $message); + $handleMethod = $this->resolver->resolveHandleMethod($projector, $message); if ($handleMethod) { $handleMethod($message); } - $information->projectorState->incrementPosition(); + $projectorState->incrementPosition(); } $this->projectorStore->saveProjectorState(...$toSave); @@ -106,14 +111,16 @@ public function run(?int $limit = null): void public function teardown(): void { - $informationCollection = $this->information()->filterByProjectorStatus(ProjectorStatus::Outdated); + $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Outdated); + + foreach ($projectorStates as $projectorState) { + $projector = $this->findProjector($projectorState->id()); - foreach ($informationCollection as $information) { - if (!$information->projector) { + if (!$projector) { continue; // hmm........................ } - $dropMethod = $this->resolver->resolveDropMethod($information->projector); + $dropMethod = $this->resolver->resolveDropMethod($projector); if (!$dropMethod) { continue; @@ -121,67 +128,62 @@ public function teardown(): void $dropMethod(); - $this->projectorStore->removeProjectorState($information->projectorState->id()); + $this->projectorStore->removeProjectorState($projectorState->id()); } } public function destroy(): void { - $informationCollection = $this->information(); + $projectorStates = $this->projectorStates(); - foreach ($informationCollection as $information) { - if ($information->projector) { - $dropMethod = $this->resolver->resolveDropMethod($information->projector); + foreach ($projectorStates as $projectorState) { + $projector = $this->findProjector($projectorState->id()); - if (!$dropMethod) { - continue; - } + if ($projector) { + $dropMethod = $this->resolver->resolveDropMethod($projector); - $dropMethod(); + if ($dropMethod) { + $dropMethod(); + } } - $this->projectorStore->removeProjectorState($information->projectorState->id()); + $this->projectorStore->removeProjectorState($projectorState->id()); } } - private function information(): ProjectorInformationCollection + private function projectorStates(): ProjectorStateCollection { - $informationCollection = new ProjectorInformationCollection(); $projectorsStates = $this->projectorStore->getStateFromAllProjectors(); - foreach ($projectorsStates as $projectorState) { - $informationCollection = $informationCollection->add( - new ProjectorInformation( - $projectorState, - $this->findProjector($projectorState->id()), - ) - ); - } - foreach ($this->projectors as $projector) { - if ($informationCollection->has($projector->projectorId())) { + if ($projectorsStates->has($projector->projectorId())) { continue; } - $informationCollection = $informationCollection->add( - new ProjectorInformation( - new ProjectorState($projector->projectorId()), - $projector - ) - ); + $projectorsStates = $projectorsStates->add(new ProjectorState($projector->projectorId())); } - return $informationCollection; + return $projectorsStates; } private function findProjector(ProjectorId $id): ?Projector { - foreach ($this->projectors as $projector) { - if ($id->toString() === $projector->projectorId()->toString()) { - return $projector; + if ($this->projectorHashmap === null) { + $this->projectorHashmap = []; + + foreach ($this->projectors as $projector) { + $this->projectorHashmap[$projector->projectorId()->toString()] = $projector; } } - return null; + return $this->projectorHashmap[$id->toString()] ?? null; + } + + /** + * @return list + */ + public function status(): array + { + return array_values([...$this->projectorStates()]); } } diff --git a/src/Projection/Projectionist.php b/src/Projection/Projectionist.php index 51176495..c7a6b0d1 100644 --- a/src/Projection/Projectionist.php +++ b/src/Projection/Projectionist.php @@ -4,6 +4,8 @@ namespace Patchlevel\EventSourcing\Projection; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; + interface Projectionist { public function boot(): void; @@ -13,4 +15,9 @@ public function run(?int $limit = null): void; public function teardown(): void; public function destroy(): void; + + /** + * @return list + */ + public function status(): array; } diff --git a/src/Projection/ProjectorInformation.php b/src/Projection/ProjectorInformation.php deleted file mode 100644 index 507ec02c..00000000 --- a/src/Projection/ProjectorInformation.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ -final class ProjectorInformationCollection implements Countable, IteratorAggregate -{ - /** @var array */ - private readonly array $projectorInformation; - - /** - * @param list $projectorInformationList - */ - public function __construct(array $projectorInformationList = []) - { - $result = []; - - foreach ($projectorInformationList as $projectorInformation) { - $result[$projectorInformation->projectorState->id()->toString()] = $projectorInformation; - } - - $this->projectorInformation = $result; - } - - public function get(ProjectorId $projectorId): ProjectorInformation - { - if (!$this->has($projectorId)) { - throw new ProjectorInformationNotFound($projectorId); - } - - return $this->projectorInformation[$projectorId->toString()]; - } - - public function has(ProjectorId $projectorId): bool - { - return array_key_exists($projectorId->toString(), $this->projectorInformation); - } - - public function add(ProjectorInformation $information): self - { - return new self( - [ - ...array_values($this->projectorInformation), - $information, - ] - ); - } - - public function minProjectorPosition(): int - { - $min = 0; - - foreach ($this->projectorInformation as $projectorInformation) { - if ($projectorInformation->projectorState->position() >= $min) { - continue; - } - - $min = $projectorInformation->projectorState->position(); - } - - return $min; - } - - public function filterByProjectorStatus(ProjectorStatus $status): self - { - $projectors = array_filter( - $this->projectorInformation, - static fn (ProjectorInformation $information) => $information->projectorState->status() === $status - ); - - return new self(array_values($projectors)); - } - - public function count(): int - { - return count($this->projectorInformation); - } - - public function getIterator(): Traversable - { - return new ArrayIterator($this->projectorInformation); - } -} diff --git a/src/Projection/ProjectorInformationNotFound.php b/src/Projection/ProjectorInformationNotFound.php deleted file mode 100644 index 3a7d8114..00000000 --- a/src/Projection/ProjectorInformationNotFound.php +++ /dev/null @@ -1,17 +0,0 @@ -toString())); - } -} diff --git a/src/Projection/ProjectorStore/InMemory.php b/src/Projection/ProjectorStore/InMemory.php index 921e6eef..9907c4cb 100644 --- a/src/Projection/ProjectorStore/InMemory.php +++ b/src/Projection/ProjectorStore/InMemory.php @@ -24,10 +24,9 @@ public function getProjectorState(ProjectorId $projectorId): ProjectorState throw new RuntimeException(); // todo } - /** @return list */ - public function getStateFromAllProjectors(): array + public function getStateFromAllProjectors(): ProjectorStateCollection { - return array_values($this->store); + return new ProjectorStateCollection(array_values($this->store)); } public function saveProjectorState(ProjectorState ...$projectorStates): void diff --git a/src/Projection/ProjectorStore/ProjectorStateCollection.php b/src/Projection/ProjectorStore/ProjectorStateCollection.php new file mode 100644 index 00000000..6c5af83d --- /dev/null +++ b/src/Projection/ProjectorStore/ProjectorStateCollection.php @@ -0,0 +1,99 @@ + + */ +final class ProjectorStateCollection implements Countable, IteratorAggregate +{ + /** @var array */ + private readonly array $projectorStates; + + /** + * @param list $projectorStates + */ + public function __construct(array $projectorStates = []) + { + $result = []; + + foreach ($projectorStates as $projectorState) { + $result[$projectorState->id()->toString()] = $projectorState; + } + + $this->projectorStates = $result; + } + + public function get(ProjectorId $projectorId): ProjectorState + { + if (!$this->has($projectorId)) { + throw new ProjectorStateNotFound(); + } + + return $this->projectorStates[$projectorId->toString()]; + } + + public function has(ProjectorId $projectorId): bool + { + return array_key_exists($projectorId->toString(), $this->projectorStates); + } + + public function add(ProjectorState $information): self + { + return new self( + [ + ...array_values($this->projectorStates), + $information, + ] + ); + } + + public function minProjectorPosition(): int + { + $min = 0; + + foreach ($this->projectorStates as $projectorState) { + if ($projectorState->position() >= $min) { + continue; + } + + $min = $projectorState->position(); + } + + return $min; + } + + public function filterByProjectorStatus(ProjectorStatus $status): self + { + $projectors = array_filter( + $this->projectorStates, + static fn (ProjectorState $projectorState) => $projectorState->status() === $status + ); + + return new self(array_values($projectors)); + } + + public function count(): int + { + return count($this->projectorStates); + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->projectorStates); + } +} diff --git a/src/Projection/ProjectorStore/ProjectorStore.php b/src/Projection/ProjectorStore/ProjectorStore.php index edb17f03..953ef099 100644 --- a/src/Projection/ProjectorStore/ProjectorStore.php +++ b/src/Projection/ProjectorStore/ProjectorStore.php @@ -10,8 +10,7 @@ interface ProjectorStore { public function getProjectorState(ProjectorId $projectorId): ProjectorState; - /** @return list */ - public function getStateFromAllProjectors(): array; + public function getStateFromAllProjectors(): ProjectorStateCollection; public function saveProjectorState(ProjectorState ...$projectorStates): void; diff --git a/src/Store/DoctrineStore.php b/src/Store/DoctrineStore.php index b9bbb2dc..c732ddcc 100644 --- a/src/Store/DoctrineStore.php +++ b/src/Store/DoctrineStore.php @@ -17,6 +17,7 @@ use Patchlevel\EventSourcing\Projection\ProjectorId; use Patchlevel\EventSourcing\Projection\ProjectorStatus; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateNotFound; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStore; use Patchlevel\EventSourcing\Serializer\EventSerializer; @@ -203,7 +204,7 @@ public function getProjectorState(ProjectorId $projectorId): ProjectorState ); } - public function getStateFromAllProjectors(): array + public function getStateFromAllProjectors(): ProjectorStateCollection { $sql = $this->connection->createQueryBuilder() ->select('*') @@ -213,15 +214,17 @@ public function getStateFromAllProjectors(): array /** @var list $result */ $result = $this->connection->fetchAllAssociative($sql); - return array_map( - static function (array $data) { - return new ProjectorState( - new ProjectorId($data['projector'], $data['version']), - ProjectorStatus::from($data['status']), - $data['position'] - ); - }, - $result + return new ProjectorStateCollection( + array_map( + static function (array $data) { + return new ProjectorState( + new ProjectorId($data['projector'], $data['version']), + ProjectorStatus::from($data['status']), + $data['position'] + ); + }, + $result + ) ); } From 409f2af7c55e9006cda76295a4fb3070ce37959f Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 20 Sep 2022 14:07:48 +0200 Subject: [PATCH 05/26] use database in integration test --- src/Store/DoctrineStore.php | 2 +- tests/Integration/Projectionist/ProjectionistTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Store/DoctrineStore.php b/src/Store/DoctrineStore.php index c732ddcc..ec8ac6d0 100644 --- a/src/Store/DoctrineStore.php +++ b/src/Store/DoctrineStore.php @@ -239,7 +239,7 @@ static function (Connection $connection) use ($projectorStates): void { 'projector' => $projectorState->id()->name(), 'version' => $projectorState->id()->version(), 'position' => $projectorState->position(), - 'status' => $projectorState->status(), + 'status' => $projectorState->status()->value, ] ); } diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index 6f0e1f3a..a4cbe5c9 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -61,7 +61,7 @@ public function testSuccessful(): void $projectionist = new DefaultProjectionist( $store, - new InMemory(), + $store, [new ProfileProjection($this->connection)], ); From 9af8e880e0b89312a34f3a4c90ac52c9bf0b1cbb Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 20 Sep 2022 15:05:18 +0200 Subject: [PATCH 06/26] fix run limit in projectionist --- docs/pages/projection.md | 28 +++++++++++++------ .../Command/ProjectionistRunCommand.php | 5 ++-- .../Command/ProjectionistStatusCommand.php | 4 ++- .../Projectionist/ProjectionistTest.php | 1 - 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/docs/pages/projection.md b/docs/pages/projection.md index 192b18c2..3234d8b8 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -8,8 +8,9 @@ and everything can always be reproduced from the events. The target of a projection can be anything. Either a file, a relational database, a no-sql database like mongodb or an elasticsearch. -## Define Projection +## Define Projector +To create a projection you need a projector. In this example we always create a new data set in a relational database when a profile is created: ```php @@ -18,9 +19,10 @@ use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Handle; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection; +use Patchlevel\EventSourcing\Projection\Projector; +use Patchlevel\EventSourcing\Projection\ProjectorId; -final class ProfileProjection implements Projection +final class ProfileProjection implements Projector { private Connection $connection; @@ -28,6 +30,14 @@ final class ProfileProjection implements Projection { $this->connection = $connection; } + + public function projectorId(): ProjectorId + { + return new ProjectorId( + name: 'profile', + version: 1 + ); + } #[Create] public function create(): void @@ -59,7 +69,7 @@ final class ProfileProjection implements Projection !!! danger - You should not execute any actions with projections, + You should not execute any actions with projectors, otherwise these will be executed again if you rebuild the projection! !!! tip @@ -67,18 +77,18 @@ final class ProfileProjection implements Projection If you are using psalm then you can install the event sourcing [plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin) to make the event method return the correct type. -Projections have a `create` and a `drop` method that is executed when the projection is created or deleted. +Projectors have a `create` and a `drop` method that is executed when the projection is created or deleted. In some cases it may be that no schema has to be created for the projection, as the target does it automatically. -In order for the projection to know which method is responsible for which event, +In order for the projector to know which method is responsible for which event, the methods must be given the `Handle` attribute with the respective event class name. As soon as the event has been dispatched, the appropriate methods are then executed. -Several projections can also listen to the same event. +Several projectors can also listen to the same event. -## Register projections +## Register projector -So that the projections are known and also executed, you have to add them to the `ProjectionHandler`. +So that the projectors are known and also executed, you have to add them to the `ProjectionHandler`. Then add this to the event bus using the `ProjectionListener`. ```php diff --git a/src/Console/Command/ProjectionistRunCommand.php b/src/Console/Command/ProjectionistRunCommand.php index ef16ba87..b886cf19 100644 --- a/src/Console/Command/ProjectionistRunCommand.php +++ b/src/Console/Command/ProjectionistRunCommand.php @@ -39,9 +39,8 @@ protected function configure(): void ->addOption( 'run-limit', null, - InputOption::VALUE_OPTIONAL, - 'The maximum number of runs this command should execute', - 1 + InputOption::VALUE_REQUIRED, + 'The maximum number of runs this command should execute' ) ->addOption( 'memory-limit', diff --git a/src/Console/Command/ProjectionistStatusCommand.php b/src/Console/Command/ProjectionistStatusCommand.php index 24ea4ec8..80cc8bfe 100644 --- a/src/Console/Command/ProjectionistStatusCommand.php +++ b/src/Console/Command/ProjectionistStatusCommand.php @@ -12,6 +12,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function array_map; + #[AsCommand( 'event-sourcing:projectionist:status', 'TODO' @@ -37,7 +39,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'status', ], array_map( - fn(ProjectorState $state) => [ + static fn (ProjectorState $state) => [ $state->id()->name(), $state->id()->version(), $state->position(), diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index a4cbe5c9..2a2165cd 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\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; use Patchlevel\EventSourcing\Projection\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\ProjectorStore\InMemory; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; From 124ae48b4a4ffb666c7ac276f4a3a2de1766aa55 Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 20 Sep 2022 15:57:59 +0200 Subject: [PATCH 07/26] fix some projectionist bugs --- src/Console/Worker/DefaultWorker.php | 2 +- src/Projection/DefaultProjectionist.php | 3 ++ src/Store/DoctrineStore.php | 44 ++++++++++++++++--------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/Console/Worker/DefaultWorker.php b/src/Console/Worker/DefaultWorker.php index 7b82418f..cbcaaed6 100644 --- a/src/Console/Worker/DefaultWorker.php +++ b/src/Console/Worker/DefaultWorker.php @@ -56,7 +56,7 @@ public function run(int $sleepTimer = 1000): void } $this->logger?->debug('Worker sleep for {sleepTimer}ms', ['sleepTimer' => $sleepFor]); - usleep($sleepFor); + usleep($sleepFor * 1000); } $this->logger?->debug('Worker stopped'); diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index c9e4b816..0f8b2a91 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -90,6 +90,9 @@ public function run(?int $limit = null): void $projector = $this->findProjector($projectorState->id()); if (!$projector) { + $projectorState->outdated(); + $toSave[] = $projectorState; + continue; } diff --git a/src/Store/DoctrineStore.php b/src/Store/DoctrineStore.php index ec8ac6d0..dbeee0ae 100644 --- a/src/Store/DoctrineStore.php +++ b/src/Store/DoctrineStore.php @@ -184,14 +184,13 @@ public function getProjectorState(ProjectorId $projectorId): ProjectorState ->select('*') ->from(self::PROJECTOR_TABLE) ->where('projector = :projector AND version = :version') - ->setParameters([ - 'projector' => $projectorId->name(), - 'version' => $projectorId->version(), - ]) ->getSQL(); /** @var array{projector: string, version: int, position: int, status: string}|false $result */ - $result = $this->connection->fetchOne($sql); + $result = $this->connection->fetchAssociative($sql, [ + 'projector' => $projectorId->name(), + 'version' => $projectorId->version(), + ]); if ($result === false) { throw new ProjectorStateNotFound(); @@ -231,17 +230,32 @@ static function (array $data) { public function saveProjectorState(ProjectorState ...$projectorStates): void { $this->connection->transactional( - static function (Connection $connection) use ($projectorStates): void { + function (Connection $connection) use ($projectorStates): void { foreach ($projectorStates as $projectorState) { - $connection->insert( - self::PROJECTOR_TABLE, - [ - 'projector' => $projectorState->id()->name(), - 'version' => $projectorState->id()->version(), - 'position' => $projectorState->position(), - 'status' => $projectorState->status()->value, - ] - ); + try { + $this->getProjectorState($projectorState->id()); + $connection->update( + self::PROJECTOR_TABLE, + [ + 'position' => $projectorState->position(), + 'status' => $projectorState->status()->value, + ], + [ + 'projector' => $projectorState->id()->name(), + 'version' => $projectorState->id()->version(), + ] + ); + } catch (ProjectorStateNotFound) { + $connection->insert( + self::PROJECTOR_TABLE, + [ + 'projector' => $projectorState->id()->name(), + 'version' => $projectorState->id()->version(), + 'position' => $projectorState->position(), + 'status' => $projectorState->status()->value, + ] + ); + } } } ); From 462bb62b69b841aa59edc94534768757ed04d42a Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 11:12:28 +0200 Subject: [PATCH 08/26] add projector repository --- .../Command/ProjectionistDestroyCommand.php | 2 +- src/Projection/DefaultProjectionist.php | 41 ++++---------- src/Projection/DefaultProjectorRepository.php | 54 +++++++++++++++++++ src/Projection/Projectionist.php | 8 +-- src/Projection/ProjectorCriteria.php | 11 ++++ src/Projection/ProjectorRepository.php | 15 ++++++ .../Projectionist/ProjectionistTest.php | 7 ++- 7 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 src/Projection/DefaultProjectorRepository.php create mode 100644 src/Projection/ProjectorCriteria.php create mode 100644 src/Projection/ProjectorRepository.php diff --git a/src/Console/Command/ProjectionistDestroyCommand.php b/src/Console/Command/ProjectionistDestroyCommand.php index 94db6c1f..50056dcd 100644 --- a/src/Console/Command/ProjectionistDestroyCommand.php +++ b/src/Console/Command/ProjectionistDestroyCommand.php @@ -24,7 +24,7 @@ public function __construct( protected function execute(InputInterface $input, OutputInterface $output): int { - $this->projectionist->destroy(); + $this->projectionist->remove(); return 0; } diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 0f8b2a91..444d59f4 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -14,26 +14,20 @@ final class DefaultProjectionist implements Projectionist { - /** @var array|null */ - private ?array $projectorHashmap = null; - - /** - * @param iterable $projectors - */ public function __construct( private readonly StreamableStore $streamableMessageStore, private readonly ProjectorStore $projectorStore, - private readonly iterable $projectors, + private readonly ProjectorRepository $projectorRepository, private readonly ProjectorResolver $resolver = new MetadataProjectorResolver() ) { } - public function boot(): void + public function boot(ProjectorCriteria $criteria = new ProjectorCriteria()): void { $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Booting); foreach ($projectorStates as $projectorState) { - $projector = $this->findProjector($projectorState->id()); + $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { continue; // throw an exception @@ -53,7 +47,7 @@ public function boot(): void foreach ($stream as $message) { foreach ($projectorStates as $projectorState) { - $projector = $this->findProjector($projectorState->id()); + $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { continue; // throw an exception @@ -73,7 +67,7 @@ public function boot(): void $this->projectorStore->saveProjectorState(...iterator_to_array($projectorStates)); } - public function run(?int $limit = null): void + public function run(ProjectorCriteria $criteria = new ProjectorCriteria(), ?int $limit = null): void { $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Active); $position = $projectorStates->minProjectorPosition(); @@ -87,7 +81,7 @@ public function run(?int $limit = null): void continue; } - $projector = $this->findProjector($projectorState->id()); + $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { $projectorState->outdated(); @@ -112,12 +106,12 @@ public function run(?int $limit = null): void } } - public function teardown(): void + public function teardown(ProjectorCriteria $criteria = new ProjectorCriteria()): void { $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Outdated); foreach ($projectorStates as $projectorState) { - $projector = $this->findProjector($projectorState->id()); + $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { continue; // hmm........................ @@ -135,12 +129,12 @@ public function teardown(): void } } - public function destroy(): void + public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): void { $projectorStates = $this->projectorStates(); foreach ($projectorStates as $projectorState) { - $projector = $this->findProjector($projectorState->id()); + $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if ($projector) { $dropMethod = $this->resolver->resolveDropMethod($projector); @@ -158,7 +152,7 @@ private function projectorStates(): ProjectorStateCollection { $projectorsStates = $this->projectorStore->getStateFromAllProjectors(); - foreach ($this->projectors as $projector) { + foreach ($this->projectorRepository->projectors() as $projector) { if ($projectorsStates->has($projector->projectorId())) { continue; } @@ -169,19 +163,6 @@ private function projectorStates(): ProjectorStateCollection return $projectorsStates; } - private function findProjector(ProjectorId $id): ?Projector - { - if ($this->projectorHashmap === null) { - $this->projectorHashmap = []; - - foreach ($this->projectors as $projector) { - $this->projectorHashmap[$projector->projectorId()->toString()] = $projector; - } - } - - return $this->projectorHashmap[$id->toString()] ?? null; - } - /** * @return list */ diff --git a/src/Projection/DefaultProjectorRepository.php b/src/Projection/DefaultProjectorRepository.php new file mode 100644 index 00000000..3b7b6863 --- /dev/null +++ b/src/Projection/DefaultProjectorRepository.php @@ -0,0 +1,54 @@ +|null */ + private ?array $projectorIdHashmap = null; + + /** @var array|null */ + private ?array $projectorNameHashmap = null; + + /** + * @param iterable $projectors + */ + public function __construct( + private readonly iterable $projectors, + ) { + } + + public function findByProjectorId(ProjectorId $projectorId): ?Projector + { + if ($this->projectorIdHashmap === null) { + $this->projectorIdHashmap = []; + + foreach ($this->projectors as $projector) { + $this->projectorIdHashmap[$projector->projectorId()->toString()] = $projector; + } + } + + return $this->projectorIdHashmap[$projectorId->toString()] ?? null; + } + + public function findByProjectorName(string $name): ?Projector + { + if ($this->projectorNameHashmap === null) { + $this->projectorNameHashmap = []; + + foreach ($this->projectors as $projector) { + $this->projectorNameHashmap[$projector->projectorId()->name()] = $projector; + } + } + + return $this->projectorNameHashmap[$name] ?? null; + } + + /** + * @return list + */ + public function projectors(): array + { + return [...$this->projectors]; + } +} \ No newline at end of file diff --git a/src/Projection/Projectionist.php b/src/Projection/Projectionist.php index c7a6b0d1..8d107b69 100644 --- a/src/Projection/Projectionist.php +++ b/src/Projection/Projectionist.php @@ -8,13 +8,13 @@ interface Projectionist { - public function boot(): void; + public function boot(ProjectorCriteria $criteria = new ProjectorCriteria()): void; - public function run(?int $limit = null): void; + public function run(ProjectorCriteria $criteria = new ProjectorCriteria(), ?int $limit = null): void; - public function teardown(): void; + public function teardown(ProjectorCriteria $criteria = new ProjectorCriteria()): void; - public function destroy(): void; + public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): void; /** * @return list diff --git a/src/Projection/ProjectorCriteria.php b/src/Projection/ProjectorCriteria.php new file mode 100644 index 00000000..ac648a1b --- /dev/null +++ b/src/Projection/ProjectorCriteria.php @@ -0,0 +1,11 @@ + + */ + public function projectors(): array; +} \ No newline at end of file diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index 2a2165cd..7497d1d0 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -9,6 +9,7 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; use Patchlevel\EventSourcing\Projection\DefaultProjectionist; +use Patchlevel\EventSourcing\Projection\DefaultProjectorRepository; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; @@ -61,7 +62,9 @@ public function testSuccessful(): void $projectionist = new DefaultProjectionist( $store, $store, - [new ProfileProjection($this->connection)], + new DefaultProjectorRepository( + [new ProfileProjection($this->connection)] + ), ); $projectionist->boot(); @@ -74,6 +77,6 @@ public function testSuccessful(): void self::assertSame('1', $result['id']); self::assertSame('John', $result['name']); - $projectionist->destroy(); + $projectionist->remove(); } } From c2cb61aec8befe972ca59c959b495becbe73222a Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 14:04:57 +0200 Subject: [PATCH 09/26] deprecate old projection services and add new ones --- src/Projection/DefaultProjectorRepository.php | 4 ++- .../MetadataAwareProjectionHandler.php | 3 ++ src/Projection/ProjectionHandler.php | 3 ++ src/Projection/ProjectionListener.php | 3 ++ src/Projection/ProjectorCriteria.php | 4 ++- src/Projection/ProjectorHandler.php | 16 ---------- src/Projection/ProjectorRepository.php | 4 ++- src/Projection/SyncProjectorListener.php | 30 +++++++++++++++++++ .../BasicIntegrationTest.php | 20 ++++++------- 9 files changed, 58 insertions(+), 29 deletions(-) delete mode 100644 src/Projection/ProjectorHandler.php create mode 100644 src/Projection/SyncProjectorListener.php diff --git a/src/Projection/DefaultProjectorRepository.php b/src/Projection/DefaultProjectorRepository.php index 3b7b6863..7fcbc36c 100644 --- a/src/Projection/DefaultProjectorRepository.php +++ b/src/Projection/DefaultProjectorRepository.php @@ -1,5 +1,7 @@ projectors]; } -} \ No newline at end of file +} diff --git a/src/Projection/MetadataAwareProjectionHandler.php b/src/Projection/MetadataAwareProjectionHandler.php index 19bbf6de..132c7e34 100644 --- a/src/Projection/MetadataAwareProjectionHandler.php +++ b/src/Projection/MetadataAwareProjectionHandler.php @@ -8,6 +8,9 @@ use Patchlevel\EventSourcing\Metadata\Projection\AttributeProjectionMetadataFactory; use Patchlevel\EventSourcing\Metadata\Projection\ProjectionMetadataFactory; +/** + * @deprecated use MetadataProjectorResolver + */ final class MetadataAwareProjectionHandler implements ProjectionHandler { /** @var iterable */ diff --git a/src/Projection/ProjectionHandler.php b/src/Projection/ProjectionHandler.php index 5f58f94d..18a9100d 100644 --- a/src/Projection/ProjectionHandler.php +++ b/src/Projection/ProjectionHandler.php @@ -6,6 +6,9 @@ use Patchlevel\EventSourcing\EventBus\Message; +/** + * @deprecated use ProjectorResolver + */ interface ProjectionHandler { public function handle(Message $message): void; diff --git a/src/Projection/ProjectionListener.php b/src/Projection/ProjectionListener.php index c3c49be0..c928b12f 100644 --- a/src/Projection/ProjectionListener.php +++ b/src/Projection/ProjectionListener.php @@ -7,6 +7,9 @@ use Patchlevel\EventSourcing\EventBus\Listener; use Patchlevel\EventSourcing\EventBus\Message; +/** + * @deprecated use SyncProjectorListener + */ final class ProjectionListener implements Listener { private ProjectionHandler $projectionHandler; diff --git a/src/Projection/ProjectorCriteria.php b/src/Projection/ProjectorCriteria.php index ac648a1b..f5ee2c57 100644 --- a/src/Projection/ProjectorCriteria.php +++ b/src/Projection/ProjectorCriteria.php @@ -1,5 +1,7 @@ */ public function projectors(): array; -} \ No newline at end of file +} diff --git a/src/Projection/SyncProjectorListener.php b/src/Projection/SyncProjectorListener.php new file mode 100644 index 00000000..cf484bf1 --- /dev/null +++ b/src/Projection/SyncProjectorListener.php @@ -0,0 +1,30 @@ +projectorRepository->projectors() as $projector) { + $handleMethod = $this->projectorResolver->resolveHandleMethod($projector, $message); + + if (!$handleMethod) { + continue; + } + + $handleMethod($message); + } + } +} diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php index 7f656e9f..751224ff 100644 --- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php +++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php @@ -12,8 +12,8 @@ use Patchlevel\EventSourcing\EventBus\SymfonyEventBus; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; -use Patchlevel\EventSourcing\Projection\MetadataAwareProjectionHandler; -use Patchlevel\EventSourcing\Projection\ProjectionListener; +use Patchlevel\EventSourcing\Projection\DefaultProjectorRepository; +use Patchlevel\EventSourcing\Projection\SyncProjectorListener; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; @@ -49,12 +49,12 @@ public function tearDown(): void public function testSuccessful(): void { $profileProjection = new ProfileProjection($this->connection); - $projectionRepository = new MetadataAwareProjectionHandler( + $projectorRepository = new DefaultProjectorRepository( [$profileProjection] ); $eventStream = new DefaultEventBus(); - $eventStream->addListener(new ProjectionListener($projectionRepository)); + $eventStream->addListener(new SyncProjectorListener($projectorRepository)); $eventStream->addListener(new SendEmailProcessor()); $store = new SingleTableStore( @@ -111,12 +111,12 @@ public function testSuccessful(): void public function testWithSymfonySuccessful(): void { $profileProjection = new ProfileProjection($this->connection); - $projectionRepository = new MetadataAwareProjectionHandler( + $projectorRepository = new DefaultProjectorRepository( [$profileProjection] ); $eventStream = SymfonyEventBus::create([ - new ProjectionListener($projectionRepository), + new SyncProjectorListener($projectorRepository), new SendEmailProcessor(), ]); @@ -175,12 +175,12 @@ public function testWithSymfonySuccessful(): void public function testMultiTableSuccessful(): void { $profileProjection = new ProfileProjection($this->connection); - $projectionRepository = new MetadataAwareProjectionHandler( + $projectorRepository = new DefaultProjectorRepository( [$profileProjection] ); $eventStream = new DefaultEventBus(); - $eventStream->addListener(new ProjectionListener($projectionRepository)); + $eventStream->addListener(new SyncProjectorListener($projectorRepository)); $eventStream->addListener(new SendEmailProcessor()); $store = new MultiTableStore( @@ -236,12 +236,12 @@ public function testMultiTableSuccessful(): void public function testSnapshot(): void { $profileProjection = new ProfileProjection($this->connection); - $projectionRepository = new MetadataAwareProjectionHandler( + $projectorRepository = new DefaultProjectorRepository( [$profileProjection] ); $eventStream = new DefaultEventBus(); - $eventStream->addListener(new ProjectionListener($projectionRepository)); + $eventStream->addListener(new SyncProjectorListener($projectorRepository)); $eventStream->addListener(new SendEmailProcessor()); $store = new SingleTableStore( From 4f8a189f6f28c1224c16cb6b9903f4fa46eabf80 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 14:41:31 +0200 Subject: [PATCH 10/26] fix static analyser --- baseline.xml | 240 ++++++++++++++++++ phpstan-baseline.neon | 5 + src/Projection/DefaultProjectionist.php | 6 +- src/Projection/MetadataProjectorResolver.php | 3 - src/Projection/ProjectorCriteria.php | 3 + .../ProjectorStateCollection.php | 10 +- ...ileProjection.php => ProfileProjector.php} | 16 +- tests/Benchmark/WriteEventsBench.php | 4 +- .../Projection/ProfileProjection.php | 14 +- .../Projection/ProfileProjection.php | 3 + 10 files changed, 289 insertions(+), 15 deletions(-) rename tests/Benchmark/BasicImplementation/Projection/{ProfileProjection.php => ProfileProjector.php} (74%) diff --git a/baseline.xml b/baseline.xml index 4955142b..56cd11a9 100644 --- a/baseline.xml +++ b/baseline.xml @@ -10,6 +10,29 @@ $sleep + + + MetadataAwareProjectionHandler + MetadataAwareProjectionHandler::class + Projection + ProjectionHandler + ProjectionHandler + ProjectionHandler + ProjectionHandler + non-empty-array<class-string<Projection>> + non-empty-array<class-string<Projection>>|null + + + + + ProjectionHandler + + + + + $sleep + + SchemaManager|SchemaDirector @@ -43,6 +66,81 @@ array<class-string, string> + + + array + class-string<Projection> + private array $projectionMetadata = []; + + + + + class-string<Projection> + + + + + class-string<Projection> + + + + + class-string<Projection> + + + + + class-string<Projection> + + + + + PipelineStore + PipelineStore + + + + + ProjectionHandler + ProjectionHandler + + + + + MetadataAwareProjectionHandler + Projection + + + + + iterable + iterable<Projection> + iterable<Projection> + + + MetadataAwareProjectionHandler + + + + + Projection + Projection + Projection + + + + + ProjectionHandler + ProjectionHandler + + + + + Projection + Projection + Projection + + $messages[0]->playhead() - 1 @@ -92,10 +190,23 @@ + + MultiTableStore + array{id: string, aggregate_id: string, playhead: string, event: string, payload: string, recorded_on: string, custom_headers: string}|null + + + SingleTableStore + + + + + ProfileProjector + + $bus @@ -112,6 +223,9 @@ + + new ProjectionListener($projectionRepository) + $bus $profile @@ -119,6 +233,79 @@ $store + + + new MetadataAwareProjectionHandler([$bankAccountProjection]) + new MetadataAwareProjectionHandler([$bankAccountProjection]) + new ProjectionListener($projectionRepository) + new ProjectionListener($projectionRepository) + + + + + BankAccountProjection + + + + + ProfileProjection + + + + + new ProjectionListener($projectionRepository) + + + + + ProfileProjection + + + + + ProfileProjection + + + + + new DoctrineSchemaManager() + + + + + $this->prophesize(ProjectionHandler::class) + new MetadataAwareProjectionHandler([$projectionA, $projectionB]) + + + + + $this->prophesize(ProjectionHandler::class) + new MetadataAwareProjectionHandler([$projectionA, $projectionB]) + + + + + $this->prophesize(PipelineStore::class) + $this->prophesize(PipelineStore::class) + $this->prophesize(PipelineStore::class) + $this->prophesize(PipelineStore::class) + $this->prophesize(ProjectionHandler::class) + $this->prophesize(ProjectionHandler::class) + $this->prophesize(ProjectionHandler::class) + new MetadataAwareProjectionHandler([$projectionA, $projectionB]) + new MetadataAwareProjectionHandler([$projectionA, $projectionB]) + + + + + Dummy2Projection + + + + + DummyProjection + + $value @@ -129,6 +316,59 @@ $event + + + class implements Projection { + class implements Projection { + class implements Projection { + class implements Projection { + class implements Projection { + + + + + $this->prophesize(PipelineStore::class) + $this->prophesize(PipelineStore::class) + $this->prophesize(PipelineStore::class) + $this->prophesize(PipelineStore::class) + + + + + $this->prophesize(ProjectionHandler::class) + + + + + class implements Projection { + + + + + $this->prophesize(PipelineStore::class) + + + + + new MetadataAwareProjectionHandler([$projection]) + new MetadataAwareProjectionHandler([$projection]) + new MetadataAwareProjectionHandler([$projection]) + new MetadataAwareProjectionHandler([$projection]) + new MetadataAwareProjectionHandler([]) + + + class implements Projection { + class implements Projection { + class implements Projection { + class implements Projection { + + + + + $this->prophesize(ProjectionHandler::class) + new ProjectionListener($projectionRepository->reveal()) + + new Comparator() diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c60b17ec..56222259 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -10,6 +10,11 @@ parameters: count: 1 path: src/Console/Worker/DefaultWorker.php + - + message: "#^Method Patchlevel\\\\EventSourcing\\\\Projection\\\\DefaultProjectorRepository\\:\\:projectors\\(\\) should return array\\ but returns array\\\\.$#" + count: 1 + path: src/Projection/DefaultProjectorRepository.php + - message: "#^Parameter \\#2 \\$data of method Patchlevel\\\\EventSourcing\\\\Serializer\\\\Hydrator\\\\AggregateRootHydrator\\:\\:hydrate\\(\\) expects array\\, mixed given\\.$#" count: 1 diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 444d59f4..376994d1 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -168,6 +168,10 @@ private function projectorStates(): ProjectorStateCollection */ public function status(): array { - return array_values([...$this->projectorStates()]); + return array_values( + iterator_to_array( + $this->projectorStates()->getIterator() + ) + ); } } diff --git a/src/Projection/MetadataProjectorResolver.php b/src/Projection/MetadataProjectorResolver.php index 3572e221..ba971e16 100644 --- a/src/Projection/MetadataProjectorResolver.php +++ b/src/Projection/MetadataProjectorResolver.php @@ -42,9 +42,6 @@ public function resolveDropMethod(Projection $projector): ?Closure return $projector->$method(...); } - /** - * @return (Closure(Message):void)|null - */ public function resolveHandleMethod(Projection $projector, Message $message): ?Closure { $event = $message->event(); diff --git a/src/Projection/ProjectorCriteria.php b/src/Projection/ProjectorCriteria.php index f5ee2c57..938e5be8 100644 --- a/src/Projection/ProjectorCriteria.php +++ b/src/Projection/ProjectorCriteria.php @@ -6,6 +6,9 @@ final class ProjectorCriteria { + /** + * @param list|null $names + */ public function __construct( public readonly ?array $names = null ) { diff --git a/src/Projection/ProjectorStore/ProjectorStateCollection.php b/src/Projection/ProjectorStore/ProjectorStateCollection.php index 6c5af83d..a69ea369 100644 --- a/src/Projection/ProjectorStore/ProjectorStateCollection.php +++ b/src/Projection/ProjectorStore/ProjectorStateCollection.php @@ -9,7 +9,6 @@ use IteratorAggregate; use Patchlevel\EventSourcing\Projection\ProjectorId; use Patchlevel\EventSourcing\Projection\ProjectorStatus; -use Traversable; use function array_filter; use function array_key_exists; @@ -17,7 +16,7 @@ use function count; /** - * @implements IteratorAggregate + * @implements IteratorAggregate */ final class ProjectorStateCollection implements Countable, IteratorAggregate { @@ -92,8 +91,11 @@ public function count(): int return count($this->projectorStates); } - public function getIterator(): Traversable + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator { - return new ArrayIterator($this->projectorStates); + return new ArrayIterator(array_values($this->projectorStates)); } } diff --git a/tests/Benchmark/BasicImplementation/Projection/ProfileProjection.php b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php similarity index 74% rename from tests/Benchmark/BasicImplementation/Projection/ProfileProjection.php rename to tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php index 615bcd50..34ab6b75 100644 --- a/tests/Benchmark/BasicImplementation/Projection/ProfileProjection.php +++ b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php @@ -9,10 +9,13 @@ use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Handle; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection; -use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; +use Patchlevel\EventSourcing\Projection\Projector; +use Patchlevel\EventSourcing\Projection\ProjectorId; +use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\ProfileCreated; -final class ProfileProjection implements Projection +use function assert; + +final class ProfileProjector implements Projector { private Connection $connection; @@ -21,6 +24,11 @@ public function __construct(Connection $connection) $this->connection = $connection; } + public function projectorId(): ProjectorId + { + return new ProjectorId('profile', 1); + } + #[Create] public function create(): void { @@ -38,6 +46,8 @@ public function handleProfileCreated(Message $message): void { $profileCreated = $message->event(); + assert($profileCreated instanceof ProfileCreated); + $this->connection->executeStatement( 'INSERT INTO projection_profile (`id`, `name`) VALUES(:id, :name);', [ diff --git a/tests/Benchmark/WriteEventsBench.php b/tests/Benchmark/WriteEventsBench.php index c1aa1388..536e648b 100644 --- a/tests/Benchmark/WriteEventsBench.php +++ b/tests/Benchmark/WriteEventsBench.php @@ -20,7 +20,7 @@ use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Aggregate\Profile; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Processor\SendEmailProcessor; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId; -use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Projection\ProfileProjection; +use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Projection\ProfileProjector; use PhpBench\Attributes as Bench; use function file_exists; @@ -47,7 +47,7 @@ public function setUp(): void 'path' => self::DB_PATH, ]); - $profileProjection = new ProfileProjection($connection); + $profileProjection = new ProfileProjector($connection); $projectionRepository = new MetadataAwareProjectionHandler( [$profileProjection] ); diff --git a/tests/Integration/BasicImplementation/Projection/ProfileProjection.php b/tests/Integration/BasicImplementation/Projection/ProfileProjection.php index b7025317..73c39b80 100644 --- a/tests/Integration/BasicImplementation/Projection/ProfileProjection.php +++ b/tests/Integration/BasicImplementation/Projection/ProfileProjection.php @@ -10,10 +10,13 @@ use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Handle; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection; +use Patchlevel\EventSourcing\Projection\Projector; +use Patchlevel\EventSourcing\Projection\ProjectorId; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; -final class ProfileProjection implements Projection +use function assert; + +final class ProfileProjection implements Projector { private Connection $connection; @@ -22,6 +25,11 @@ public function __construct(Connection $connection) $this->connection = $connection; } + public function projectorId(): ProjectorId + { + return new ProjectorId('profile', 1); + } + #[Create] public function create(): void { @@ -44,6 +52,8 @@ public function handleProfileCreated(Message $message): void { $profileCreated = $message->event(); + assert($profileCreated instanceof ProfileCreated); + $this->connection->executeStatement( 'INSERT INTO projection_profile (id, name) VALUES(:id, :name);', [ diff --git a/tests/Integration/Projectionist/Projection/ProfileProjection.php b/tests/Integration/Projectionist/Projection/ProfileProjection.php index 340afd49..1558bb66 100644 --- a/tests/Integration/Projectionist/Projection/ProfileProjection.php +++ b/tests/Integration/Projectionist/Projection/ProfileProjection.php @@ -14,6 +14,7 @@ use Patchlevel\EventSourcing\Projection\ProjectorId; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; +use function assert; use function sprintf; final class ProfileProjection implements Projector @@ -47,6 +48,8 @@ public function handleProfileCreated(Message $message): void { $profileCreated = $message->event(); + assert($profileCreated instanceof ProfileCreated); + $this->connection->executeStatement( 'INSERT INTO ' . $this->tableName() . ' (id, name) VALUES(:id, :name);', [ From baf21cda7f5bebca5581f27af67b540dae1f975f Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 19:12:20 +0200 Subject: [PATCH 11/26] cleanup projectionist & add logger --- .../Command/ProjectionistBootCommand.php | 16 +- src/Console/Command/ProjectionistCommand.php | 23 +++ ...and.php => ProjectionistRemoveCommand.php} | 16 +- .../Command/ProjectionistRunCommand.php | 23 +-- .../Command/ProjectionistStatusCommand.php | 12 +- .../Command/ProjectionistTeardownCommand.php | 16 +- src/Projection/DefaultProjectionist.php | 154 ++++++++++++------ src/Projection/DefaultProjectorRepository.php | 16 -- src/Projection/Projectionist.php | 37 +++-- src/Projection/ProjectorCriteria.php | 2 +- src/Projection/ProjectorRepository.php | 2 - src/Projection/ProjectorStatus.php | 1 + .../{InMemory.php => InMemoryStore.php} | 2 +- .../ProjectorStore/ProjectorState.php | 15 +- .../ProjectorStateCollection.php | 24 +++ 15 files changed, 219 insertions(+), 140 deletions(-) create mode 100644 src/Console/Command/ProjectionistCommand.php rename src/Console/Command/{ProjectionistDestroyCommand.php => ProjectionistRemoveCommand.php} (56%) rename src/Projection/ProjectorStore/{InMemory.php => InMemoryStore.php} (95%) diff --git a/src/Console/Command/ProjectionistBootCommand.php b/src/Console/Command/ProjectionistBootCommand.php index 705ad6c4..2e47f604 100644 --- a/src/Console/Command/ProjectionistBootCommand.php +++ b/src/Console/Command/ProjectionistBootCommand.php @@ -4,27 +4,23 @@ namespace Patchlevel\EventSourcing\Console\Command; -use Patchlevel\EventSourcing\Projection\Projectionist; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( 'event-sourcing:projectionist:boot', 'TODO' )] -final class ProjectionistBootCommand extends Command +final class ProjectionistBootCommand extends ProjectionistCommand { - public function __construct( - private readonly Projectionist $projectionist - ) { - parent::__construct(); - } - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->projectionist->boot(); + $logger = new ConsoleLogger($output); + + $criteria = $this->projectorCriteria(); + $this->projectionist->boot($criteria, $logger); return 0; } diff --git a/src/Console/Command/ProjectionistCommand.php b/src/Console/Command/ProjectionistCommand.php new file mode 100644 index 00000000..f0a266d3 --- /dev/null +++ b/src/Console/Command/ProjectionistCommand.php @@ -0,0 +1,23 @@ +projectionist->remove(); + $logger = new ConsoleLogger($output); + + $criteria = $this->projectorCriteria(); + $this->projectionist->remove($criteria, $logger); return 0; } diff --git a/src/Console/Command/ProjectionistRunCommand.php b/src/Console/Command/ProjectionistRunCommand.php index b886cf19..b99c1ba6 100644 --- a/src/Console/Command/ProjectionistRunCommand.php +++ b/src/Console/Command/ProjectionistRunCommand.php @@ -12,9 +12,7 @@ use Patchlevel\EventSourcing\Console\Worker\Listener\StopWorkerOnMemoryLimitListener; use Patchlevel\EventSourcing\Console\Worker\Listener\StopWorkerOnSigtermSignalListener; use Patchlevel\EventSourcing\Console\Worker\Listener\StopWorkerOnTimeLimitListener; -use Patchlevel\EventSourcing\Projection\Projectionist; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Logger\ConsoleLogger; @@ -25,14 +23,8 @@ 'event-sourcing:projectionist:run', 'TODO' )] -final class ProjectionistRunCommand extends Command +final class ProjectionistRunCommand extends ProjectionistCommand { - public function __construct( - private readonly Projectionist $projectionist - ) { - parent::__construct(); - } - protected function configure(): void { $this @@ -42,6 +34,13 @@ protected function configure(): void InputOption::VALUE_REQUIRED, 'The maximum number of runs this command should execute' ) + ->addOption( + 'message-limit', + null, + InputOption::VALUE_REQUIRED, + 'How many messages should be consumed in one run', + 100 + ) ->addOption( 'memory-limit', null, @@ -66,9 +65,11 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $runLimit = InputHelper::nullableInt($input->getOption('run-limit')); + $messageLimit = InputHelper::int($input->getOption('message-limit')); $memoryLimit = InputHelper::nullableString($input->getOption('memory-limit')); $timeLimit = InputHelper::nullableInt($input->getOption('time-limit')); $sleep = InputHelper::int($input->getOption('sleep')); + $criteria = $this->projectorCriteria(); $logger = new ConsoleLogger($output); @@ -96,8 +97,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $worker = new DefaultWorker( - function (): void { - $this->projectionist->run(); + function () use ($criteria, $messageLimit, $logger): void { + $this->projectionist->run($criteria, $messageLimit, $logger); }, $eventDispatcher, $logger diff --git a/src/Console/Command/ProjectionistStatusCommand.php b/src/Console/Command/ProjectionistStatusCommand.php index 80cc8bfe..dd9075c3 100644 --- a/src/Console/Command/ProjectionistStatusCommand.php +++ b/src/Console/Command/ProjectionistStatusCommand.php @@ -5,10 +5,8 @@ namespace Patchlevel\EventSourcing\Console\Command; use Patchlevel\EventSourcing\Console\OutputStyle; -use Patchlevel\EventSourcing\Projection\Projectionist; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -18,14 +16,8 @@ 'event-sourcing:projectionist:status', 'TODO' )] -final class ProjectionistStatusCommand extends Command +final class ProjectionistStatusCommand extends ProjectionistCommand { - public function __construct( - private readonly Projectionist $projectionist - ) { - parent::__construct(); - } - protected function execute(InputInterface $input, OutputInterface $output): int { $io = new OutputStyle($input, $output); @@ -45,7 +37,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $state->position(), $state->status()->value, ], - $states + [...$states] ) ); diff --git a/src/Console/Command/ProjectionistTeardownCommand.php b/src/Console/Command/ProjectionistTeardownCommand.php index f539d17f..f29114e5 100644 --- a/src/Console/Command/ProjectionistTeardownCommand.php +++ b/src/Console/Command/ProjectionistTeardownCommand.php @@ -4,27 +4,23 @@ namespace Patchlevel\EventSourcing\Console\Command; -use Patchlevel\EventSourcing\Projection\Projectionist; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( 'event-sourcing:projectionist:teardown', 'TODO' )] -final class ProjectionistTeardownCommand extends Command +final class ProjectionistTeardownCommand extends ProjectionistCommand { - public function __construct( - private readonly Projectionist $projectionist - ) { - parent::__construct(); - } - protected function execute(InputInterface $input, OutputInterface $output): int { - $this->projectionist->teardown(); + $logger = new ConsoleLogger($output); + + $criteria = $this->projectorCriteria(); + $this->projectionist->teardown($criteria, $logger); return 0; } diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 376994d1..2e1cb493 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -8,9 +8,11 @@ use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStore; use Patchlevel\EventSourcing\Store\StreamableStore; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; -use function array_values; -use function iterator_to_array; +use function sprintf; final class DefaultProjectionist implements Projectionist { @@ -18,119 +20,170 @@ public function __construct( private readonly StreamableStore $streamableMessageStore, private readonly ProjectorStore $projectorStore, private readonly ProjectorRepository $projectorRepository, - private readonly ProjectorResolver $resolver = new MetadataProjectorResolver() + private readonly ProjectorResolver $resolver = new MetadataProjectorResolver(), ) { } - public function boot(ProjectorCriteria $criteria = new ProjectorCriteria()): void - { - $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Booting); + public function boot( + ProjectorCriteria $criteria = new ProjectorCriteria(), + ?LoggerInterface $logger = null + ): void { + $projectorStates = $this->projectorStates() + ->filterByProjectorStatus(ProjectorStatus::New) + ->filterByCriteria($criteria); foreach ($projectorStates as $projectorState) { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - continue; // throw an exception + throw new RuntimeException(); } + $projectorState->booting(); + $this->projectorStore->saveProjectorState($projectorState); + $createMethod = $this->resolver->resolveCreateMethod($projector); - $projectorState->active(); if (!$createMethod) { continue; } - $createMethod(); + try { + $createMethod(); + $logger?->info(sprintf('%s created', $projectorState->id()->toString())); + } catch (Throwable $e) { + $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); + $logger?->error($e->getMessage()); + $projectorState->error(); + $this->projectorStore->saveProjectorState($projectorState); + } } $stream = $this->streamableMessageStore->stream(); foreach ($stream as $message) { - foreach ($projectorStates as $projectorState) { + foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Booting) as $projectorState) { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - continue; // throw an exception + throw new RuntimeException(); } $handleMethod = $this->resolver->resolveHandleMethod($projector, $message); - $projectorState->incrementPosition(); - if (!$handleMethod) { - continue; + if ($handleMethod) { + try { + $handleMethod($message); + } catch (Throwable $e) { + $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); + $logger?->error($e->getMessage()); + $projectorState->error(); + $this->projectorStore->saveProjectorState($projectorState); + + continue; + } } - $handleMethod($message); + $projectorState->incrementPosition(); + $this->projectorStore->saveProjectorState($projectorState); } } - $this->projectorStore->saveProjectorState(...iterator_to_array($projectorStates)); + foreach ($projectorStates as $projectorState) { + $projectorState->active(); + $this->projectorStore->saveProjectorState($projectorState); + } } - public function run(ProjectorCriteria $criteria = new ProjectorCriteria(), ?int $limit = null): void - { - $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Active); - $position = $projectorStates->minProjectorPosition(); - $stream = $this->streamableMessageStore->stream($position); + public function run( + ProjectorCriteria $criteria = new ProjectorCriteria(), + ?int $limit = null, + ?LoggerInterface $logger = null + ): void { + $projectorStates = $this->projectorStates() + ->filterByProjectorStatus(ProjectorStatus::Active) + ->filterByCriteria($criteria); - foreach ($stream as $message) { - $toSave = []; + $currentPosition = $projectorStates->minProjectorPosition(); + $stream = $this->streamableMessageStore->stream($currentPosition); - foreach ($projectorStates as $projectorState) { - if ($projectorState->position() > $position) { + foreach ($stream as $message) { + foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Active) as $projectorState) { + if ($projectorState->position() > $currentPosition) { continue; } $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - $projectorState->outdated(); - $toSave[] = $projectorState; + $projectorState->outdated(); // richtige stelle? + $this->projectorStore->saveProjectorState($projectorState); continue; } - $toSave[] = $projectorState; - $handleMethod = $this->resolver->resolveHandleMethod($projector, $message); if ($handleMethod) { - $handleMethod($message); + try { + $handleMethod($message); + } catch (Throwable $e) { + $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); + $logger?->error($e->getMessage()); + $projectorState->error(); + $this->projectorStore->saveProjectorState($projectorState); + + continue; + } } $projectorState->incrementPosition(); + $this->projectorStore->saveProjectorState($projectorState); } - $this->projectorStore->saveProjectorState(...$toSave); - $position++; + $currentPosition++; } } - public function teardown(ProjectorCriteria $criteria = new ProjectorCriteria()): void - { + public function teardown( + ProjectorCriteria $criteria = new ProjectorCriteria(), + ?LoggerInterface $logger = null + ): void { $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Outdated); foreach ($projectorStates as $projectorState) { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - continue; // hmm........................ + $logger?->warning('WARNING!!!'); // todo + + continue; } $dropMethod = $this->resolver->resolveDropMethod($projector); - if (!$dropMethod) { - continue; - } + if ($dropMethod) { + try { + $dropMethod(); + $logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); + } catch (Throwable $e) { + $logger?->error(sprintf('%s drop error', $projectorState->id()->toString())); + $logger?->error($e->getMessage()); + $projectorState->error(); + $this->projectorStore->saveProjectorState($projectorState); - $dropMethod(); + continue; + } + } $this->projectorStore->removeProjectorState($projectorState->id()); } } - public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): void - { + public function remove( + ProjectorCriteria $criteria = new ProjectorCriteria(), + ?LoggerInterface $logger = null + ): void { $projectorStates = $this->projectorStates(); foreach ($projectorStates as $projectorState) { @@ -140,7 +193,13 @@ public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): v $dropMethod = $this->resolver->resolveDropMethod($projector); if ($dropMethod) { - $dropMethod(); + try { + $dropMethod(); + $logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); + } catch (Throwable $e) { + $logger?->warning(sprintf('%s drop error, skipped', $projectorState->id()->toString())); + $logger?->error($e->getMessage()); + } } } @@ -163,15 +222,8 @@ private function projectorStates(): ProjectorStateCollection return $projectorsStates; } - /** - * @return list - */ - public function status(): array + public function status(): ProjectorStateCollection { - return array_values( - iterator_to_array( - $this->projectorStates()->getIterator() - ) - ); + return $this->projectorStates(); } } diff --git a/src/Projection/DefaultProjectorRepository.php b/src/Projection/DefaultProjectorRepository.php index 7fcbc36c..96ca90c1 100644 --- a/src/Projection/DefaultProjectorRepository.php +++ b/src/Projection/DefaultProjectorRepository.php @@ -9,9 +9,6 @@ final class DefaultProjectorRepository implements ProjectorRepository /** @var array|null */ private ?array $projectorIdHashmap = null; - /** @var array|null */ - private ?array $projectorNameHashmap = null; - /** * @param iterable $projectors */ @@ -33,19 +30,6 @@ public function findByProjectorId(ProjectorId $projectorId): ?Projector return $this->projectorIdHashmap[$projectorId->toString()] ?? null; } - public function findByProjectorName(string $name): ?Projector - { - if ($this->projectorNameHashmap === null) { - $this->projectorNameHashmap = []; - - foreach ($this->projectors as $projector) { - $this->projectorNameHashmap[$projector->projectorId()->name()] = $projector; - } - } - - return $this->projectorNameHashmap[$name] ?? null; - } - /** * @return list */ diff --git a/src/Projection/Projectionist.php b/src/Projection/Projectionist.php index 8d107b69..9519557f 100644 --- a/src/Projection/Projectionist.php +++ b/src/Projection/Projectionist.php @@ -4,20 +4,31 @@ namespace Patchlevel\EventSourcing\Projection; -use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; +use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection; +use Psr\Log\LoggerInterface; interface Projectionist { - public function boot(ProjectorCriteria $criteria = new ProjectorCriteria()): void; - - public function run(ProjectorCriteria $criteria = new ProjectorCriteria(), ?int $limit = null): void; - - public function teardown(ProjectorCriteria $criteria = new ProjectorCriteria()): void; - - public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): void; - - /** - * @return list - */ - public function status(): array; + public function boot( + ProjectorCriteria $criteria = new ProjectorCriteria(), + ?LoggerInterface $logger = null + ): void; + + public function run( + ProjectorCriteria $criteria = new ProjectorCriteria(), + ?int $limit = null, + ?LoggerInterface $logger = null + ): void; + + public function teardown( + ProjectorCriteria $criteria = new ProjectorCriteria(), + ?LoggerInterface $logger = null + ): void; + + public function remove( + ProjectorCriteria $criteria = new ProjectorCriteria(), + ?LoggerInterface $logger = null + ): void; + + public function status(): ProjectorStateCollection; } diff --git a/src/Projection/ProjectorCriteria.php b/src/Projection/ProjectorCriteria.php index 938e5be8..2db9e629 100644 --- a/src/Projection/ProjectorCriteria.php +++ b/src/Projection/ProjectorCriteria.php @@ -10,7 +10,7 @@ final class ProjectorCriteria * @param list|null $names */ public function __construct( - public readonly ?array $names = null + public readonly ?array $names = null, ) { } } diff --git a/src/Projection/ProjectorRepository.php b/src/Projection/ProjectorRepository.php index f4ffeade..b1329ba4 100644 --- a/src/Projection/ProjectorRepository.php +++ b/src/Projection/ProjectorRepository.php @@ -8,8 +8,6 @@ interface ProjectorRepository { public function findByProjectorId(ProjectorId $projectorId): ?Projector; - public function findByProjectorName(string $name): ?Projector; - /** * @return list */ diff --git a/src/Projection/ProjectorStatus.php b/src/Projection/ProjectorStatus.php index 11746ac2..6561fb69 100644 --- a/src/Projection/ProjectorStatus.php +++ b/src/Projection/ProjectorStatus.php @@ -6,6 +6,7 @@ enum ProjectorStatus: string { + case New = 'new'; case Booting = 'booting'; case Active = 'active'; case Outdated = 'outdated'; diff --git a/src/Projection/ProjectorStore/InMemory.php b/src/Projection/ProjectorStore/InMemoryStore.php similarity index 95% rename from src/Projection/ProjectorStore/InMemory.php rename to src/Projection/ProjectorStore/InMemoryStore.php index 9907c4cb..9e746220 100644 --- a/src/Projection/ProjectorStore/InMemory.php +++ b/src/Projection/ProjectorStore/InMemoryStore.php @@ -10,7 +10,7 @@ use function array_key_exists; use function array_values; -final class InMemory implements ProjectorStore +final class InMemoryStore implements ProjectorStore { /** @var array */ private array $store = []; diff --git a/src/Projection/ProjectorStore/ProjectorState.php b/src/Projection/ProjectorStore/ProjectorState.php index 80be89df..6381432e 100644 --- a/src/Projection/ProjectorStore/ProjectorState.php +++ b/src/Projection/ProjectorStore/ProjectorState.php @@ -11,7 +11,7 @@ final class ProjectorState { public function __construct( private readonly ProjectorId $id, - private ProjectorStatus $status = ProjectorStatus::Booting, + private ProjectorStatus $status = ProjectorStatus::New, private int $position = 0 ) { } @@ -36,9 +36,14 @@ public function incrementPosition(): void $this->position++; } - public function error(): void + public function booting(): void { - $this->status = ProjectorStatus::Error; + $this->status = ProjectorStatus::Outdated; + } + + public function active(): void + { + $this->status = ProjectorStatus::Active; } public function outdated(): void @@ -46,8 +51,8 @@ public function outdated(): void $this->status = ProjectorStatus::Outdated; } - public function active(): void + public function error(): void { - $this->status = ProjectorStatus::Active; + $this->status = ProjectorStatus::Error; } } diff --git a/src/Projection/ProjectorStore/ProjectorStateCollection.php b/src/Projection/ProjectorStore/ProjectorStateCollection.php index a69ea369..2e04bce3 100644 --- a/src/Projection/ProjectorStore/ProjectorStateCollection.php +++ b/src/Projection/ProjectorStore/ProjectorStateCollection.php @@ -7,6 +7,7 @@ use ArrayIterator; use Countable; use IteratorAggregate; +use Patchlevel\EventSourcing\Projection\ProjectorCriteria; use Patchlevel\EventSourcing\Projection\ProjectorId; use Patchlevel\EventSourcing\Projection\ProjectorStatus; @@ -17,6 +18,7 @@ /** * @implements IteratorAggregate + * @psalm-immutable */ final class ProjectorStateCollection implements Countable, IteratorAggregate { @@ -86,6 +88,28 @@ public function filterByProjectorStatus(ProjectorStatus $status): self return new self(array_values($projectors)); } + public function filterByCriteria(ProjectorCriteria $criteria): self + { + $projectors = array_filter( + $this->projectorStates, + static function (ProjectorState $projectorState) use ($criteria): bool { + if ($criteria->names !== null) { + foreach ($criteria->names as $name) { + if ($projectorState->id()->name() === $name) { + return true; + } + } + + return false; + } + + return true; + } + ); + + return new self(array_values($projectors)); + } + public function count(): int { return count($this->projectorStates); From 8a656d0dc81f356f55c12f4765507d646d11a6b7 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 19:20:00 +0200 Subject: [PATCH 12/26] remove duplicated code --- .../Command/ProjectionistStatusCommand.php | 2 +- src/Projection/DefaultProjectionist.php | 98 ++++++++----------- src/Projection/Projectionist.php | 2 +- 3 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/Console/Command/ProjectionistStatusCommand.php b/src/Console/Command/ProjectionistStatusCommand.php index dd9075c3..b97cb746 100644 --- a/src/Console/Command/ProjectionistStatusCommand.php +++ b/src/Console/Command/ProjectionistStatusCommand.php @@ -21,7 +21,7 @@ final class ProjectionistStatusCommand extends ProjectionistCommand protected function execute(InputInterface $input, OutputInterface $output): int { $io = new OutputStyle($input, $output); - $states = $this->projectionist->status(); + $states = $this->projectionist->projectorStates(); $io->table( [ diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 2e1cb493..76cbde3e 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -4,6 +4,7 @@ namespace Patchlevel\EventSourcing\Projection; +use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStore; @@ -63,29 +64,7 @@ public function boot( foreach ($stream as $message) { foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Booting) as $projectorState) { - $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); - - if (!$projector) { - throw new RuntimeException(); - } - - $handleMethod = $this->resolver->resolveHandleMethod($projector, $message); - - if ($handleMethod) { - try { - $handleMethod($message); - } catch (Throwable $e) { - $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); - $projectorState->error(); - $this->projectorStore->saveProjectorState($projectorState); - - continue; - } - } - - $projectorState->incrementPosition(); - $this->projectorStore->saveProjectorState($projectorState); + $this->handleMessage($message, $projectorState); } } @@ -113,32 +92,7 @@ public function run( continue; } - $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); - - if (!$projector) { - $projectorState->outdated(); // richtige stelle? - $this->projectorStore->saveProjectorState($projectorState); - - continue; - } - - $handleMethod = $this->resolver->resolveHandleMethod($projector, $message); - - if ($handleMethod) { - try { - $handleMethod($message); - } catch (Throwable $e) { - $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); - $projectorState->error(); - $this->projectorStore->saveProjectorState($projectorState); - - continue; - } - } - - $projectorState->incrementPosition(); - $this->projectorStore->saveProjectorState($projectorState); + $this->handleMessage($message, $projectorState); } $currentPosition++; @@ -156,7 +110,6 @@ public function teardown( if (!$projector) { $logger?->warning('WARNING!!!'); // todo - continue; } @@ -207,10 +160,48 @@ public function remove( } } - private function projectorStates(): ProjectorStateCollection + private function handleMessage(Message $message, ProjectorState $projectorState): void + { + $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); + + if (!$projector) { + throw new RuntimeException(); + } + + $handleMethod = $this->resolver->resolveHandleMethod($projector, $message); + + if ($handleMethod) { + try { + $handleMethod($message); + } catch (Throwable $e) { + $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); + $logger?->error($e->getMessage()); + $projectorState->error(); + $this->projectorStore->saveProjectorState($projectorState); + + return; + } + } + + $projectorState->incrementPosition(); + $this->projectorStore->saveProjectorState($projectorState); + } + + public function projectorStates(): ProjectorStateCollection { $projectorsStates = $this->projectorStore->getStateFromAllProjectors(); + foreach ($projectorsStates as $projectorState) { + $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); + + if ($projector) { + continue; + } + + $projectorState->outdated(); + $this->projectorStore->saveProjectorState($projectorState); + } + foreach ($this->projectorRepository->projectors() as $projector) { if ($projectorsStates->has($projector->projectorId())) { continue; @@ -221,9 +212,4 @@ private function projectorStates(): ProjectorStateCollection return $projectorsStates; } - - public function status(): ProjectorStateCollection - { - return $this->projectorStates(); - } } diff --git a/src/Projection/Projectionist.php b/src/Projection/Projectionist.php index 9519557f..27d3cf1a 100644 --- a/src/Projection/Projectionist.php +++ b/src/Projection/Projectionist.php @@ -30,5 +30,5 @@ public function remove( ?LoggerInterface $logger = null ): void; - public function status(): ProjectorStateCollection; + public function projectorStates(): ProjectorStateCollection; } From 4415daac1383d7e3fc2d45e3dfac7aebfba6992c Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 19:33:14 +0200 Subject: [PATCH 13/26] fix status detect --- src/Projection/DefaultProjectionist.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 76cbde3e..8414c9ec 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -194,12 +194,15 @@ public function projectorStates(): ProjectorStateCollection foreach ($projectorsStates as $projectorState) { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); - if ($projector) { - continue; + if ($projector && $projectorState->status() === ProjectorStatus::Outdated) { + $projectorState->active(); + $this->projectorStore->saveProjectorState($projectorState); } - $projectorState->outdated(); - $this->projectorStore->saveProjectorState($projectorState); + if (!$projector && $projectorState->status() !== ProjectorStatus::Outdated) { + $projectorState->outdated(); + $this->projectorStore->saveProjectorState($projectorState); + } } foreach ($this->projectorRepository->projectors() as $projector) { From b9dae5cff5992add15f94f54b22e9b06d6f87abc Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 19:38:54 +0200 Subject: [PATCH 14/26] revert status detect --- src/Projection/DefaultProjectionist.php | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 8414c9ec..0b0c9ec4 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -86,6 +86,15 @@ public function run( $currentPosition = $projectorStates->minProjectorPosition(); $stream = $this->streamableMessageStore->stream($currentPosition); + foreach ($projectorStates as $projectorState) { + $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); + + if (!$projector) { + $projectorState->outdated(); + $this->projectorStore->saveProjectorState($projectorState); + } + } + foreach ($stream as $message) { foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Active) as $projectorState) { if ($projectorState->position() > $currentPosition) { @@ -191,20 +200,6 @@ public function projectorStates(): ProjectorStateCollection { $projectorsStates = $this->projectorStore->getStateFromAllProjectors(); - foreach ($projectorsStates as $projectorState) { - $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); - - if ($projector && $projectorState->status() === ProjectorStatus::Outdated) { - $projectorState->active(); - $this->projectorStore->saveProjectorState($projectorState); - } - - if (!$projector && $projectorState->status() !== ProjectorStatus::Outdated) { - $projectorState->outdated(); - $this->projectorStore->saveProjectorState($projectorState); - } - } - foreach ($this->projectorRepository->projectors() as $projector) { if ($projectorsStates->has($projector->projectorId())) { continue; From 417016cae1661a448413982074cc9faefd1c8e42 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 19:41:43 +0200 Subject: [PATCH 15/26] fix boosting status --- src/Projection/ProjectorStore/ProjectorState.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Projection/ProjectorStore/ProjectorState.php b/src/Projection/ProjectorStore/ProjectorState.php index 6381432e..5486a468 100644 --- a/src/Projection/ProjectorStore/ProjectorState.php +++ b/src/Projection/ProjectorStore/ProjectorState.php @@ -38,7 +38,7 @@ public function incrementPosition(): void public function booting(): void { - $this->status = ProjectorStatus::Outdated; + $this->status = ProjectorStatus::Booting; } public function active(): void From f25d83ee652ef98df597379614f2ef334643162c Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 21 Sep 2022 19:48:07 +0200 Subject: [PATCH 16/26] improve logger information --- src/Console/Command/ProjectionistRemoveCommand.php | 2 +- src/Projection/DefaultProjectionist.php | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Console/Command/ProjectionistRemoveCommand.php b/src/Console/Command/ProjectionistRemoveCommand.php index daff11f2..e5e7616b 100644 --- a/src/Console/Command/ProjectionistRemoveCommand.php +++ b/src/Console/Command/ProjectionistRemoveCommand.php @@ -10,7 +10,7 @@ use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( - 'event-sourcing:projectionist:destroy', + 'event-sourcing:projectionist:remove', 'TODO' )] final class ProjectionistRemoveCommand extends ProjectionistCommand diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 0b0c9ec4..f80f5585 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -64,7 +64,7 @@ public function boot( foreach ($stream as $message) { foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Booting) as $projectorState) { - $this->handleMessage($message, $projectorState); + $this->handleMessage($message, $projectorState, $logger); } } @@ -105,6 +105,8 @@ public function run( } $currentPosition++; + + $logger?->info(sprintf('position: ', $currentPosition)); } } @@ -169,8 +171,11 @@ public function remove( } } - private function handleMessage(Message $message, ProjectorState $projectorState): void - { + private function handleMessage( + Message $message, + ProjectorState $projectorState, + ?LoggerInterface $logger = null + ): void { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { @@ -183,7 +188,7 @@ private function handleMessage(Message $message, ProjectorState $projectorState) try { $handleMethod($message); } catch (Throwable $e) { - $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); + $logger?->error(sprintf('%s message error', $projectorState->id()->toString())); $logger?->error($e->getMessage()); $projectorState->error(); $this->projectorStore->saveProjectorState($projectorState); From b322f496565a598ab018aec8e73db4079478881d Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 22 Sep 2022 15:09:11 +0200 Subject: [PATCH 17/26] add SyncProjectorListener, MetadataProjectorResolver and DefaultProjectorRepository tests --- src/Projection/DefaultProjectorRepository.php | 2 +- .../ProjectorStore/ProjectorState.php | 25 ++++ .../DefaultProjectorRepositoryTest.php | 53 ++++++++ .../MetadataProjectorResolverTest.php | 128 ++++++++++++++++++ .../Projection/SyncProjectorListenerTest.php | 101 ++++++++++++++ 5 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Projection/DefaultProjectorRepositoryTest.php create mode 100644 tests/Unit/Projection/MetadataProjectorResolverTest.php create mode 100644 tests/Unit/Projection/SyncProjectorListenerTest.php diff --git a/src/Projection/DefaultProjectorRepository.php b/src/Projection/DefaultProjectorRepository.php index 96ca90c1..7858e516 100644 --- a/src/Projection/DefaultProjectorRepository.php +++ b/src/Projection/DefaultProjectorRepository.php @@ -13,7 +13,7 @@ final class DefaultProjectorRepository implements ProjectorRepository * @param iterable $projectors */ public function __construct( - private readonly iterable $projectors, + private readonly iterable $projectors = [], ) { } diff --git a/src/Projection/ProjectorStore/ProjectorState.php b/src/Projection/ProjectorStore/ProjectorState.php index 5486a468..d33a6f26 100644 --- a/src/Projection/ProjectorStore/ProjectorState.php +++ b/src/Projection/ProjectorStore/ProjectorState.php @@ -36,23 +36,48 @@ public function incrementPosition(): void $this->position++; } + public function isNew(): bool + { + return $this->status === ProjectorStatus::New; + } + public function booting(): void { $this->status = ProjectorStatus::Booting; } + public function isBooting(): bool + { + return $this->status === ProjectorStatus::Booting; + } + public function active(): void { $this->status = ProjectorStatus::Active; } + public function isActive(): bool + { + return $this->status === ProjectorStatus::Active; + } + public function outdated(): void { $this->status = ProjectorStatus::Outdated; } + public function isOutdated(): bool + { + return $this->status === ProjectorStatus::Outdated; + } + public function error(): void { $this->status = ProjectorStatus::Error; } + + public function isError(): bool + { + return $this->status === ProjectorStatus::Error; + } } diff --git a/tests/Unit/Projection/DefaultProjectorRepositoryTest.php b/tests/Unit/Projection/DefaultProjectorRepositoryTest.php new file mode 100644 index 00000000..f0d8465c --- /dev/null +++ b/tests/Unit/Projection/DefaultProjectorRepositoryTest.php @@ -0,0 +1,53 @@ +projectors()); + } + + public function testGetAllProjectors(): void + { + $projector = new class implements Projector { + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + }; + + $repository = new DefaultProjectorRepository([$projector]); + + self::assertEquals([$projector], $repository->projectors()); + } + + public function testFindProjector(): void + { + $projector = new class implements Projector { + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + }; + + $repository = new DefaultProjectorRepository([$projector]); + + self::assertSame($projector, $repository->findByProjectorId(new ProjectorId('test', 1))); + } + + public function testProjectorNotFound(): void + { + $repository = new DefaultProjectorRepository(); + + self::assertNull($repository->findByProjectorId(new ProjectorId('test', 1))); + } +} \ No newline at end of file diff --git a/tests/Unit/Projection/MetadataProjectorResolverTest.php b/tests/Unit/Projection/MetadataProjectorResolverTest.php new file mode 100644 index 00000000..dce2f9e6 --- /dev/null +++ b/tests/Unit/Projection/MetadataProjectorResolverTest.php @@ -0,0 +1,128 @@ +resolveHandleMethod($projection, $message); + + self::assertIsCallable($result); + + $result($message); + + self::assertSame($message, $projection::$handledMessage); + } + + public function testNotResolveHandleMethod(): void + { + $projection = new class implements Projection {}; + + $message = new Message( + new ProfileVisited( + ProfileId::fromString('1') + ) + ); + + $resolver = new MetadataProjectorResolver(); + $result = $resolver->resolveHandleMethod($projection, $message); + + self::assertNull($result); + } + + public function testResolveCreateMethod(): void + { + $projection = new class implements Projection { + public static bool $called = false; + + #[Create] + public function method(): void + { + self::$called = true; + } + }; + + $resolver = new MetadataProjectorResolver(); + $result = $resolver->resolveCreateMethod($projection); + + self::assertIsCallable($result); + + $result(); + + self::assertTrue($projection::$called); + } + + public function testNotResolveCreateMethod(): void + { + $projection = new class implements Projection {}; + + $resolver = new MetadataProjectorResolver(); + $result = $resolver->resolveCreateMethod($projection); + + self::assertNull($result); + } + + public function testResolveDropMethod(): void + { + $projection = new class implements Projection { + public static bool $called = false; + + #[Drop] + public function method(): void + { + self::$called = true; + } + }; + + $resolver = new MetadataProjectorResolver(); + $result = $resolver->resolveDropMethod($projection); + + self::assertIsCallable($result); + + $result(); + + self::assertTrue($projection::$called); + } + + public function testNotResolveDropMethod(): void + { + $projection = new class implements Projection {}; + + $resolver = new MetadataProjectorResolver(); + $result = $resolver->resolveDropMethod($projection); + + self::assertNull($result); + } +} \ No newline at end of file diff --git a/tests/Unit/Projection/SyncProjectorListenerTest.php b/tests/Unit/Projection/SyncProjectorListenerTest.php new file mode 100644 index 00000000..fbf5ce1c --- /dev/null +++ b/tests/Unit/Projection/SyncProjectorListenerTest.php @@ -0,0 +1,101 @@ +message = $message; + } + }; + + $message = new Message( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('foo@bar.com') + ) + ); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + + $resolver = $this->prophesize(ProjectorResolver::class); + $resolver->resolveHandleMethod($projector, $message)->willReturn($projector->handleProfileCreated(...))->shouldBeCalledOnce(); + + $projectionListener = new SyncProjectorListener( + $projectorRepository->reveal(), + $resolver->reveal() + ); + + $projectionListener($message); + + self::assertSame($message, $projector->message); + } + + public function testNoMethod(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function handleProfileCreated(Message $message): void + { + $this->message = $message; + } + }; + + $message = new Message( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('foo@bar.com') + ) + ); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + + $resolver = $this->prophesize(ProjectorResolver::class); + $resolver->resolveHandleMethod($projector, $message)->willReturn(null)->shouldBeCalledOnce(); + + $projectionListener = new SyncProjectorListener( + $projectorRepository->reveal(), + $resolver->reveal() + ); + + $projectionListener($message); + + self::assertNull($projector->message); + } +} From 43071692476add202b6d02b5ce26a6ef59ab6842 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 28 Sep 2022 11:23:01 +0200 Subject: [PATCH 18/26] refactor, use new schema director --- baseline.xml | 37 ++++- src/Projection/DefaultProjectionist.php | 10 +- .../ProjectorStore/DatabaseStore.php | 130 ++++++++++++++++++ src/Store/DoctrineStore.php | 116 +--------------- src/Store/MultiTableStore.php | 1 - src/Store/SingleTableStore.php | 1 - .../Projectionist/ProjectionistTest.php | 19 ++- .../DefaultProjectorRepositoryTest.php | 6 +- .../MetadataProjectorResolverTest.php | 13 +- .../Projection/SyncProjectorListenerTest.php | 2 +- 10 files changed, 198 insertions(+), 137 deletions(-) create mode 100644 src/Projection/ProjectorStore/DatabaseStore.php diff --git a/baseline.xml b/baseline.xml index 56cd11a9..9f5acaa2 100644 --- a/baseline.xml +++ b/baseline.xml @@ -141,6 +141,16 @@ Projection + + + array_filter + + + toString + toString + toString + + $messages[0]->playhead() - 1 @@ -266,11 +276,6 @@ ProfileProjection - - - new DoctrineSchemaManager() - - $this->prophesize(ProjectionHandler::class) @@ -348,6 +353,12 @@ $this->prophesize(PipelineStore::class) + + + class implements Projector { + class implements Projector { + + new MetadataAwareProjectionHandler([$projection]) @@ -363,12 +374,28 @@ class implements Projection { + + + class implements Projection { + class implements Projection { + class implements Projection { + class implements Projection { + class implements Projection { + class implements Projection { + + $this->prophesize(ProjectionHandler::class) new ProjectionListener($projectionRepository->reveal()) + + + class implements Projector { + class implements Projector { + + new Comparator() diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index f80f5585..d6e58512 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -89,10 +89,12 @@ public function run( foreach ($projectorStates as $projectorState) { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); - if (!$projector) { - $projectorState->outdated(); - $this->projectorStore->saveProjectorState($projectorState); + if ($projector) { + continue; } + + $projectorState->outdated(); + $this->projectorStore->saveProjectorState($projectorState); } foreach ($stream as $message) { @@ -106,7 +108,7 @@ public function run( $currentPosition++; - $logger?->info(sprintf('position: ', $currentPosition)); + $logger?->info(sprintf('position: %s', $currentPosition)); } } diff --git a/src/Projection/ProjectorStore/DatabaseStore.php b/src/Projection/ProjectorStore/DatabaseStore.php new file mode 100644 index 00000000..d880948e --- /dev/null +++ b/src/Projection/ProjectorStore/DatabaseStore.php @@ -0,0 +1,130 @@ +connection->createQueryBuilder() + ->select('*') + ->from($this->projectorTable) + ->where('projector = :projector AND version = :version') + ->getSQL(); + + /** @var array{projector: string, version: int, position: int, status: string}|false $result */ + $result = $this->connection->fetchAssociative($sql, [ + 'projector' => $projectorId->name(), + 'version' => $projectorId->version(), + ]); + + if ($result === false) { + throw new ProjectorStateNotFound(); + } + + return new ProjectorState( + new ProjectorId($result['projector'], $result['version']), + ProjectorStatus::from($result['status']), + $result['position'] + ); + } + + public function getStateFromAllProjectors(): ProjectorStateCollection + { + $sql = $this->connection->createQueryBuilder() + ->select('*') + ->from($this->projectorTable) + ->getSQL(); + + /** @var list $result */ + $result = $this->connection->fetchAllAssociative($sql); + + return new ProjectorStateCollection( + array_map( + static function (array $data) { + return new ProjectorState( + new ProjectorId($data['projector'], $data['version']), + ProjectorStatus::from($data['status']), + $data['position'] + ); + }, + $result + ) + ); + } + + public function saveProjectorState(ProjectorState ...$projectorStates): void + { + $this->connection->transactional( + function (Connection $connection) use ($projectorStates): void { + foreach ($projectorStates as $projectorState) { + try { + $this->getProjectorState($projectorState->id()); + $connection->update( + $this->projectorTable, + [ + 'position' => $projectorState->position(), + 'status' => $projectorState->status()->value, + ], + [ + 'projector' => $projectorState->id()->name(), + 'version' => $projectorState->id()->version(), + ] + ); + } catch (ProjectorStateNotFound) { + $connection->insert( + $this->projectorTable, + [ + 'projector' => $projectorState->id()->name(), + 'version' => $projectorState->id()->version(), + 'position' => $projectorState->position(), + 'status' => $projectorState->status()->value, + ] + ); + } + } + } + ); + } + + public function removeProjectorState(ProjectorId $projectorId): void + { + $this->connection->delete($this->projectorTable, [ + 'projector' => $projectorId->name(), + 'version' => $projectorId->version(), + ]); + } + + public function configureSchema(Schema $schema, Connection $connection): void + { + $table = $schema->createTable($this->projectorTable); + + $table->addColumn('projector', Types::STRING) + ->setNotnull(true); + $table->addColumn('version', Types::INTEGER) + ->setNotnull(true); + $table->addColumn('position', Types::INTEGER) + ->setNotnull(true); + $table->addColumn('status', Types::STRING) + ->setNotnull(true); + + $table->setPrimaryKey(['projector', 'version']); + } +} diff --git a/src/Store/DoctrineStore.php b/src/Store/DoctrineStore.php index dbeee0ae..e6726e68 100644 --- a/src/Store/DoctrineStore.php +++ b/src/Store/DoctrineStore.php @@ -14,12 +14,6 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Schema\SchemaConfigurator; -use Patchlevel\EventSourcing\Projection\ProjectorId; -use Patchlevel\EventSourcing\Projection\ProjectorStatus; -use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState; -use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection; -use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateNotFound; -use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStore; use Patchlevel\EventSourcing\Serializer\EventSerializer; use Patchlevel\EventSourcing\Serializer\SerializedEvent; @@ -28,10 +22,9 @@ use function is_int; use function is_string; -abstract class DoctrineStore implements Store, TransactionStore, OutboxStore, ProjectorStore, SplitEventstreamStore +abstract class DoctrineStore implements Store, TransactionStore, OutboxStore, SplitEventstreamStore { private const OUTBOX_TABLE = 'outbox'; - private const PROJECTOR_TABLE = 'projector'; public function __construct( protected Connection $connection, @@ -178,97 +171,6 @@ public function schema(): Schema return $schema; } - public function getProjectorState(ProjectorId $projectorId): ProjectorState - { - $sql = $this->connection->createQueryBuilder() - ->select('*') - ->from(self::PROJECTOR_TABLE) - ->where('projector = :projector AND version = :version') - ->getSQL(); - - /** @var array{projector: string, version: int, position: int, status: string}|false $result */ - $result = $this->connection->fetchAssociative($sql, [ - 'projector' => $projectorId->name(), - 'version' => $projectorId->version(), - ]); - - if ($result === false) { - throw new ProjectorStateNotFound(); - } - - return new ProjectorState( - new ProjectorId($result['projector'], $result['version']), - ProjectorStatus::from($result['status']), - $result['position'] - ); - } - - public function getStateFromAllProjectors(): ProjectorStateCollection - { - $sql = $this->connection->createQueryBuilder() - ->select('*') - ->from(self::PROJECTOR_TABLE) - ->getSQL(); - - /** @var list $result */ - $result = $this->connection->fetchAllAssociative($sql); - - return new ProjectorStateCollection( - array_map( - static function (array $data) { - return new ProjectorState( - new ProjectorId($data['projector'], $data['version']), - ProjectorStatus::from($data['status']), - $data['position'] - ); - }, - $result - ) - ); - } - - public function saveProjectorState(ProjectorState ...$projectorStates): void - { - $this->connection->transactional( - function (Connection $connection) use ($projectorStates): void { - foreach ($projectorStates as $projectorState) { - try { - $this->getProjectorState($projectorState->id()); - $connection->update( - self::PROJECTOR_TABLE, - [ - 'position' => $projectorState->position(), - 'status' => $projectorState->status()->value, - ], - [ - 'projector' => $projectorState->id()->name(), - 'version' => $projectorState->id()->version(), - ] - ); - } catch (ProjectorStateNotFound) { - $connection->insert( - self::PROJECTOR_TABLE, - [ - 'projector' => $projectorState->id()->name(), - 'version' => $projectorState->id()->version(), - 'position' => $projectorState->position(), - 'status' => $projectorState->status()->value, - ] - ); - } - } - } - ); - } - - public function removeProjectorState(ProjectorId $projectorId): void - { - $this->connection->delete(self::PROJECTOR_TABLE, [ - 'projector' => $projectorId->name(), - 'version' => $projectorId->version(), - ]); - } - protected static function normalizeRecordedOn(string $recordedOn, AbstractPlatform $platform): DateTimeImmutable { $normalizedRecordedOn = Type::getType(Types::DATETIMETZ_IMMUTABLE)->convertToPHPValue($recordedOn, $platform); @@ -329,20 +231,4 @@ protected function addOutboxSchema(Schema $schema): void $table->setPrimaryKey(['aggregate', 'aggregate_id', 'playhead']); } - - protected function addProjectorSchema(Schema $schema): void - { - $table = $schema->createTable(self::PROJECTOR_TABLE); - - $table->addColumn('projector', Types::STRING) - ->setNotnull(true); - $table->addColumn('version', Types::INTEGER) - ->setNotnull(true); - $table->addColumn('position', Types::INTEGER) - ->setNotnull(true); - $table->addColumn('status', Types::STRING) - ->setNotnull(true); - - $table->setPrimaryKey(['projector', 'version']); - } } diff --git a/src/Store/MultiTableStore.php b/src/Store/MultiTableStore.php index 051b7103..f44a9641 100644 --- a/src/Store/MultiTableStore.php +++ b/src/Store/MultiTableStore.php @@ -268,7 +268,6 @@ public function configureSchema(Schema $schema, Connection $connection): void } $this->addOutboxSchema($schema); - $this->addProjectorSchema($schema); } private function addMetaTableToSchema(Schema $schema): void diff --git a/src/Store/SingleTableStore.php b/src/Store/SingleTableStore.php index a63f8cfb..ffb2d0a3 100644 --- a/src/Store/SingleTableStore.php +++ b/src/Store/SingleTableStore.php @@ -260,6 +260,5 @@ public function configureSchema(Schema $schema, Connection $connection): void $table->addIndex(['aggregate', 'aggregate_id', 'playhead', 'archived']); $this->addOutboxSchema($schema); - $this->addProjectorSchema($schema); } } diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index 7497d1d0..dbbcf2a0 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -10,8 +10,10 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; use Patchlevel\EventSourcing\Projection\DefaultProjectionist; use Patchlevel\EventSourcing\Projection\DefaultProjectorRepository; +use Patchlevel\EventSourcing\Projection\ProjectorStore\DatabaseStore; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; -use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; +use Patchlevel\EventSourcing\Schema\ChainSchemaConfigurator; +use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\SingleTableStore; use Patchlevel\EventSourcing\Tests\Integration\DbalManager; @@ -45,6 +47,8 @@ public function testSuccessful(): void 'eventstore' ); + $projectorStore = new DatabaseStore($this->connection); + $manager = new DefaultRepositoryManager( new AggregateRootRegistry(['profile' => Profile::class]), $store, @@ -53,15 +57,22 @@ public function testSuccessful(): void $repository = $manager->get(Profile::class); - // create tables - (new DoctrineSchemaManager())->create($store); + $schemaDirector = new DoctrineSchemaDirector( + $this->connection, + new ChainSchemaConfigurator([ + $store, + $projectorStore, + ]) + ); + + $schemaDirector->create(); $profile = Profile::create(ProfileId::fromString('1'), 'John'); $repository->save($profile); $projectionist = new DefaultProjectionist( $store, - $store, + $projectorStore, new DefaultProjectorRepository( [new ProfileProjection($this->connection)] ), diff --git a/tests/Unit/Projection/DefaultProjectorRepositoryTest.php b/tests/Unit/Projection/DefaultProjectorRepositoryTest.php index f0d8465c..a5c4b95c 100644 --- a/tests/Unit/Projection/DefaultProjectorRepositoryTest.php +++ b/tests/Unit/Projection/DefaultProjectorRepositoryTest.php @@ -1,5 +1,7 @@ findByProjectorId(new ProjectorId('test', 1))); } -} \ No newline at end of file +} diff --git a/tests/Unit/Projection/MetadataProjectorResolverTest.php b/tests/Unit/Projection/MetadataProjectorResolverTest.php index dce2f9e6..15fd4133 100644 --- a/tests/Unit/Projection/MetadataProjectorResolverTest.php +++ b/tests/Unit/Projection/MetadataProjectorResolverTest.php @@ -1,5 +1,7 @@ resolveCreateMethod($projection); @@ -118,11 +122,12 @@ public function method(): void public function testNotResolveDropMethod(): void { - $projection = new class implements Projection {}; + $projection = new class implements Projection { + }; $resolver = new MetadataProjectorResolver(); $result = $resolver->resolveDropMethod($projection); self::assertNull($result); } -} \ No newline at end of file +} diff --git a/tests/Unit/Projection/SyncProjectorListenerTest.php b/tests/Unit/Projection/SyncProjectorListenerTest.php index fbf5ce1c..6931a5b5 100644 --- a/tests/Unit/Projection/SyncProjectorListenerTest.php +++ b/tests/Unit/Projection/SyncProjectorListenerTest.php @@ -24,7 +24,7 @@ final class SyncProjectorListenerTest extends TestCase public function testMethodHandle(): void { $projector = new class implements Projector { - public Message $message; + public ?Message $message = null; public function projectorId(): ProjectorId { From 920415865876b9e6e6b3d65c75eb92bd4dca90e0 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 28 Sep 2022 14:03:08 +0200 Subject: [PATCH 19/26] add more tests --- src/Projection/DefaultProjectionist.php | 7 ++ .../ProjectorStore/DatabaseStore.php | 2 +- .../ProjectorStore/InMemoryStore.php | 3 +- .../ProjectorStore/ProjectorStateNotFound.php | 7 ++ .../Unit/Projection/ProjectorCriteriaTest.php | 21 ++++++ tests/Unit/Projection/ProjectorIdTest.php | 24 +++++++ .../ProjectorStore/InMemoryStoreTest.php | 65 +++++++++++++++++++ 7 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Projection/ProjectorCriteriaTest.php create mode 100644 tests/Unit/Projection/ProjectorIdTest.php create mode 100644 tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index d6e58512..b776b415 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -97,6 +97,8 @@ public function run( $this->projectorStore->saveProjectorState($projectorState); } + $messageCounter = 0; + foreach ($stream as $message) { foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Active) as $projectorState) { if ($projectorState->position() > $currentPosition) { @@ -109,6 +111,11 @@ public function run( $currentPosition++; $logger?->info(sprintf('position: %s', $currentPosition)); + + $messageCounter++; + if ($messageCounter >= $limit) { + return; + } } } diff --git a/src/Projection/ProjectorStore/DatabaseStore.php b/src/Projection/ProjectorStore/DatabaseStore.php index d880948e..a1c987d8 100644 --- a/src/Projection/ProjectorStore/DatabaseStore.php +++ b/src/Projection/ProjectorStore/DatabaseStore.php @@ -36,7 +36,7 @@ public function getProjectorState(ProjectorId $projectorId): ProjectorState ]); if ($result === false) { - throw new ProjectorStateNotFound(); + throw new ProjectorStateNotFound($projectorId); } return new ProjectorState( diff --git a/src/Projection/ProjectorStore/InMemoryStore.php b/src/Projection/ProjectorStore/InMemoryStore.php index 9e746220..3095a7d0 100644 --- a/src/Projection/ProjectorStore/InMemoryStore.php +++ b/src/Projection/ProjectorStore/InMemoryStore.php @@ -5,7 +5,6 @@ namespace Patchlevel\EventSourcing\Projection\ProjectorStore; use Patchlevel\EventSourcing\Projection\ProjectorId; -use RuntimeException; use function array_key_exists; use function array_values; @@ -21,7 +20,7 @@ public function getProjectorState(ProjectorId $projectorId): ProjectorState return $this->store[$projectorId->toString()]; } - throw new RuntimeException(); // todo + throw new ProjectorStateNotFound($projectorId); } public function getStateFromAllProjectors(): ProjectorStateCollection diff --git a/src/Projection/ProjectorStore/ProjectorStateNotFound.php b/src/Projection/ProjectorStore/ProjectorStateNotFound.php index 1469e7c5..4c3c31ae 100644 --- a/src/Projection/ProjectorStore/ProjectorStateNotFound.php +++ b/src/Projection/ProjectorStore/ProjectorStateNotFound.php @@ -4,8 +4,15 @@ namespace Patchlevel\EventSourcing\Projection\ProjectorStore; +use Patchlevel\EventSourcing\Projection\ProjectorId; use RuntimeException; +use function sprintf; + final class ProjectorStateNotFound extends RuntimeException { + public function __construct(ProjectorId $projectorId) + { + parent::__construct(sprintf('projector state with the id "%s" not found', $projectorId->toString())); + } } diff --git a/tests/Unit/Projection/ProjectorCriteriaTest.php b/tests/Unit/Projection/ProjectorCriteriaTest.php new file mode 100644 index 00000000..35d124f6 --- /dev/null +++ b/tests/Unit/Projection/ProjectorCriteriaTest.php @@ -0,0 +1,21 @@ +names); + } +} diff --git a/tests/Unit/Projection/ProjectorIdTest.php b/tests/Unit/Projection/ProjectorIdTest.php new file mode 100644 index 00000000..1dcad7a2 --- /dev/null +++ b/tests/Unit/Projection/ProjectorIdTest.php @@ -0,0 +1,24 @@ +name()); + self::assertSame(1, $projectorId->version()); + self::assertSame('test-1', $projectorId->toString()); + } +} diff --git a/tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php b/tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php new file mode 100644 index 00000000..904a62cc --- /dev/null +++ b/tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php @@ -0,0 +1,65 @@ +saveProjectorState($state); + + self::assertEquals($state, $store->getProjectorState($id)); + + $collection = $store->getStateFromAllProjectors(); + + self::assertTrue($collection->has($id)); + } + + public function testNotFound(): void + { + $this->expectException(ProjectorStateNotFound::class); + + $store = new InMemoryStore(); + $store->getProjectorState(new ProjectorId('test', 1)); + } + + public function testRemove(): void + { + $store = new InMemoryStore(); + + $id = new ProjectorId('test', 1); + + $state = new ProjectorState( + $id + ); + + $store->saveProjectorState($state); + + $collection = $store->getStateFromAllProjectors(); + + self::assertTrue($collection->has($id)); + + $store->removeProjectorState($id); + + $collection = $store->getStateFromAllProjectors(); + + self::assertFalse($collection->has($id)); + } +} From 839971c2a3c35d7a4efbc45ac5841b8180c75735 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 28 Sep 2022 14:46:44 +0200 Subject: [PATCH 20/26] add more tests --- src/Projection/ProjectorCriteria.php | 4 +- src/Projection/ProjectorId.php | 20 ++ .../ProjectorStore/DuplicateProjectorId.php | 18 ++ .../ProjectorStateCollection.php | 26 ++- .../Unit/Projection/ProjectorCriteriaTest.php | 7 +- tests/Unit/Projection/ProjectorIdTest.php | 27 +++ .../ProjectorStateCollectionTest.php | 190 ++++++++++++++++++ .../ProjectorStore/ProjectorStateTest.php | 107 ++++++++++ 8 files changed, 386 insertions(+), 13 deletions(-) create mode 100644 src/Projection/ProjectorStore/DuplicateProjectorId.php create mode 100644 tests/Unit/Projection/ProjectorStore/ProjectorStateCollectionTest.php create mode 100644 tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php diff --git a/src/Projection/ProjectorCriteria.php b/src/Projection/ProjectorCriteria.php index 2db9e629..3ad25025 100644 --- a/src/Projection/ProjectorCriteria.php +++ b/src/Projection/ProjectorCriteria.php @@ -7,10 +7,10 @@ final class ProjectorCriteria { /** - * @param list|null $names + * @param list|null $ids */ public function __construct( - public readonly ?array $names = null, + public readonly ?array $ids = null, ) { } } diff --git a/src/Projection/ProjectorId.php b/src/Projection/ProjectorId.php index 354b8bc4..a31296eb 100644 --- a/src/Projection/ProjectorId.php +++ b/src/Projection/ProjectorId.php @@ -4,6 +4,10 @@ namespace Patchlevel\EventSourcing\Projection; +use RuntimeException; + +use function count; +use function explode; use function sprintf; final class ProjectorId @@ -28,4 +32,20 @@ public function version(): int { return $this->version; } + + public function equals(self $other): bool + { + return $this->name === $other->name && $this->version === $other->version; + } + + public static function fromString(string $value): self + { + $parts = explode('-', $value); // todo regex! + + if (count($parts) !== 2) { + throw new RuntimeException(); + } + + return new self($parts[0], (int)$parts[1]); + } } diff --git a/src/Projection/ProjectorStore/DuplicateProjectorId.php b/src/Projection/ProjectorStore/DuplicateProjectorId.php new file mode 100644 index 00000000..5f29f05f --- /dev/null +++ b/src/Projection/ProjectorStore/DuplicateProjectorId.php @@ -0,0 +1,18 @@ +toString())); + } +} diff --git a/src/Projection/ProjectorStore/ProjectorStateCollection.php b/src/Projection/ProjectorStore/ProjectorStateCollection.php index 2e04bce3..b66d243f 100644 --- a/src/Projection/ProjectorStore/ProjectorStateCollection.php +++ b/src/Projection/ProjectorStore/ProjectorStateCollection.php @@ -33,6 +33,10 @@ public function __construct(array $projectorStates = []) $result = []; foreach ($projectorStates as $projectorState) { + if (array_key_exists($projectorState->id()->toString(), $result)) { + throw new DuplicateProjectorId($projectorState->id()); + } + $result[$projectorState->id()->toString()] = $projectorState; } @@ -42,7 +46,7 @@ public function __construct(array $projectorStates = []) public function get(ProjectorId $projectorId): ProjectorState { if (!$this->has($projectorId)) { - throw new ProjectorStateNotFound(); + throw new ProjectorStateNotFound($projectorId); } return $this->projectorStates[$projectorId->toString()]; @@ -53,29 +57,33 @@ public function has(ProjectorId $projectorId): bool return array_key_exists($projectorId->toString(), $this->projectorStates); } - public function add(ProjectorState $information): self + public function add(ProjectorState $state): self { + if ($this->has($state->id())) { + throw new DuplicateProjectorId($state->id()); + } + return new self( [ ...array_values($this->projectorStates), - $information, + $state, ] ); } public function minProjectorPosition(): int { - $min = 0; + $min = null; foreach ($this->projectorStates as $projectorState) { - if ($projectorState->position() >= $min) { + if ($min !== null && $projectorState->position() >= $min) { continue; } $min = $projectorState->position(); } - return $min; + return $min ?: 0; } public function filterByProjectorStatus(ProjectorStatus $status): self @@ -93,9 +101,9 @@ public function filterByCriteria(ProjectorCriteria $criteria): self $projectors = array_filter( $this->projectorStates, static function (ProjectorState $projectorState) use ($criteria): bool { - if ($criteria->names !== null) { - foreach ($criteria->names as $name) { - if ($projectorState->id()->name() === $name) { + if ($criteria->ids !== null) { + foreach ($criteria->ids as $id) { + if ($projectorState->id()->equals($id)) { return true; } } diff --git a/tests/Unit/Projection/ProjectorCriteriaTest.php b/tests/Unit/Projection/ProjectorCriteriaTest.php index 35d124f6..1418a117 100644 --- a/tests/Unit/Projection/ProjectorCriteriaTest.php +++ b/tests/Unit/Projection/ProjectorCriteriaTest.php @@ -5,6 +5,7 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection; use Patchlevel\EventSourcing\Projection\ProjectorCriteria; +use Patchlevel\EventSourcing\Projection\ProjectorId; use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\ProjectorCriteria */ @@ -12,10 +13,12 @@ class ProjectorCriteriaTest extends TestCase { public function testProjectorId(): void { + $id = new ProjectorId('test', 1); + $projectorId = new ProjectorCriteria( - ['test'], + [$id], ); - self::assertEquals(['test'], $projectorId->names); + self::assertEquals([$id], $projectorId->ids); } } diff --git a/tests/Unit/Projection/ProjectorIdTest.php b/tests/Unit/Projection/ProjectorIdTest.php index 1dcad7a2..1eac8a3f 100644 --- a/tests/Unit/Projection/ProjectorIdTest.php +++ b/tests/Unit/Projection/ProjectorIdTest.php @@ -21,4 +21,31 @@ public function testProjectorId(): void self::assertSame(1, $projectorId->version()); self::assertSame('test-1', $projectorId->toString()); } + + public function testEquals(): void + { + $a = new ProjectorId( + 'test', + 1 + ); + + $b = new ProjectorId( + 'test', + 1 + ); + + $c = new ProjectorId( + 'foo', + 1 + ); + + $d = new ProjectorId( + 'test', + 2 + ); + + self::assertTrue($a->equals($b)); + self::assertFalse($a->equals($c)); + self::assertFalse($a->equals($d)); + } } diff --git a/tests/Unit/Projection/ProjectorStore/ProjectorStateCollectionTest.php b/tests/Unit/Projection/ProjectorStore/ProjectorStateCollectionTest.php new file mode 100644 index 00000000..4f05e14d --- /dev/null +++ b/tests/Unit/Projection/ProjectorStore/ProjectorStateCollectionTest.php @@ -0,0 +1,190 @@ +has($id)); + self::assertSame($state, $collection->get($id)); + self::assertSame(1, $collection->count()); + } + + public function testCreateWithDuplicatedId(): void + { + $this->expectException(DuplicateProjectorId::class); + + $id = new ProjectorId('test', 1); + + new ProjectorStateCollection([ + new ProjectorState( + $id + ), + new ProjectorState( + $id + ), + ]); + } + + public function testNotFound(): void + { + $this->expectException(ProjectorStateNotFound::class); + + $collection = new ProjectorStateCollection(); + $collection->get(new ProjectorId('test', 1)); + } + + public function testAdd(): void + { + $id = new ProjectorId('test', 1); + + $state = new ProjectorState( + $id + ); + + $collection = new ProjectorStateCollection(); + $newCollection = $collection->add($state); + + self::assertNotSame($collection, $newCollection); + self::assertTrue($newCollection->has($id)); + self::assertSame($state, $newCollection->get($id)); + } + + public function testAddWithDuplicatedId(): void + { + $this->expectException(DuplicateProjectorId::class); + + $id = new ProjectorId('test', 1); + + (new ProjectorStateCollection()) + ->add(new ProjectorState($id)) + ->add(new ProjectorState($id)); + } + + public function testMinProjectorPosition(): void + { + $collection = new ProjectorStateCollection([ + new ProjectorState( + new ProjectorId('foo', 1), + ProjectorStatus::Active, + 10 + ), + new ProjectorState( + new ProjectorId('bar', 1), + ProjectorStatus::Active, + 5 + ), + new ProjectorState( + new ProjectorId('baz', 1), + ProjectorStatus::Active, + 15 + ), + ]); + + self::assertSame(5, $collection->minProjectorPosition()); + } + + public function testMinProjectorPositionWithEmptyCollection(): void + { + $collection = new ProjectorStateCollection(); + + self::assertSame(0, $collection->minProjectorPosition()); + } + + public function testFilterByProjectStatus(): void + { + $fooId = new ProjectorId('foo', 1); + $barId = new ProjectorId('bar', 1); + + $collection = new ProjectorStateCollection([ + new ProjectorState( + $fooId, + ProjectorStatus::Booting, + ), + new ProjectorState( + $barId, + ProjectorStatus::Active, + ), + ]); + + $newCollection = $collection->filterByProjectorStatus(ProjectorStatus::Active); + + self::assertNotSame($collection, $newCollection); + self::assertFalse($newCollection->has($fooId)); + self::assertTrue($newCollection->has($barId)); + self::assertSame(1, $newCollection->count()); + } + + public function testFilterByCriteriaEmpty(): void + { + $fooId = new ProjectorId('foo', 1); + $barId = new ProjectorId('bar', 1); + + $collection = new ProjectorStateCollection([ + new ProjectorState( + $fooId, + ProjectorStatus::Booting, + ), + new ProjectorState( + $barId, + ProjectorStatus::Active, + ), + ]); + + $criteria = new ProjectorCriteria(); + + $newCollection = $collection->filterByCriteria($criteria); + + self::assertNotSame($collection, $newCollection); + self::assertTrue($newCollection->has($fooId)); + self::assertTrue($newCollection->has($barId)); + self::assertSame(2, $newCollection->count()); + } + + public function testFilterByCriteriaWithIds(): void + { + $fooId = new ProjectorId('foo', 1); + $barId = new ProjectorId('bar', 1); + + $collection = new ProjectorStateCollection([ + new ProjectorState( + $fooId, + ProjectorStatus::Booting, + ), + new ProjectorState( + $barId, + ProjectorStatus::Active, + ), + ]); + + $criteria = new ProjectorCriteria([$fooId]); + + $newCollection = $collection->filterByCriteria($criteria); + + self::assertNotSame($collection, $newCollection); + self::assertTrue($newCollection->has($fooId)); + self::assertFalse($newCollection->has($barId)); + self::assertSame(1, $newCollection->count()); + } +} diff --git a/tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php b/tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php new file mode 100644 index 00000000..c2c5e76e --- /dev/null +++ b/tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php @@ -0,0 +1,107 @@ +id()); + self::assertEquals(ProjectorStatus::New, $state->status()); + self::assertEquals(0, $state->position()); + self::assertTrue($state->isNew()); + self::assertFalse($state->isBooting()); + self::assertFalse($state->isActive()); + self::assertFalse($state->isError()); + self::assertFalse($state->isOutdated()); + } + + public function testBooting(): void + { + $state = new ProjectorState( + new ProjectorId('test', 1) + ); + + $state->booting(); + + self::assertEquals(ProjectorStatus::Booting, $state->status()); + self::assertFalse($state->isNew()); + self::assertTrue($state->isBooting()); + self::assertFalse($state->isActive()); + self::assertFalse($state->isError()); + self::assertFalse($state->isOutdated()); + } + + public function testActive(): void + { + $state = new ProjectorState( + new ProjectorId('test', 1) + ); + + $state->active(); + + self::assertEquals(ProjectorStatus::Active, $state->status()); + self::assertFalse($state->isNew()); + self::assertFalse($state->isBooting()); + self::assertTrue($state->isActive()); + self::assertFalse($state->isError()); + self::assertFalse($state->isOutdated()); + } + + public function testError(): void + { + $state = new ProjectorState( + new ProjectorId('test', 1) + ); + + $state->error(); + + self::assertEquals(ProjectorStatus::Error, $state->status()); + self::assertFalse($state->isNew()); + self::assertFalse($state->isBooting()); + self::assertFalse($state->isActive()); + self::assertTrue($state->isError()); + self::assertFalse($state->isOutdated()); + } + + public function testOutdated(): void + { + $state = new ProjectorState( + new ProjectorId('test', 1) + ); + + $state->outdated(); + + self::assertEquals(ProjectorStatus::Outdated, $state->status()); + self::assertFalse($state->isNew()); + self::assertFalse($state->isBooting()); + self::assertFalse($state->isActive()); + self::assertFalse($state->isError()); + self::assertTrue($state->isOutdated()); + } + + public function testIncrementPosition(): void + { + $state = new ProjectorState( + new ProjectorId('test', 1) + ); + + $state->incrementPosition(); + + self::assertEquals(1, $state->position()); + } +} From 938b87b6c7239c7f8a100206bb5a60937e228e47 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 28 Sep 2022 17:57:45 +0200 Subject: [PATCH 21/26] fix psalm errors --- baseline.xml | 5 ----- src/Projection/ProjectorId.php | 3 +++ tests/Unit/Projection/ProjectorCriteriaTest.php | 2 +- tests/Unit/Projection/ProjectorIdTest.php | 2 +- tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php | 2 +- .../ProjectorStore/ProjectorStateCollectionTest.php | 4 +++- tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/baseline.xml b/baseline.xml index 9f5acaa2..0ff45e17 100644 --- a/baseline.xml +++ b/baseline.xml @@ -145,11 +145,6 @@ array_filter - - toString - toString - toString - diff --git a/src/Projection/ProjectorId.php b/src/Projection/ProjectorId.php index a31296eb..a5bc6fb9 100644 --- a/src/Projection/ProjectorId.php +++ b/src/Projection/ProjectorId.php @@ -10,6 +10,9 @@ use function explode; use function sprintf; +/** + * @psalm-immutable + */ final class ProjectorId { public function __construct( diff --git a/tests/Unit/Projection/ProjectorCriteriaTest.php b/tests/Unit/Projection/ProjectorCriteriaTest.php index 1418a117..48e7e05d 100644 --- a/tests/Unit/Projection/ProjectorCriteriaTest.php +++ b/tests/Unit/Projection/ProjectorCriteriaTest.php @@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\ProjectorCriteria */ -class ProjectorCriteriaTest extends TestCase +final class ProjectorCriteriaTest extends TestCase { public function testProjectorId(): void { diff --git a/tests/Unit/Projection/ProjectorIdTest.php b/tests/Unit/Projection/ProjectorIdTest.php index 1eac8a3f..3609b933 100644 --- a/tests/Unit/Projection/ProjectorIdTest.php +++ b/tests/Unit/Projection/ProjectorIdTest.php @@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\ProjectorId */ -class ProjectorIdTest extends TestCase +final class ProjectorIdTest extends TestCase { public function testProjectorId(): void { diff --git a/tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php b/tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php index 904a62cc..f6320e59 100644 --- a/tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php +++ b/tests/Unit/Projection/ProjectorStore/InMemoryStoreTest.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\ProjectorStore\InMemoryStore */ -class InMemoryStoreTest extends TestCase +final class InMemoryStoreTest extends TestCase { public function testSave(): void { diff --git a/tests/Unit/Projection/ProjectorStore/ProjectorStateCollectionTest.php b/tests/Unit/Projection/ProjectorStore/ProjectorStateCollectionTest.php index 4f05e14d..bac736b8 100644 --- a/tests/Unit/Projection/ProjectorStore/ProjectorStateCollectionTest.php +++ b/tests/Unit/Projection/ProjectorStore/ProjectorStateCollectionTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection */ -class ProjectorStateCollectionTest extends TestCase +final class ProjectorStateCollectionTest extends TestCase { public function testCreate(): void { @@ -52,6 +52,7 @@ public function testNotFound(): void $this->expectException(ProjectorStateNotFound::class); $collection = new ProjectorStateCollection(); + /** @psalm-suppress UnusedMethodCall */ $collection->get(new ProjectorId('test', 1)); } @@ -77,6 +78,7 @@ public function testAddWithDuplicatedId(): void $id = new ProjectorId('test', 1); + /** @psalm-suppress UnusedMethodCall */ (new ProjectorStateCollection()) ->add(new ProjectorState($id)) ->add(new ProjectorState($id)); diff --git a/tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php b/tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php index c2c5e76e..2ac5b9f1 100644 --- a/tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php +++ b/tests/Unit/Projection/ProjectorStore/ProjectorStateTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorState */ -class ProjectorStateTest extends TestCase +final class ProjectorStateTest extends TestCase { public function testCreate(): void { From 528af6423f129ca7f74a5b668e38e103518f46bb Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 28 Sep 2022 18:18:23 +0200 Subject: [PATCH 22/26] add new exceptions --- src/Projection/DefaultProjectionist.php | 7 +++---- src/Projection/ProjectorId.php | 15 --------------- src/Projection/ProjectorNotFound.php | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 19 deletions(-) create mode 100644 src/Projection/ProjectorNotFound.php diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index b776b415..1fe9bfe4 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -10,7 +10,6 @@ use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStore; use Patchlevel\EventSourcing\Store\StreamableStore; use Psr\Log\LoggerInterface; -use RuntimeException; use Throwable; use function sprintf; @@ -37,7 +36,7 @@ public function boot( $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - throw new RuntimeException(); + throw new ProjectorNotFound($projectorState->id()); } $projectorState->booting(); @@ -129,7 +128,7 @@ public function teardown( $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - $logger?->warning('WARNING!!!'); // todo + $logger?->warning(sprintf('projector witt the id "%s" not found', $projectorState->id()->toString())); continue; } @@ -188,7 +187,7 @@ private function handleMessage( $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - throw new RuntimeException(); + throw new ProjectorNotFound($projectorState->id()); } $handleMethod = $this->resolver->resolveHandleMethod($projector, $message); diff --git a/src/Projection/ProjectorId.php b/src/Projection/ProjectorId.php index a5bc6fb9..b687a365 100644 --- a/src/Projection/ProjectorId.php +++ b/src/Projection/ProjectorId.php @@ -4,10 +4,6 @@ namespace Patchlevel\EventSourcing\Projection; -use RuntimeException; - -use function count; -use function explode; use function sprintf; /** @@ -40,15 +36,4 @@ public function equals(self $other): bool { return $this->name === $other->name && $this->version === $other->version; } - - public static function fromString(string $value): self - { - $parts = explode('-', $value); // todo regex! - - if (count($parts) !== 2) { - throw new RuntimeException(); - } - - return new self($parts[0], (int)$parts[1]); - } } diff --git a/src/Projection/ProjectorNotFound.php b/src/Projection/ProjectorNotFound.php new file mode 100644 index 00000000..9f53f3a3 --- /dev/null +++ b/src/Projection/ProjectorNotFound.php @@ -0,0 +1,17 @@ +toString())); + } +} From c12af3afdc82268de08f2e45de93adbd35665c63 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 28 Sep 2022 21:22:25 +0200 Subject: [PATCH 23/26] add descriptions for commands --- src/Console/Command/ProjectionistBootCommand.php | 2 +- src/Console/Command/ProjectionistRemoveCommand.php | 2 +- src/Console/Command/ProjectionistRunCommand.php | 2 +- src/Console/Command/ProjectionistStatusCommand.php | 2 +- src/Console/Command/ProjectionistTeardownCommand.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Console/Command/ProjectionistBootCommand.php b/src/Console/Command/ProjectionistBootCommand.php index 2e47f604..04eb339a 100644 --- a/src/Console/Command/ProjectionistBootCommand.php +++ b/src/Console/Command/ProjectionistBootCommand.php @@ -11,7 +11,7 @@ #[AsCommand( 'event-sourcing:projectionist:boot', - 'TODO' + 'Prepare new projections and catch up with the event store' )] final class ProjectionistBootCommand extends ProjectionistCommand { diff --git a/src/Console/Command/ProjectionistRemoveCommand.php b/src/Console/Command/ProjectionistRemoveCommand.php index e5e7616b..0ad5eb25 100644 --- a/src/Console/Command/ProjectionistRemoveCommand.php +++ b/src/Console/Command/ProjectionistRemoveCommand.php @@ -11,7 +11,7 @@ #[AsCommand( 'event-sourcing:projectionist:remove', - 'TODO' + 'Delete a projection and remove it from the store' )] final class ProjectionistRemoveCommand extends ProjectionistCommand { diff --git a/src/Console/Command/ProjectionistRunCommand.php b/src/Console/Command/ProjectionistRunCommand.php index b99c1ba6..a3ea24c6 100644 --- a/src/Console/Command/ProjectionistRunCommand.php +++ b/src/Console/Command/ProjectionistRunCommand.php @@ -21,7 +21,7 @@ #[AsCommand( 'event-sourcing:projectionist:run', - 'TODO' + 'Run the active projectors' )] final class ProjectionistRunCommand extends ProjectionistCommand { diff --git a/src/Console/Command/ProjectionistStatusCommand.php b/src/Console/Command/ProjectionistStatusCommand.php index b97cb746..0935f698 100644 --- a/src/Console/Command/ProjectionistStatusCommand.php +++ b/src/Console/Command/ProjectionistStatusCommand.php @@ -14,7 +14,7 @@ #[AsCommand( 'event-sourcing:projectionist:status', - 'TODO' + 'View the current status of the projectors' )] final class ProjectionistStatusCommand extends ProjectionistCommand { diff --git a/src/Console/Command/ProjectionistTeardownCommand.php b/src/Console/Command/ProjectionistTeardownCommand.php index f29114e5..8254a7d9 100644 --- a/src/Console/Command/ProjectionistTeardownCommand.php +++ b/src/Console/Command/ProjectionistTeardownCommand.php @@ -11,7 +11,7 @@ #[AsCommand( 'event-sourcing:projectionist:teardown', - 'TODO' + 'Shut down and delete the outdated projections' )] final class ProjectionistTeardownCommand extends ProjectionistCommand { From 6a5496ff8e602886399d202b00c2cdcdcc2b960b Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 29 Sep 2022 10:54:04 +0200 Subject: [PATCH 24/26] typo Co-authored-by: Daniel Badura --- src/Projection/DefaultProjectionist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index 1fe9bfe4..a6809b36 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -128,7 +128,7 @@ public function teardown( $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - $logger?->warning(sprintf('projector witt the id "%s" not found', $projectorState->id()->toString())); + $logger?->warning(sprintf('projector with the id "%s" not found', $projectorState->id()->toString())); continue; } From 2d483c55f509e0b332e0c91d22b0f36bd448f598 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 29 Sep 2022 11:09:06 +0200 Subject: [PATCH 25/26] fix psalm impure issue & rename database store into doctrine store --- baseline.xml | 5 ----- src/Projection/ProjectorCriteria.php | 3 +++ .../ProjectorStore/{DatabaseStore.php => DoctrineStore.php} | 2 +- tests/Integration/Projectionist/ProjectionistTest.php | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) rename src/Projection/ProjectorStore/{DatabaseStore.php => DoctrineStore.php} (98%) diff --git a/baseline.xml b/baseline.xml index 0ff45e17..97584f87 100644 --- a/baseline.xml +++ b/baseline.xml @@ -141,11 +141,6 @@ Projection - - - array_filter - - $messages[0]->playhead() - 1 diff --git a/src/Projection/ProjectorCriteria.php b/src/Projection/ProjectorCriteria.php index 3ad25025..58a67261 100644 --- a/src/Projection/ProjectorCriteria.php +++ b/src/Projection/ProjectorCriteria.php @@ -4,6 +4,9 @@ namespace Patchlevel\EventSourcing\Projection; +/** + * @psalm-immutable + */ final class ProjectorCriteria { /** diff --git a/src/Projection/ProjectorStore/DatabaseStore.php b/src/Projection/ProjectorStore/DoctrineStore.php similarity index 98% rename from src/Projection/ProjectorStore/DatabaseStore.php rename to src/Projection/ProjectorStore/DoctrineStore.php index a1c987d8..ba0d6a1d 100644 --- a/src/Projection/ProjectorStore/DatabaseStore.php +++ b/src/Projection/ProjectorStore/DoctrineStore.php @@ -13,7 +13,7 @@ use function array_map; -final class DatabaseStore implements ProjectorStore, SchemaConfigurator +final class DoctrineStore implements ProjectorStore, SchemaConfigurator { public function __construct( private readonly Connection $connection, diff --git a/tests/Integration/Projectionist/ProjectionistTest.php b/tests/Integration/Projectionist/ProjectionistTest.php index dbbcf2a0..9fc8333e 100644 --- a/tests/Integration/Projectionist/ProjectionistTest.php +++ b/tests/Integration/Projectionist/ProjectionistTest.php @@ -10,7 +10,7 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; use Patchlevel\EventSourcing\Projection\DefaultProjectionist; use Patchlevel\EventSourcing\Projection\DefaultProjectorRepository; -use Patchlevel\EventSourcing\Projection\ProjectorStore\DatabaseStore; +use Patchlevel\EventSourcing\Projection\ProjectorStore\DoctrineStore; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Schema\ChainSchemaConfigurator; use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector; @@ -47,7 +47,7 @@ public function testSuccessful(): void 'eventstore' ); - $projectorStore = new DatabaseStore($this->connection); + $projectorStore = new DoctrineStore($this->connection); $manager = new DefaultRepositoryManager( new AggregateRootRegistry(['profile' => Profile::class]), From 0febbd00f4573d1b5deddb2b355f4ba264fee4de Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 6 Oct 2022 15:33:42 +0200 Subject: [PATCH 26/26] remove logger from projectionist api and add tests --- baseline.xml | 16 +- .../Command/ProjectionistBootCommand.php | 5 +- src/Console/Command/ProjectionistCommand.php | 2 +- .../Command/ProjectionistRemoveCommand.php | 5 +- .../Command/ProjectionistRunCommand.php | 14 +- .../Command/ProjectionistTeardownCommand.php | 5 +- src/Projection/DefaultProjectionist.php | 68 +-- src/Projection/Projectionist.php | 31 +- .../Projection/DefaultProjectionistTest.php | 574 ++++++++++++++++++ tests/Unit/Projection/DummyStore.php | 62 ++ 10 files changed, 703 insertions(+), 79 deletions(-) create mode 100644 tests/Unit/Projection/DefaultProjectionistTest.php create mode 100644 tests/Unit/Projection/DummyStore.php diff --git a/baseline.xml b/baseline.xml index 97584f87..423f2f78 100644 --- a/baseline.xml +++ b/baseline.xml @@ -29,7 +29,8 @@ - + + $messageLimit $sleep @@ -343,6 +344,19 @@ $this->prophesize(PipelineStore::class) + + + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + + class implements Projector { diff --git a/src/Console/Command/ProjectionistBootCommand.php b/src/Console/Command/ProjectionistBootCommand.php index 04eb339a..e6ba3314 100644 --- a/src/Console/Command/ProjectionistBootCommand.php +++ b/src/Console/Command/ProjectionistBootCommand.php @@ -6,7 +6,6 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( @@ -17,10 +16,8 @@ final class ProjectionistBootCommand extends ProjectionistCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $logger = new ConsoleLogger($output); - $criteria = $this->projectorCriteria(); - $this->projectionist->boot($criteria, $logger); + $this->projectionist->boot($criteria); return 0; } diff --git a/src/Console/Command/ProjectionistCommand.php b/src/Console/Command/ProjectionistCommand.php index f0a266d3..5a095a97 100644 --- a/src/Console/Command/ProjectionistCommand.php +++ b/src/Console/Command/ProjectionistCommand.php @@ -11,7 +11,7 @@ abstract class ProjectionistCommand extends Command { public function __construct( - protected readonly Projectionist $projectionist + protected readonly Projectionist $projectionist, ) { parent::__construct(); } diff --git a/src/Console/Command/ProjectionistRemoveCommand.php b/src/Console/Command/ProjectionistRemoveCommand.php index 0ad5eb25..61645c4c 100644 --- a/src/Console/Command/ProjectionistRemoveCommand.php +++ b/src/Console/Command/ProjectionistRemoveCommand.php @@ -6,7 +6,6 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( @@ -17,10 +16,8 @@ final class ProjectionistRemoveCommand extends ProjectionistCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $logger = new ConsoleLogger($output); - $criteria = $this->projectorCriteria(); - $this->projectionist->remove($criteria, $logger); + $this->projectionist->remove($criteria); return 0; } diff --git a/src/Console/Command/ProjectionistRunCommand.php b/src/Console/Command/ProjectionistRunCommand.php index a3ea24c6..023e134d 100644 --- a/src/Console/Command/ProjectionistRunCommand.php +++ b/src/Console/Command/ProjectionistRunCommand.php @@ -65,7 +65,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $runLimit = InputHelper::nullableInt($input->getOption('run-limit')); - $messageLimit = InputHelper::int($input->getOption('message-limit')); + $messageLimit = InputHelper::nullableInt($input->getOption('message-limit')); $memoryLimit = InputHelper::nullableString($input->getOption('memory-limit')); $timeLimit = InputHelper::nullableInt($input->getOption('time-limit')); $sleep = InputHelper::int($input->getOption('sleep')); @@ -85,7 +85,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($memoryLimit) { - $eventDispatcher->addSubscriber(new StopWorkerOnMemoryLimitListener(Bytes::parseFromString($memoryLimit), $logger)); + $eventDispatcher->addSubscriber( + new StopWorkerOnMemoryLimitListener(Bytes::parseFromString($memoryLimit), $logger) + ); } if ($timeLimit) { @@ -96,9 +98,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($timeLimit, $logger)); } + if ($messageLimit !== null && $messageLimit <= 0) { + throw new InvalidArgumentGiven($messageLimit, 'null|positive-int'); + } + $worker = new DefaultWorker( - function () use ($criteria, $messageLimit, $logger): void { - $this->projectionist->run($criteria, $messageLimit, $logger); + function () use ($criteria, $messageLimit): void { + $this->projectionist->run($criteria, $messageLimit); }, $eventDispatcher, $logger diff --git a/src/Console/Command/ProjectionistTeardownCommand.php b/src/Console/Command/ProjectionistTeardownCommand.php index 8254a7d9..7d29f3e6 100644 --- a/src/Console/Command/ProjectionistTeardownCommand.php +++ b/src/Console/Command/ProjectionistTeardownCommand.php @@ -6,7 +6,6 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( @@ -17,10 +16,8 @@ final class ProjectionistTeardownCommand extends ProjectionistCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $logger = new ConsoleLogger($output); - $criteria = $this->projectorCriteria(); - $this->projectionist->teardown($criteria, $logger); + $this->projectionist->teardown($criteria); return 0; } diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index a6809b36..6adadd37 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -21,13 +21,12 @@ public function __construct( private readonly ProjectorStore $projectorStore, private readonly ProjectorRepository $projectorRepository, private readonly ProjectorResolver $resolver = new MetadataProjectorResolver(), + private readonly ?LoggerInterface $logger = null ) { } - public function boot( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void { + public function boot(ProjectorCriteria $criteria = new ProjectorCriteria()): void + { $projectorStates = $this->projectorStates() ->filterByProjectorStatus(ProjectorStatus::New) ->filterByCriteria($criteria); @@ -50,10 +49,10 @@ public function boot( try { $createMethod(); - $logger?->info(sprintf('%s created', $projectorState->id()->toString())); + $this->logger?->info(sprintf('%s created', $projectorState->id()->toString())); } catch (Throwable $e) { - $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); + $this->logger?->error(sprintf('%s create error', $projectorState->id()->toString())); + $this->logger?->error($e->getMessage()); $projectorState->error(); $this->projectorStore->saveProjectorState($projectorState); } @@ -63,21 +62,18 @@ public function boot( foreach ($stream as $message) { foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Booting) as $projectorState) { - $this->handleMessage($message, $projectorState, $logger); + $this->handleMessage($message, $projectorState); } } - foreach ($projectorStates as $projectorState) { + foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Booting) as $projectorState) { $projectorState->active(); $this->projectorStore->saveProjectorState($projectorState); } } - public function run( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?int $limit = null, - ?LoggerInterface $logger = null - ): void { + public function run(ProjectorCriteria $criteria = new ProjectorCriteria(), ?int $limit = null): void + { $projectorStates = $this->projectorStates() ->filterByProjectorStatus(ProjectorStatus::Active) ->filterByCriteria($criteria); @@ -109,7 +105,7 @@ public function run( $currentPosition++; - $logger?->info(sprintf('position: %s', $currentPosition)); + $this->logger?->info(sprintf('position: %s', $currentPosition)); $messageCounter++; if ($messageCounter >= $limit) { @@ -118,17 +114,17 @@ public function run( } } - public function teardown( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void { + public function teardown(ProjectorCriteria $criteria = new ProjectorCriteria()): void + { $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Outdated); foreach ($projectorStates as $projectorState) { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - $logger?->warning(sprintf('projector with the id "%s" not found', $projectorState->id()->toString())); + $this->logger?->warning( + sprintf('projector with the id "%s" not found', $projectorState->id()->toString()) + ); continue; } @@ -137,13 +133,10 @@ public function teardown( if ($dropMethod) { try { $dropMethod(); - $logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); + $this->logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); } catch (Throwable $e) { - $logger?->error(sprintf('%s drop error', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); - $projectorState->error(); - $this->projectorStore->saveProjectorState($projectorState); - + $this->logger?->error(sprintf('%s drop error', $projectorState->id()->toString())); + $this->logger?->error($e->getMessage()); continue; } } @@ -152,10 +145,8 @@ public function teardown( } } - public function remove( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void { + public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): void + { $projectorStates = $this->projectorStates(); foreach ($projectorStates as $projectorState) { @@ -167,10 +158,10 @@ public function remove( if ($dropMethod) { try { $dropMethod(); - $logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); + $this->logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); } catch (Throwable $e) { - $logger?->warning(sprintf('%s drop error, skipped', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); + $this->logger?->warning(sprintf('%s drop error, skipped', $projectorState->id()->toString())); + $this->logger?->error($e->getMessage()); } } } @@ -179,11 +170,8 @@ public function remove( } } - private function handleMessage( - Message $message, - ProjectorState $projectorState, - ?LoggerInterface $logger = null - ): void { + private function handleMessage(Message $message, ProjectorState $projectorState): void + { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { @@ -196,8 +184,8 @@ private function handleMessage( try { $handleMethod($message); } catch (Throwable $e) { - $logger?->error(sprintf('%s message error', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); + $this->logger?->error(sprintf('%s message error', $projectorState->id()->toString())); + $this->logger?->error($e->getMessage()); $projectorState->error(); $this->projectorStore->saveProjectorState($projectorState); diff --git a/src/Projection/Projectionist.php b/src/Projection/Projectionist.php index 27d3cf1a..4aa72242 100644 --- a/src/Projection/Projectionist.php +++ b/src/Projection/Projectionist.php @@ -5,30 +5,19 @@ namespace Patchlevel\EventSourcing\Projection; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection; -use Psr\Log\LoggerInterface; interface Projectionist { - public function boot( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void; - - public function run( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?int $limit = null, - ?LoggerInterface $logger = null - ): void; - - public function teardown( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void; - - public function remove( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void; + public function boot(ProjectorCriteria $criteria = new ProjectorCriteria()): void; + + /** + * @param positive-int $limit + */ + public function run(ProjectorCriteria $criteria = new ProjectorCriteria(), ?int $limit = null): void; + + public function teardown(ProjectorCriteria $criteria = new ProjectorCriteria()): void; + + public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): void; public function projectorStates(): ProjectorStateCollection; } diff --git a/tests/Unit/Projection/DefaultProjectionistTest.php b/tests/Unit/Projection/DefaultProjectionistTest.php new file mode 100644 index 00000000..aa696c89 --- /dev/null +++ b/tests/Unit/Projection/DefaultProjectionistTest.php @@ -0,0 +1,574 @@ +prophesize(StreamableStore::class); + $streamableStore->stream()->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorStore = $this->prophesize(ProjectorStore::class); + $projectorStore->getStateFromAllProjectors()->willReturn($projectorStateCollection)->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore->reveal(), + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->boot(); + } + + public function testBootWithoutCreateMethod(): void + { + $projector = new class implements Projector { + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + }; + + $projectorStore = new DummyStore([ + new ProjectorState($projector->projectorId()), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream()->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 2 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->boot(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting), + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting, 1), + new ProjectorState($projector->projectorId(), ProjectorStatus::Active, 1), + ], $projectorStore->savedStates); + } + + public function testBootWithMethods(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $created = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function create(): void + { + $this->created = true; + } + + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $projectorStore = new DummyStore(); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream()->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 2 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveCreateMethod($projector)->willReturn($projector->create(...)); + $projectorResolver->resolveHandleMethod($projector, $message)->willReturn($projector->handle(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->boot(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting), + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting, 1), + new ProjectorState($projector->projectorId(), ProjectorStatus::Active, 1), + ], $projectorStore->savedStates); + + self::assertTrue($projector->created); + self::assertSame($message, $projector->message); + } + + public function testBootWithCreateError(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $created = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function create(): void + { + throw new RuntimeException('ERROR'); + } + }; + + $projectorStore = new DummyStore([ + new ProjectorState($projector->projectorId()), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream()->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 1 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveCreateMethod($projector)->willReturn($projector->create(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->boot(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting), + new ProjectorState($projector->projectorId(), ProjectorStatus::Error), + ], $projectorStore->savedStates); + } + + public function testRunning(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Active)]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream(0)->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 2 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveHandleMethod($projector, $message)->willReturn($projector->handle(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->run(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Active, 1), + ], $projectorStore->savedStates); + + self::assertSame($message, $projector->message); + } + + public function testRunningWithError(): void + { + $projector = new class implements Projector { + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function handle(Message $message): void + { + throw new RuntimeException('ERROR'); + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Active)]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream(0)->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 2 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveHandleMethod($projector, $message)->willReturn($projector->handle(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->run(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Error, 0), + ], $projectorStore->savedStates); + } + + public function testRunningMarkOutdated(): void + { + $projectorId = new ProjectorId('test', 1); + + $projectorStore = new DummyStore([new ProjectorState($projectorId, ProjectorStatus::Active)]); + + $generatorFactory = static function (): Generator { + yield from []; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream(0)->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projectorId)->willReturn(null)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->run(); + + self::assertEquals([ + new ProjectorState($projectorId, ProjectorStatus::Outdated, 0), + ], $projectorStore->savedStates); + } + + public function testTeardownWithProjector(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $dropped = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function drop(): void + { + $this->dropped = true; + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->teardown(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([$projector->projectorId()], $projectorStore->removedIds); + self::assertTrue($projector->dropped); + } + + public function testTeardownWithProjectorAndError(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $dropped = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function drop(): void + { + throw new RuntimeException('ERROR'); + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->teardown(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([], $projectorStore->removedIds); + } + + public function testTeardownWithoutProjector(): void + { + $projectorId = new ProjectorId('test', 1); + + $projectorStore = new DummyStore([new ProjectorState($projectorId, ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projectorId)->willReturn(null)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->teardown(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([], $projectorStore->removedIds); + } + + public function testRemoveWithProjector(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $dropped = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function drop(): void + { + $this->dropped = true; + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->remove(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([$projector->projectorId()], $projectorStore->removedIds); + self::assertTrue($projector->dropped); + } + + public function testRemoveWithProjectorAndError(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $dropped = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function drop(): void + { + throw new RuntimeException('ERROR'); + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->remove(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([$projector->projectorId()], $projectorStore->removedIds); + } + + public function testRemoveWithoutProjector(): void + { + $projectorId = new ProjectorId('test', 1); + + $projectorStore = new DummyStore([new ProjectorState($projectorId, ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projectorId)->willReturn(null)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->remove(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([$projectorId], $projectorStore->removedIds); + } +} diff --git a/tests/Unit/Projection/DummyStore.php b/tests/Unit/Projection/DummyStore.php new file mode 100644 index 00000000..31b64d46 --- /dev/null +++ b/tests/Unit/Projection/DummyStore.php @@ -0,0 +1,62 @@ + */ + private array $store = []; + /** @var list */ + public array $savedStates = []; + /** @var list */ + public array $removedIds = []; + + /** + * @param list $store + */ + public function __construct(array $store = []) + { + foreach ($store as $state) { + $this->store[$state->id()->toString()] = $state; + } + } + + public function getProjectorState(ProjectorId $projectorId): ProjectorState + { + if (array_key_exists($projectorId->toString(), $this->store)) { + return $this->store[$projectorId->toString()]; + } + + throw new ProjectorStateNotFound($projectorId); + } + + public function getStateFromAllProjectors(): ProjectorStateCollection + { + return new ProjectorStateCollection(array_values($this->store)); + } + + public function saveProjectorState(ProjectorState ...$projectorStates): void + { + foreach ($projectorStates as $state) { + $this->store[$state->id()->toString()] = $state; + $this->savedStates[] = clone $state; + } + } + + public function removeProjectorState(ProjectorId $projectorId): void + { + $this->removedIds[] = $projectorId; + unset($this->store[$projectorId->toString()]); + } +}