From 077816ae1034b91ee510fc1c73a3257ecec754fd Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 23 Dec 2023 18:39:42 +0100 Subject: [PATCH 1/4] use attribute to define projection target for dx --- docs/pages/getting_started.md | 11 ++---- docs/pages/projection.md | 12 ++---- src/Attribute/Projection.php | 27 ++++++++++++++ src/Projection/Projector/BasicProjector.php | 37 +++++++++++++++++++ .../Projector/ProjectionAttributeNotFound.php | 20 ++++++++++ .../Projection/ProfileProjector.php | 12 ++---- .../Projection/ProfileProjection.php | 12 ++---- .../Projector/BasicProjectorTest.php | 37 +++++++++++++++++++ 8 files changed, 137 insertions(+), 31 deletions(-) create mode 100644 src/Attribute/Projection.php create mode 100644 src/Projection/Projector/BasicProjector.php create mode 100644 src/Projection/Projector/ProjectionAttributeNotFound.php create mode 100644 tests/Unit/Projection/Projector/BasicProjectorTest.php diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index 34392f30b..cd96eff98 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -157,22 +157,19 @@ use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; +use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; -final class HotelProjection implements Projector +#[Projection('hotel', 1)] +final class HotelProjection extends BasicProjector { public function __construct( private readonly Connection $db ) { } - public function targetProjection(): ProjectionId - { - return new ProjectionId('hotel', 1); - } - /** * @return list */ diff --git a/docs/pages/projection.md b/docs/pages/projection.md index 61065ef84..888dddd37 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -18,22 +18,18 @@ use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; +use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; -final class ProfileProjection implements Projector +#[Projection('profile', 1)] +final class ProfileProjector extends BasicProjector { public function __construct( private readonly Connection $connection ) { } - public function targetProjection(): ProjectionId - { - return new ProjectionId('profile', 1) - } - /** * @return list */ diff --git a/src/Attribute/Projection.php b/src/Attribute/Projection.php new file mode 100644 index 000000000..647894e19 --- /dev/null +++ b/src/Attribute/Projection.php @@ -0,0 +1,27 @@ +name; + } + + public function version(): int + { + return $this->version; + } +} diff --git a/src/Projection/Projector/BasicProjector.php b/src/Projection/Projector/BasicProjector.php new file mode 100644 index 000000000..82aa8ff7c --- /dev/null +++ b/src/Projection/Projector/BasicProjector.php @@ -0,0 +1,37 @@ +projectionId) { + return $this->projectionId; + } + + $reflection = new ReflectionClass($this); + $attributes = $reflection->getAttributes(Projection::class); + + if ($attributes === []) { + throw new ProjectionAttributeNotFound($reflection->getName()); + } + + $attribute = $attributes[0]->newInstance(); + + $this->projectionId = new ProjectionId( + $attribute->name(), + $attribute->version(), + ); + + return $this->projectionId; + } +} diff --git a/src/Projection/Projector/ProjectionAttributeNotFound.php b/src/Projection/Projector/ProjectionAttributeNotFound.php new file mode 100644 index 000000000..3f76eb52d --- /dev/null +++ b/src/Projection/Projector/ProjectionAttributeNotFound.php @@ -0,0 +1,20 @@ +targetProjection(), + ); + } + + public function testMissingProjectionId(): void + { + $this->expectException(ProjectionAttributeNotFound::class); + + $projector = new class extends BasicProjector { + }; + + $projector->targetProjection(); + } +} From 6a16649c511caf1f86a95ae7bc4e8de12f080d8f Mon Sep 17 00:00:00 2001 From: David Badura Date: Sun, 24 Dec 2023 11:18:54 +0100 Subject: [PATCH 2/4] update docs about projections --- docs/pages/getting_started.md | 31 +++++++---- docs/pages/projection.md | 100 ++++++++++++++++++++++++---------- 2 files changed, 92 insertions(+), 39 deletions(-) diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index cd96eff98..57f8313ad 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -162,8 +162,8 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; -#[Projection('hotel', 1)] -final class HotelProjection extends BasicProjector +#[Projection('hotel')] +final class HotelProjector extends BasicProjector { public function __construct( private readonly Connection $db @@ -175,7 +175,7 @@ final class HotelProjection extends BasicProjector */ public function getHotels(): array { - return $this->db->fetchAllAssociative('SELECT id, name, guests FROM hotel;') + return $this->db->fetchAllAssociative("SELECT id, name, guests FROM ${this->table()};"); } #[Subscribe(HotelCreated::class)] @@ -184,7 +184,7 @@ final class HotelProjection extends BasicProjector $event = $message->event(); $this->db->insert( - 'hotel', + $this->slug(), [ 'id' => $event->hotelId, 'name' => $event->hotelName, @@ -197,7 +197,7 @@ final class HotelProjection extends BasicProjector public function handleGuestIsCheckedIn(Message $message): void { $this->db->executeStatement( - 'UPDATE hotel SET guests = guests + 1 WHERE id = ?;', + "UPDATE ${this->table()} SET guests = guests + 1 WHERE id = ?;", [$message->aggregateId()] ); } @@ -206,7 +206,7 @@ final class HotelProjection extends BasicProjector public function handleGuestIsCheckedOut(Message $message): void { $this->db->executeStatement( - 'UPDATE hotel SET guests = guests - 1 WHERE id = ?;', + "UPDATE ${this->table()} SET guests = guests - 1 WHERE id = ?;", [$message->aggregateId()] ); } @@ -214,13 +214,22 @@ final class HotelProjection extends BasicProjector #[Create] public function create(): void { - $this->db->executeStatement('CREATE TABLE IF NOT EXISTS hotel (id VARCHAR PRIMARY KEY, name VARCHAR, guests INTEGER);'); + $this->db->executeStatement("CREATE TABLE IF NOT EXISTS ${this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR, guests INTEGER);"); } #[Drop] public function drop(): void { - $this->db->executeStatement('DROP TABLE IF EXISTS hotel;'); + $this->db->executeStatement("DROP TABLE IF EXISTS ${this->table()};"); + } + + private function table(): string + { + return sprintf( + 'projection_%s_%s', + $this->targetProjection()->name(), + $this->targetProjection()->version() + ); } } ``` @@ -231,7 +240,7 @@ final class HotelProjection extends BasicProjector ## Processor -In our example we also want to send an email to the head office as soon as a guest is checked in. +In our example we also want to email the head office as soon as a guest is checked in. ```php use Patchlevel\EventSourcing\Attribute\Subscribe; @@ -293,10 +302,10 @@ $store = new DoctrineDbalStore( $aggregateRegistry ); -$hotelProjection = new HotelProjection($connection); +$hotelProjector = new HotelProjector($connection); $projectorRepository = new ProjectorRepository([ - $hotelProjection, + $hotelProjector, ]); $projectionStore = new DoctrineStore($connection); diff --git a/docs/pages/projection.md b/docs/pages/projection.md index 888dddd37..800275170 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -1,8 +1,8 @@ # Projections -With `projections` you can create your data optimized for reading. +With `projections` you can transform your data optimized for reading. projections can be adjusted, deleted or rebuilt at any time. -This is possible because the source of truth remains untouched +This is possible because the event store remains untouched and everything can always be reproduced from the events. A projection can be anything. @@ -22,7 +22,7 @@ use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; -#[Projection('profile', 1)] +#[Projection('profile')] final class ProfileProjector extends BasicProjector { public function __construct( @@ -35,28 +35,21 @@ final class ProfileProjector extends BasicProjector */ public function getProfiles(): array { - return $this->connection->fetchAllAssociative( - sprintf('SELECT id, name FROM %s;', $this->table()) - ); + return $this->connection->fetchAllAssociative("SELECT id, name FROM ${this->table()};"); } #[Create] public function create(): void { $this->connection->executeStatement( - sprintf( - 'CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);', - $this->table() - ) + "CREATE TABLE IF NOT EXISTS ${this->table()} (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);" ); } #[Drop] public function drop(): void { - $this->connection->executeStatement( - sprintf('DROP TABLE IF EXISTS %s;', $this->table()) - ); + $this->connection->executeStatement("DROP TABLE IF EXISTS ${this->table()};"); } #[Subscribe(ProfileCreated::class)] @@ -65,9 +58,9 @@ final class ProfileProjector extends BasicProjector $profileCreated = $message->event(); $this->connection->executeStatement( - sprintf('INSERT INTO %s (`id`, `name`) VALUES(:id, :name);', $this->table()), + "INSERT INTO ${this->table()} (id, name) VALUES(?, ?);", [ - 'id' => $profileCreated->profileId, + 'id' => $profileCreated->profileId->toString(), 'name' => $profileCreated->name ] ); @@ -84,14 +77,18 @@ final class ProfileProjector extends BasicProjector } ``` -Each projector is responsible for a specific projection and version. -In order for us to be able to define this, we have to use the `targetProjection` method to return a ProjectionId. -So that several versions of the projection can exist, -the version of the projection should flow into the table or collection name. +Each projector is responsible for a specific projection and version. +This combination of information results in the so-called `project ID`. +In order for us to be able to define this, we have to use the `Projection` attribute. +In our example, the projection is called "profile" and has the version "0" because we did not specify it. +So that there is no problems with existing projection, +both the name of the projection and the version should be part of the table/collection name. +In our example, we build a `table` helper method, what creates the following string: "projection_profile_0". Projectors can have one `create` and `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. -To do this, you must add either the `Create` or `Drop` attribute to the method. The method name itself doesn't matter. +For this there are the attributes `Create` and `Drop`. The method name itself doesn't matter. +In some cases it may be that no schema has to be created for the projection, +as the target does it automatically, so you can skip this. Otherwise, a projector can subscribe any number of events. In order to say which method is responsible for which event, you need the `Subscribe` attribute. @@ -103,7 +100,7 @@ Several projectors can also listen to the same event. !!! danger - You should not execute any actions with projectors, + You should not execute any actions like commands with projectors, otherwise these will be executed again if you rebuild the projection! !!! tip @@ -111,9 +108,39 @@ Several projectors can also listen to the same event. 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. +## Versioning + +As soon as the structure of a projection changes, the version must be change or increment. +Otherwise the projectionist will not recognize that the projection has changed and will not rebuild it. +To do this, you have to change the version in the `Projection` attribute. + +```php +use Doctrine\DBAL\Connection; +use Patchlevel\EventSourcing\Attribute\Create; +use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Handle; +use Patchlevel\EventSourcing\Attribute\Projection; +use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; + +#[Projection('profile', version: 1)] +final class ProfileProjector extends BasicProjector +{ + // ... +} +``` + +!!! warning + + If you change the version, you must also change the table/collection name. + +!!! tip + + You can also use the `ProjectionId` from `targetProjection` to build the table/collection name. + ## Projector Repository -The projector repository can hold and make available all projectors. +The projector repository is responsible for managing the projectors. ```php use Patchlevel\EventSourcing\Projection\Projector\InMemoryProjectorRepository; @@ -141,8 +168,25 @@ If something breaks, the projectionist marks the individual projections as fault ## Projection Id A projection id consists of a unique name and a version. +It can be defined using the `Projection` attribute. + +```php +use Patchlevel\EventSourcing\Attribute\Projection; + +#[Projection('profile', version: 1)] +final class ProfileProjector extends BasicProjector +{ + // ... +} +``` + As soon as the projection changes, such as the structure or the data, the version of the projection must be incremented. -This tells the projectionist to build an another projection. +This tells the projectionist to build an another projection with this projector. + +!!! note + + Most databases have a limit on the length of the table/collection name. + The limit is usually 64 characters. ## Projection Position @@ -171,7 +215,7 @@ stateDiagram-v2 ### New -A projection gets the status new if there is a projector with an unknown projection id. +A projection gets the status new if there is a projector with an unknown `projection id`. This can happen when either a new projector has been added, the version has changed or the projection has been manually deleted from the projection store. @@ -184,7 +228,7 @@ As soon as the projection is built up to the current status, the status changes ### Active The active status describes the projections currently being actively managed by the projectionist. -These projections have a projector, follow the event stream and are up to date. +These projections have a projector, follow the event stream and should be up-to-date. ### Outdated @@ -193,7 +237,7 @@ that does not have a projector in the source code with a corresponding projectio then this projection is marked as outdated. This happens when either the projector has been deleted or the projection id of a projector has changed. -In the last case there is a new projection. +In the last case there should be a new projection. An outdated projection does not automatically become active again when the projection id exists again. This happens, for example, when an old version was deployed again during a rollback. @@ -220,7 +264,7 @@ In order for the projectionist to be able to do its work, you have to assemble i In order for the projectionist to know the status and position of the projections, they must be saved. -Currently there is only the Doctrine Store. +We can use the `DoctrineStore` for this: ```php use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore; From 3e116c55cbf56bfbe207986d0bbe707e59bcebee Mon Sep 17 00:00:00 2001 From: David Badura Date: Mon, 25 Dec 2023 10:26:37 +0100 Subject: [PATCH 3/4] refactor projector --- baseline.xml | 9 +- phpstan-baseline.neon | 2 +- .../AttributeProjectorMetadataFactory.php | 16 +- .../Projector/ClassIsNotAProjector.php | 23 ++ .../Projector/DuplicateCreateMethod.php | 3 +- .../Projector/DuplicateDropMethod.php | 3 +- .../Projector/DuplicateSubscribeMethod.php | 5 +- src/Metadata/Projector/ProjectorMetadata.php | 2 + .../Projector/ProjectorMetadataFactory.php | 4 +- .../Psr16ProjectorMetadataFactory.php | 3 +- .../Psr6ProjectorMetadataFactory.php | 3 +- src/Pipeline/Target/ProjectorTarget.php | 3 +- .../Projection/Store/ErrorSerializer.php | 6 +- .../Projectionist/DefaultProjectionist.php | 20 +- src/Projection/Projector/BasicProjector.php | 37 --- .../Projector/InMemoryProjectorRepository.php | 4 +- .../Projector/MetadataProjectorResolver.php | 17 +- .../Projector/ProjectionAttributeNotFound.php | 20 -- src/Projection/Projector/Projector.php | 12 - src/Projection/Projector/ProjectorHelper.php | 6 +- .../Projector/ProjectorRepository.php | 2 +- .../Projector/ProjectorResolver.php | 9 +- .../Projection/ProfileProjector.php | 3 +- .../Projection/BankAccountProjection.php | 11 +- .../Projection/ProfileProjection.php | 3 +- .../Outbox/Projection/ProfileProjection.php | 11 +- .../Projection/ProfileProjection.php | 18 +- tests/Unit/Fixture/Dummy2Projection.php | 5 +- tests/Unit/Fixture/DummyProjection.php | 5 +- .../AttributeProjectorMetadataFactoryTest.php | 56 ++--- .../DefaultProjectionistTest.php | 237 ++++++++---------- .../Projector/BasicProjectorTest.php | 37 --- .../InMemoryProjectorRepositoryTest.php | 8 +- .../MetadataProjectorResolverTest.php | 61 ++--- .../Projector/ProjectorHelperTest.php | 19 +- 35 files changed, 275 insertions(+), 408 deletions(-) create mode 100644 src/Metadata/Projector/ClassIsNotAProjector.php delete mode 100644 src/Projection/Projector/BasicProjector.php delete mode 100644 src/Projection/Projector/ProjectionAttributeNotFound.php delete mode 100644 src/Projection/Projector/Projector.php delete mode 100644 tests/Unit/Projection/Projector/BasicProjectorTest.php diff --git a/baseline.xml b/baseline.xml index 6ad294974..1a315c922 100644 --- a/baseline.xml +++ b/baseline.xml @@ -1,5 +1,5 @@ - + new static() @@ -55,6 +55,13 @@ projectors]]> + + + $method + $method + $subscribeMethod + + new WeakMap() diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ad64f2441..b54320150 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6,7 +6,7 @@ parameters: path: src/EventBus/Message.php - - message: "#^Method Patchlevel\\\\EventSourcing\\\\Projection\\\\Projector\\\\InMemoryProjectorRepository\\:\\:projectors\\(\\) should return array\\ but returns array\\\\.$#" + message: "#^Method Patchlevel\\\\EventSourcing\\\\Projection\\\\Projector\\\\InMemoryProjectorRepository\\:\\:projectors\\(\\) should return array\\ but returns array\\\\.$#" count: 1 path: src/Projection/Projector/InMemoryProjectorRepository.php diff --git a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php b/src/Metadata/Projector/AttributeProjectorMetadataFactory.php index 3e0e65294..358028ce2 100644 --- a/src/Metadata/Projector/AttributeProjectorMetadataFactory.php +++ b/src/Metadata/Projector/AttributeProjectorMetadataFactory.php @@ -6,18 +6,18 @@ use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use ReflectionClass; use function array_key_exists; final class AttributeProjectorMetadataFactory implements ProjectorMetadataFactory { - /** @var array, ProjectorMetadata> */ + /** @var array */ private array $projectorMetadata = []; - /** @param class-string $projector */ + /** @param class-string $projector */ public function metadata(string $projector): ProjectorMetadata { if (array_key_exists($projector, $this->projectorMetadata)) { @@ -26,6 +26,14 @@ public function metadata(string $projector): ProjectorMetadata $reflector = new ReflectionClass($projector); + $attributes = $reflector->getAttributes(Projection::class); + + if ($attributes === []) { + throw new ClassIsNotAProjector($projector); + } + + $projection = $attributes[0]->newInstance(); + $methods = $reflector->getMethods(); $subscribeMethods = []; @@ -79,6 +87,8 @@ public function metadata(string $projector): ProjectorMetadata } $metadata = new ProjectorMetadata( + $projection->name(), + $projection->version(), $subscribeMethods, $createMethod, $dropMethod, diff --git a/src/Metadata/Projector/ClassIsNotAProjector.php b/src/Metadata/Projector/ClassIsNotAProjector.php new file mode 100644 index 000000000..feb9276ff --- /dev/null +++ b/src/Metadata/Projector/ClassIsNotAProjector.php @@ -0,0 +1,23 @@ + $projector */ + /** @param class-string $projector */ public function __construct(string $projector, string $fistMethod, string $secondMethod) { parent::__construct( diff --git a/src/Metadata/Projector/DuplicateDropMethod.php b/src/Metadata/Projector/DuplicateDropMethod.php index 33ba479f9..35bb77717 100644 --- a/src/Metadata/Projector/DuplicateDropMethod.php +++ b/src/Metadata/Projector/DuplicateDropMethod.php @@ -5,13 +5,12 @@ namespace Patchlevel\EventSourcing\Metadata\Projector; use Patchlevel\EventSourcing\Metadata\MetadataException; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use function sprintf; final class DuplicateDropMethod extends MetadataException { - /** @param class-string $projector */ + /** @param class-string $projector */ public function __construct(string $projector, string $fistMethod, string $secondMethod) { parent::__construct( diff --git a/src/Metadata/Projector/DuplicateSubscribeMethod.php b/src/Metadata/Projector/DuplicateSubscribeMethod.php index ff72e1a5c..8896c478e 100644 --- a/src/Metadata/Projector/DuplicateSubscribeMethod.php +++ b/src/Metadata/Projector/DuplicateSubscribeMethod.php @@ -5,15 +5,14 @@ namespace Patchlevel\EventSourcing\Metadata\Projector; use Patchlevel\EventSourcing\Metadata\MetadataException; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use function sprintf; final class DuplicateSubscribeMethod extends MetadataException { /** - * @param class-string $projector - * @param class-string $event + * @param class-string $projector + * @param class-string $event */ public function __construct(string $projector, string $event, string $fistMethod, string $secondMethod) { diff --git a/src/Metadata/Projector/ProjectorMetadata.php b/src/Metadata/Projector/ProjectorMetadata.php index c236a4515..11904cfa3 100644 --- a/src/Metadata/Projector/ProjectorMetadata.php +++ b/src/Metadata/Projector/ProjectorMetadata.php @@ -7,6 +7,8 @@ final class ProjectorMetadata { public function __construct( + public readonly string $name, + public readonly int $version, /** @var array */ public readonly array $subscribeMethods = [], public readonly string|null $createMethod = null, diff --git a/src/Metadata/Projector/ProjectorMetadataFactory.php b/src/Metadata/Projector/ProjectorMetadataFactory.php index 445d82616..cc910600f 100644 --- a/src/Metadata/Projector/ProjectorMetadataFactory.php +++ b/src/Metadata/Projector/ProjectorMetadataFactory.php @@ -4,10 +4,8 @@ namespace Patchlevel\EventSourcing\Metadata\Projector; -use Patchlevel\EventSourcing\Projection\Projector\Projector; - interface ProjectorMetadataFactory { - /** @param class-string $projector */ + /** @param class-string $projector */ public function metadata(string $projector): ProjectorMetadata; } diff --git a/src/Metadata/Projector/Psr16ProjectorMetadataFactory.php b/src/Metadata/Projector/Psr16ProjectorMetadataFactory.php index 847bf4199..89650daf3 100644 --- a/src/Metadata/Projector/Psr16ProjectorMetadataFactory.php +++ b/src/Metadata/Projector/Psr16ProjectorMetadataFactory.php @@ -4,7 +4,6 @@ namespace Patchlevel\EventSourcing\Metadata\Projector; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Psr\SimpleCache\CacheInterface; final class Psr16ProjectorMetadataFactory implements ProjectorMetadataFactory @@ -15,7 +14,7 @@ public function __construct( ) { } - /** @param class-string $projector */ + /** @param class-string $projector */ public function metadata(string $projector): ProjectorMetadata { /** @var ?ProjectorMetadata $metadata */ diff --git a/src/Metadata/Projector/Psr6ProjectorMetadataFactory.php b/src/Metadata/Projector/Psr6ProjectorMetadataFactory.php index 33514f076..0146cd24f 100644 --- a/src/Metadata/Projector/Psr6ProjectorMetadataFactory.php +++ b/src/Metadata/Projector/Psr6ProjectorMetadataFactory.php @@ -4,7 +4,6 @@ namespace Patchlevel\EventSourcing\Metadata\Projector; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Psr\Cache\CacheItemPoolInterface; use function assert; @@ -17,7 +16,7 @@ public function __construct( ) { } - /** @param class-string $projector */ + /** @param class-string $projector */ public function metadata(string $projector): ProjectorMetadata { $item = $this->cache->getItem($projector); diff --git a/src/Pipeline/Target/ProjectorTarget.php b/src/Pipeline/Target/ProjectorTarget.php index bdb02b909..bfadd3701 100644 --- a/src/Pipeline/Target/ProjectorTarget.php +++ b/src/Pipeline/Target/ProjectorTarget.php @@ -6,14 +6,13 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorResolver; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Projection\Projector\ProjectorHelper; use Patchlevel\EventSourcing\Projection\Projector\ProjectorResolver; final class ProjectorTarget implements Target { public function __construct( - private readonly Projector $projector, + private readonly object $projector, private readonly ProjectorResolver $projectorResolver = new MetadataProjectorResolver(), ) { } diff --git a/src/Projection/Projection/Store/ErrorSerializer.php b/src/Projection/Projection/Store/ErrorSerializer.php index 6be0749dd..28a74a19c 100644 --- a/src/Projection/Projection/Store/ErrorSerializer.php +++ b/src/Projection/Projection/Store/ErrorSerializer.php @@ -17,7 +17,11 @@ public static function serialize(Throwable|null $error): string|null return null; } - return serialize($error); + try { + return serialize($error); + } catch (Throwable) { + return null; + } } public static function unserialize(string|null $error): Throwable|null diff --git a/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php index 979bf1367..2b3c340c9 100644 --- a/src/Projection/Projectionist/DefaultProjectionist.php +++ b/src/Projection/Projectionist/DefaultProjectionist.php @@ -12,7 +12,6 @@ use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus; use Patchlevel\EventSourcing\Projection\Projection\Store\ProjectionStore; use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorResolver; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Projection\Projector\ProjectorRepository; use Patchlevel\EventSourcing\Projection\Projector\ProjectorResolver; use Patchlevel\EventSourcing\Store\CriteriaBuilder; @@ -24,7 +23,7 @@ final class DefaultProjectionist implements Projectionist { - /** @var array|null */ + /** @var array|null */ private array|null $projectors = null; public function __construct( @@ -354,15 +353,16 @@ public function reactivate(ProjectionCriteria $criteria = new ProjectionCriteria public function projections(): ProjectionCollection { $projections = $this->projectionStore->all(); + $projectors = $this->projectors(); - foreach ($this->projectors() as $projector) { - $targetProjection = $projector->targetProjection(); + foreach ($projectors as $projector) { + $projectionId = $this->projectorResolver->projectionId($projector); - if ($projections->has($targetProjection)) { + if ($projections->has($projectionId)) { continue; } - $projections = $projections->add(new Projection($targetProjection)); + $projections = $projections->add(new Projection($projectionId)); } return $projections; @@ -419,23 +419,23 @@ private function handleMessage(Message $message, Projection $projection, bool $t $this->projectionStore->save($projection); } - private function projector(ProjectionId $projectorId): Projector|null + private function projector(ProjectionId $projectorId): object|null { $projectors = $this->projectors(); return $projectors[$projectorId->toString()] ?? null; } - /** @return array */ + /** @return array */ private function projectors(): array { if ($this->projectors === null) { $this->projectors = []; foreach ($this->projectorRepository->projectors() as $projector) { - $targetProjection = $projector->targetProjection(); + $projectionId = $this->projectorResolver->projectionId($projector); - $this->projectors[$targetProjection->toString()] = $projector; + $this->projectors[$projectionId->toString()] = $projector; } } diff --git a/src/Projection/Projector/BasicProjector.php b/src/Projection/Projector/BasicProjector.php deleted file mode 100644 index 82aa8ff7c..000000000 --- a/src/Projection/Projector/BasicProjector.php +++ /dev/null @@ -1,37 +0,0 @@ -projectionId) { - return $this->projectionId; - } - - $reflection = new ReflectionClass($this); - $attributes = $reflection->getAttributes(Projection::class); - - if ($attributes === []) { - throw new ProjectionAttributeNotFound($reflection->getName()); - } - - $attribute = $attributes[0]->newInstance(); - - $this->projectionId = new ProjectionId( - $attribute->name(), - $attribute->version(), - ); - - return $this->projectionId; - } -} diff --git a/src/Projection/Projector/InMemoryProjectorRepository.php b/src/Projection/Projector/InMemoryProjectorRepository.php index 4643784c2..ef2aac7df 100644 --- a/src/Projection/Projector/InMemoryProjectorRepository.php +++ b/src/Projection/Projector/InMemoryProjectorRepository.php @@ -6,13 +6,13 @@ final class InMemoryProjectorRepository implements ProjectorRepository { - /** @param iterable $projectors */ + /** @param iterable $projectors */ public function __construct( private readonly iterable $projectors = [], ) { } - /** @return list */ + /** @return list */ public function projectors(): array { return [...$this->projectors]; diff --git a/src/Projection/Projector/MetadataProjectorResolver.php b/src/Projection/Projector/MetadataProjectorResolver.php index 477381ad1..6a3678fe6 100644 --- a/src/Projection/Projector/MetadataProjectorResolver.php +++ b/src/Projection/Projector/MetadataProjectorResolver.php @@ -8,6 +8,7 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory; use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadataFactory; +use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; use function array_key_exists; @@ -18,7 +19,7 @@ public function __construct( ) { } - public function resolveCreateMethod(Projector $projector): Closure|null + public function resolveCreateMethod(object $projector): Closure|null { $metadata = $this->metadataFactory->metadata($projector::class); $method = $metadata->createMethod; @@ -30,7 +31,7 @@ public function resolveCreateMethod(Projector $projector): Closure|null return $projector->$method(...); } - public function resolveDropMethod(Projector $projector): Closure|null + public function resolveDropMethod(object $projector): Closure|null { $metadata = $this->metadataFactory->metadata($projector::class); $method = $metadata->dropMethod; @@ -42,7 +43,7 @@ public function resolveDropMethod(Projector $projector): Closure|null return $projector->$method(...); } - public function resolveSubscribeMethod(Projector $projector, Message $message): Closure|null + public function resolveSubscribeMethod(object $projector, Message $message): Closure|null { $event = $message->event(); $metadata = $this->metadataFactory->metadata($projector::class); @@ -55,4 +56,14 @@ public function resolveSubscribeMethod(Projector $projector, Message $message): return $projector->$subscribeMethod(...); } + + public function projectionId(object $projector): ProjectionId + { + $metadata = $this->metadataFactory->metadata($projector::class); + + return new ProjectionId( + $metadata->name, + $metadata->version, + ); + } } diff --git a/src/Projection/Projector/ProjectionAttributeNotFound.php b/src/Projection/Projector/ProjectionAttributeNotFound.php deleted file mode 100644 index 3f76eb52d..000000000 --- a/src/Projection/Projector/ProjectionAttributeNotFound.php +++ /dev/null @@ -1,20 +0,0 @@ -projectorResolver->resolveSubscribeMethod($projector, $message); @@ -26,7 +26,7 @@ public function handleMessage(Message $message, Projector ...$projectors): void } } - public function createProjection(Projector ...$projectors): void + public function createProjection(object ...$projectors): void { foreach ($projectors as $projector) { $createMethod = $this->projectorResolver->resolveCreateMethod($projector); @@ -39,7 +39,7 @@ public function createProjection(Projector ...$projectors): void } } - public function dropProjection(Projector ...$projectors): void + public function dropProjection(object ...$projectors): void { foreach ($projectors as $projector) { $dropMethod = $this->projectorResolver->resolveDropMethod($projector); diff --git a/src/Projection/Projector/ProjectorRepository.php b/src/Projection/Projector/ProjectorRepository.php index 6840f83cf..f6c4c4e45 100644 --- a/src/Projection/Projector/ProjectorRepository.php +++ b/src/Projection/Projector/ProjectorRepository.php @@ -6,6 +6,6 @@ interface ProjectorRepository { - /** @return list */ + /** @return list */ public function projectors(): array; } diff --git a/src/Projection/Projector/ProjectorResolver.php b/src/Projection/Projector/ProjectorResolver.php index bb9906147..83fea7bd2 100644 --- a/src/Projection/Projector/ProjectorResolver.php +++ b/src/Projection/Projector/ProjectorResolver.php @@ -6,12 +6,15 @@ use Closure; use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; interface ProjectorResolver { - public function resolveCreateMethod(Projector $projector): Closure|null; + public function resolveCreateMethod(object $projector): Closure|null; - public function resolveDropMethod(Projector $projector): Closure|null; + public function resolveDropMethod(object $projector): Closure|null; - public function resolveSubscribeMethod(Projector $projector, Message $message): Closure|null; + public function resolveSubscribeMethod(object $projector, Message $message): Closure|null; + + public function projectionId(object $projector): ProjectionId; } diff --git a/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php index e89ebf4f5..0376c17d7 100644 --- a/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php +++ b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php @@ -10,13 +10,12 @@ use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\ProfileCreated; use function assert; #[Projection('dummy', 1)] -final class ProfileProjector extends BasicProjector +final class ProfileProjector { public function __construct( private Connection $connection, diff --git a/tests/Integration/BankAccountSplitStream/Projection/BankAccountProjection.php b/tests/Integration/BankAccountSplitStream/Projection/BankAccountProjection.php index 24fca1cff..50de2991c 100644 --- a/tests/Integration/BankAccountSplitStream/Projection/BankAccountProjection.php +++ b/tests/Integration/BankAccountSplitStream/Projection/BankAccountProjection.php @@ -8,25 +8,20 @@ use Doctrine\DBAL\Schema\Table; use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream\Events\BalanceAdded; use Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream\Events\BankAccountCreated; -final class BankAccountProjection implements Projector +#[Projection('dummy', 1)] +final class BankAccountProjection { public function __construct( private Connection $connection, ) { } - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - #[Create] public function create(): void { diff --git a/tests/Integration/BasicImplementation/Projection/ProfileProjection.php b/tests/Integration/BasicImplementation/Projection/ProfileProjection.php index 60803c680..927c8271a 100644 --- a/tests/Integration/BasicImplementation/Projection/ProfileProjection.php +++ b/tests/Integration/BasicImplementation/Projection/ProfileProjection.php @@ -11,13 +11,12 @@ use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; use function assert; #[Projection('profile', 1)] -final class ProfileProjection extends BasicProjector +final class ProfileProjection { public function __construct( private Connection $connection, diff --git a/tests/Integration/Outbox/Projection/ProfileProjection.php b/tests/Integration/Outbox/Projection/ProfileProjection.php index 5bbdc89de..e4535208b 100644 --- a/tests/Integration/Outbox/Projection/ProfileProjection.php +++ b/tests/Integration/Outbox/Projection/ProfileProjection.php @@ -8,24 +8,19 @@ use Doctrine\DBAL\Schema\Table; use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Tests\Integration\Outbox\Events\ProfileCreated; -final class ProfileProjection implements Projector +#[Projection('dummy', 1)] +final class ProfileProjection { public function __construct( private Connection $connection, ) { } - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - #[Create] public function create(): void { diff --git a/tests/Integration/Projectionist/Projection/ProfileProjection.php b/tests/Integration/Projectionist/Projection/ProfileProjection.php index 02084c979..16230abec 100644 --- a/tests/Integration/Projectionist/Projection/ProfileProjection.php +++ b/tests/Integration/Projectionist/Projection/ProfileProjection.php @@ -8,16 +8,15 @@ use Doctrine\DBAL\Schema\Table; use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; use function assert; -use function sprintf; -final class ProfileProjection implements Projector +#[Projection('profile', 1)] +final class ProfileProjection { public function __construct( private Connection $connection, @@ -59,15 +58,6 @@ public function handleProfileCreated(Message $message): void private function tableName(): string { - return sprintf( - 'projection_%s_%s', - $this->targetProjection()->name(), - $this->targetProjection()->version(), - ); - } - - public function targetProjection(): ProjectionId - { - return new ProjectionId('profile', 1); + return 'projection_profile_1'; } } diff --git a/tests/Unit/Fixture/Dummy2Projection.php b/tests/Unit/Fixture/Dummy2Projection.php index 036a6ccd7..aef8dbb33 100644 --- a/tests/Unit/Fixture/Dummy2Projection.php +++ b/tests/Unit/Fixture/Dummy2Projection.php @@ -6,12 +6,13 @@ use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message as EventMessage; use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; -final class Dummy2Projection implements Projector +#[Projection('dummy2', 1)] +final class Dummy2Projection { public EventMessage|null $handledMessage = null; public bool $createCalled = false; diff --git a/tests/Unit/Fixture/DummyProjection.php b/tests/Unit/Fixture/DummyProjection.php index dcc76b35c..2d28f590b 100644 --- a/tests/Unit/Fixture/DummyProjection.php +++ b/tests/Unit/Fixture/DummyProjection.php @@ -6,12 +6,13 @@ use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message as EventMessage; use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; -final class DummyProjection implements Projector +#[Projection('dummy', 1)] +final class DummyProjection { public EventMessage|null $handledMessage = null; public bool $createCalled = false; diff --git a/tests/Unit/Metadata/Projector/AttributeProjectorMetadataFactoryTest.php b/tests/Unit/Metadata/Projector/AttributeProjectorMetadataFactoryTest.php index bfec4d5e0..ff02a17d3 100644 --- a/tests/Unit/Metadata/Projector/AttributeProjectorMetadataFactoryTest.php +++ b/tests/Unit/Metadata/Projector/AttributeProjectorMetadataFactoryTest.php @@ -6,25 +6,33 @@ use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory; +use Patchlevel\EventSourcing\Metadata\Projector\ClassIsNotAProjector; use Patchlevel\EventSourcing\Metadata\Projector\DuplicateCreateMethod; use Patchlevel\EventSourcing\Metadata\Projector\DuplicateDropMethod; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileVisited; use PHPUnit\Framework\TestCase; final class AttributeProjectorMetadataFactoryTest extends TestCase { + public function testNotAProjection(): void + { + $this->expectException(ClassIsNotAProjector::class); + + $projection = new class { + }; + + $metadataFactory = new AttributeProjectorMetadataFactory(); + $metadataFactory->metadata($projection::class); + } + public function testEmptyProjection(): void { - $projection = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('foo', 1); - } + $projection = new #[Projection('foo', 1)] + class { }; $metadataFactory = new AttributeProjectorMetadataFactory(); @@ -33,16 +41,14 @@ public function targetProjection(): ProjectionId self::assertSame([], $metadata->subscribeMethods); self::assertNull($metadata->createMethod); self::assertNull($metadata->dropMethod); + self::assertSame('foo', $metadata->name); + self::assertSame(1, $metadata->version); } public function testStandardProjection(): void { - $projection = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('foo', 1); - } - + $projection = new #[Projection('foo', 1)] + class { #[Subscribe(ProfileVisited::class)] public function handle(): void { @@ -73,12 +79,8 @@ public function drop(): void public function testMultipleHandlerOnOneMethod(): void { - $projection = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('foo', 1); - } - + $projection = new #[Projection('foo', 1)] + class { #[Subscribe(ProfileVisited::class)] #[Subscribe(ProfileCreated::class)] public function handle(): void @@ -102,12 +104,8 @@ public function testDuplicateCreateAttributeException(): void { $this->expectException(DuplicateCreateMethod::class); - $projection = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('foo', 1); - } - + $projection = new #[Projection('foo', 1)] + class { #[Create] public function create1(): void { @@ -127,12 +125,8 @@ public function testDuplicateDropAttributeException(): void { $this->expectException(DuplicateDropMethod::class); - $projection = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('foo', 1); - } - + $projection = new #[Projection('foo', 1)] + class { #[Drop] public function drop1(): void { diff --git a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php index 50b0a16f9..4c7dbff67 100644 --- a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php +++ b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php @@ -4,6 +4,7 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projectionist; +use Patchlevel\EventSourcing\Attribute\Projection as ProjectionAttribute; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projection\Projection; use Patchlevel\EventSourcing\Projection\Projection\ProjectionCollection; @@ -12,7 +13,6 @@ use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus; use Patchlevel\EventSourcing\Projection\Projection\Store\ProjectionStore; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Projection\Projector\ProjectorRepository; use Patchlevel\EventSourcing\Projection\Projector\ProjectorResolver; use Patchlevel\EventSourcing\Store\ArrayStream; @@ -58,15 +58,13 @@ public function testNothingToBoot(): void public function testBootWithoutCreateMethod(): void { - $projector = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { }; $projectionStore = new DummyStore([ - new Projection($projector->targetProjection()), + new Projection($projectionId), ]); $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); @@ -78,6 +76,9 @@ public function targetProjection(): ProjectionId $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->projectionId($projector)->willReturn($projectionId); + $projectorResolver->resolveCreateMethod($projector)->willReturn(null); + $projectorResolver->resolveSubscribeMethod($projector, $message)->willReturn(null); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -89,23 +90,20 @@ public function targetProjection(): ProjectionId $projectionist->boot(); self::assertEquals([ - new Projection($projector->targetProjection(), ProjectionStatus::Booting), - new Projection($projector->targetProjection(), ProjectionStatus::Booting, 1), - new Projection($projector->targetProjection(), ProjectionStatus::Active, 1), + new Projection($projectionId, ProjectionStatus::Booting), + new Projection($projectionId, ProjectionStatus::Booting, 1), + new Projection($projectionId, ProjectionStatus::Active, 1), ], $projectionStore->savedProjections); } public function testBootWithMethods(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public Message|null $message = null; public bool $created = false; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function create(): void { $this->created = true; @@ -130,6 +128,7 @@ public function handle(Message $message): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveCreateMethod($projector)->willReturn($projector->create(...)); $projectorResolver->resolveSubscribeMethod($projector, $message)->willReturn($projector->handle(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -141,9 +140,9 @@ public function handle(Message $message): void $projectionist->boot(); self::assertEquals([ - new Projection($projector->targetProjection(), ProjectionStatus::Booting), - new Projection($projector->targetProjection(), ProjectionStatus::Booting, 1), - new Projection($projector->targetProjection(), ProjectionStatus::Active, 1), + new Projection($projectionId, ProjectionStatus::Booting), + new Projection($projectionId, ProjectionStatus::Booting, 1), + new Projection($projectionId, ProjectionStatus::Active, 1), ], $projectionStore->savedProjections); self::assertTrue($projector->created); @@ -152,15 +151,12 @@ public function handle(Message $message): void public function testBootWithLimit(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public Message|null $message = null; public bool $created = false; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function create(): void { $this->created = true; @@ -185,6 +181,7 @@ public function handle(Message $message): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveCreateMethod($projector)->willReturn($projector->create(...)); $projectorResolver->resolveSubscribeMethod($projector, $message)->willReturn($projector->handle(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -196,8 +193,8 @@ public function handle(Message $message): void $projectionist->boot(new ProjectionCriteria(), 1); self::assertEquals([ - new Projection($projector->targetProjection(), ProjectionStatus::Booting), - new Projection($projector->targetProjection(), ProjectionStatus::Booting, 1), + new Projection($projectionId, ProjectionStatus::Booting), + new Projection($projectionId, ProjectionStatus::Booting, 1), ], $projectionStore->savedProjections); self::assertTrue($projector->created); @@ -206,17 +203,14 @@ public function handle(Message $message): void public function testBootWithCreateError(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public function __construct( public readonly RuntimeException $exception = new RuntimeException('ERROR'), ) { } - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function create(): void { throw $this->exception; @@ -224,7 +218,7 @@ public function create(): void }; $projectionStore = new DummyStore([ - new Projection($projector->targetProjection()), + new Projection($projectionId), ]); $streamableStore = $this->prophesize(Store::class); @@ -235,6 +229,7 @@ public function create(): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveCreateMethod($projector)->willReturn($projector->create(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -248,11 +243,11 @@ public function create(): void self::assertEquals( [ new Projection( - $projector->targetProjection(), + $projectionId, ProjectionStatus::Booting, ), new Projection( - $projector->targetProjection(), + $projectionId, ProjectionStatus::Error, 0, 'ERROR', @@ -265,21 +260,18 @@ public function create(): void public function testRunning(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public Message|null $message = null; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function handle(Message $message): void { $this->message = $message; } }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Active)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Active)]); $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); @@ -291,6 +283,7 @@ public function handle(Message $message): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveSubscribeMethod($projector, $message)->willReturn($projector->handle(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -302,7 +295,7 @@ public function handle(Message $message): void $projectionist->run(); self::assertEquals([ - new Projection($projector->targetProjection(), ProjectionStatus::Active, 1), + new Projection($projectionId, ProjectionStatus::Active, 1), ], $projectionStore->savedProjections); self::assertSame($message, $projector->message); @@ -310,21 +303,18 @@ public function handle(Message $message): void public function testRunningWithLimit(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public Message|null $message = null; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function handle(Message $message): void { $this->message = $message; } }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Active)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Active)]); $message1 = new Message(new ProfileVisited(ProfileId::fromString('test'))); $message2 = new Message(new ProfileVisited(ProfileId::fromString('test'))); @@ -340,6 +330,7 @@ public function handle(Message $message): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveSubscribeMethod($projector, $message1)->willReturn($projector->handle(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -351,7 +342,7 @@ public function handle(Message $message): void $projectionist->run(new ProjectionCriteria(), 1); self::assertEquals([ - new Projection($projector->targetProjection(), ProjectionStatus::Active, 1), + new Projection($projectionId, ProjectionStatus::Active, 1), ], $projectionStore->savedProjections); self::assertSame($message1, $projector->message); @@ -359,28 +350,22 @@ public function handle(Message $message): void public function testRunningWithSkip(): void { - $projector1 = new class implements Projector { + $projectionId1 = new ProjectionId('test1', 1); + $projector1 = new #[ProjectionAttribute('test1', 1)] + class { public Message|null $message = null; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test1', 1); - } - public function handle(Message $message): void { $this->message = $message; } }; - $projector2 = new class implements Projector { + $projectionId2 = new ProjectionId('test2', 1); + $projector2 = new #[ProjectionAttribute('test1', 1)] + class { public Message|null $message = null; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test2', 1); - } - public function handle(Message $message): void { $this->message = $message; @@ -388,8 +373,8 @@ public function handle(Message $message): void }; $projectionStore = new DummyStore([ - new Projection($projector1->targetProjection(), ProjectionStatus::Active), - new Projection($projector2->targetProjection(), ProjectionStatus::Active, 1), + new Projection($projectionId1, ProjectionStatus::Active), + new Projection($projectionId2, ProjectionStatus::Active, 1), ]); $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); @@ -402,6 +387,8 @@ public function handle(Message $message): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveSubscribeMethod($projector1, $message)->willReturn($projector1->handle(...)); + $projectorResolver->projectionId($projector1)->willReturn($projectionId1); + $projectorResolver->projectionId($projector2)->willReturn($projectionId2); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -413,7 +400,7 @@ public function handle(Message $message): void $projectionist->run(); self::assertEquals([ - new Projection($projector1->targetProjection(), ProjectionStatus::Active, 1), + new Projection($projectionId1, ProjectionStatus::Active, 1), ], $projectionStore->savedProjections); self::assertSame($message, $projector1->message); @@ -422,24 +409,21 @@ public function handle(Message $message): void public function testRunningWithError(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public function __construct( public readonly RuntimeException $exception = new RuntimeException('ERROR'), ) { } - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function handle(Message $message): void { throw $this->exception; } }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Active)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Active)]); $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); @@ -451,6 +435,7 @@ public function handle(Message $message): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveSubscribeMethod($projector, $message)->willReturn($projector->handle(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -464,7 +449,7 @@ public function handle(Message $message): void self::assertEquals( [ new Projection( - $projector->targetProjection(), + $projectionId, ProjectionStatus::Error, 0, 'ERROR', @@ -477,9 +462,9 @@ public function handle(Message $message): void public function testRunningMarkOutdated(): void { - $projectorId = new ProjectionId('test', 1); + $projectionId = new ProjectionId('test', 1); - $projectionStore = new DummyStore([new Projection($projectorId, ProjectionStatus::Active)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Active)]); $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->shouldNotBeCalled(); @@ -499,27 +484,15 @@ public function testRunningMarkOutdated(): void $projectionist->run(); self::assertEquals([ - new Projection($projectorId, ProjectionStatus::Outdated, 0), + new Projection($projectionId, ProjectionStatus::Outdated, 0), ], $projectionStore->savedProjections); } public function testRunningWithoutActiveProjectors(): void { - $projector = new class implements Projector { - public Message|null $message = null; + $projectionId = new ProjectionId('test', 1); - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - - public function handle(Message $message): void - { - $this->message = $message; - } - }; - - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Booting)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Booting)]); $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->shouldNotBeCalled(); @@ -543,22 +516,19 @@ public function handle(Message $message): void public function testTeardownWithProjector(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public Message|null $message = null; public bool $dropped = false; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function drop(): void { $this->dropped = true; } }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Outdated)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Outdated)]); $streamableStore = $this->prophesize(Store::class); @@ -567,6 +537,7 @@ public function drop(): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -578,28 +549,25 @@ public function drop(): void $projectionist->teardown(); self::assertEquals([], $projectionStore->savedProjections); - self::assertEquals([$projector->targetProjection()], $projectionStore->removedProjectionIds); + self::assertEquals([$projectionId], $projectionStore->removedProjectionIds); self::assertTrue($projector->dropped); } public function testTeardownWithProjectorAndError(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public Message|null $message = null; public bool $dropped = false; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function drop(): void { throw new RuntimeException('ERROR'); } }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Outdated)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Outdated)]); $streamableStore = $this->prophesize(Store::class); @@ -608,6 +576,7 @@ public function drop(): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -650,21 +619,18 @@ public function testTeardownWithoutProjector(): void public function testRemoveWithProjector(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public bool $dropped = false; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function drop(): void { $this->dropped = true; } }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Outdated)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Outdated)]); $streamableStore = $this->prophesize(Store::class); @@ -673,6 +639,7 @@ public function drop(): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -684,20 +651,18 @@ public function drop(): void $projectionist->remove(); self::assertEquals([], $projectionStore->savedProjections); - self::assertEquals([$projector->targetProjection()], $projectionStore->removedProjectionIds); + self::assertEquals([$projectionId], $projectionStore->removedProjectionIds); self::assertTrue($projector->dropped); } public function testRemoveWithoutDropMethod(): void { - $projector = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Outdated)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Outdated)]); $streamableStore = $this->prophesize(Store::class); @@ -706,6 +671,7 @@ public function targetProjection(): ProjectionId $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveDropMethod($projector)->willReturn(null); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -717,26 +683,23 @@ public function targetProjection(): ProjectionId $projectionist->remove(); self::assertEquals([], $projectionStore->savedProjections); - self::assertEquals([$projector->targetProjection()], $projectionStore->removedProjectionIds); + self::assertEquals([$projectionId], $projectionStore->removedProjectionIds); } public function testRemoveWithProjectorAndError(): void { - $projector = new class implements Projector { + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { public bool $dropped = false; - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } - public function drop(): void { throw new RuntimeException('ERROR'); } }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Outdated)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Outdated)]); $streamableStore = $this->prophesize(Store::class); @@ -745,6 +708,7 @@ public function drop(): void $projectorResolver = $this->prophesize(ProjectorResolver::class); $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -756,7 +720,7 @@ public function drop(): void $projectionist->remove(); self::assertEquals([], $projectionStore->savedProjections); - self::assertEquals([$projector->targetProjection()], $projectionStore->removedProjectionIds); + self::assertEquals([$projectionId], $projectionStore->removedProjectionIds); } public function testRemoveWithoutProjector(): void @@ -787,14 +751,12 @@ public function testRemoveWithoutProjector(): void public function testReactivate(): void { - $projector = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('test', 1); - } + $projectionId = new ProjectionId('test', 1); + $projector = new #[ProjectionAttribute('test', 1)] + class { }; - $projectionStore = new DummyStore([new Projection($projector->targetProjection(), ProjectionStatus::Error)]); + $projectionStore = new DummyStore([new Projection($projectionId, ProjectionStatus::Error)]); $streamableStore = $this->prophesize(Store::class); @@ -802,6 +764,7 @@ public function targetProjection(): ProjectionId $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->projectionId($projector)->willReturn($projectionId); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -813,7 +776,7 @@ public function targetProjection(): ProjectionId $projectionist->reactivate(); self::assertEquals([ - new Projection($projector->targetProjection(), ProjectionStatus::Active, 0), + new Projection($projectionId, ProjectionStatus::Active, 0), ], $projectionStore->savedProjections); } diff --git a/tests/Unit/Projection/Projector/BasicProjectorTest.php b/tests/Unit/Projection/Projector/BasicProjectorTest.php deleted file mode 100644 index 2e244d455..000000000 --- a/tests/Unit/Projection/Projector/BasicProjectorTest.php +++ /dev/null @@ -1,37 +0,0 @@ -targetProjection(), - ); - } - - public function testMissingProjectionId(): void - { - $this->expectException(ProjectionAttributeNotFound::class); - - $projector = new class extends BasicProjector { - }; - - $projector->targetProjection(); - } -} diff --git a/tests/Unit/Projection/Projector/InMemoryProjectorRepositoryTest.php b/tests/Unit/Projection/Projector/InMemoryProjectorRepositoryTest.php index e2804f5a8..746408b9b 100644 --- a/tests/Unit/Projection/Projector/InMemoryProjectorRepositoryTest.php +++ b/tests/Unit/Projection/Projector/InMemoryProjectorRepositoryTest.php @@ -4,9 +4,7 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projector; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; use Patchlevel\EventSourcing\Projection\Projector\InMemoryProjectorRepository; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\Projector\InMemoryProjectorRepository */ @@ -20,11 +18,7 @@ public function testGetAllProjectorsIsEmpty(): void public function testGetAllProjectors(): void { - $projector = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } + $projector = new class { }; $repository = new InMemoryProjectorRepository([$projector]); diff --git a/tests/Unit/Projection/Projector/MetadataProjectorResolverTest.php b/tests/Unit/Projection/Projector/MetadataProjectorResolverTest.php index ff9416287..b07880c54 100644 --- a/tests/Unit/Projection/Projector/MetadataProjectorResolverTest.php +++ b/tests/Unit/Projection/Projector/MetadataProjectorResolverTest.php @@ -6,11 +6,10 @@ use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorResolver; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileId; @@ -22,14 +21,10 @@ final class MetadataProjectorResolverTest extends TestCase { public function testResolveHandleMethod(): void { - $projection = new class implements Projector { + $projection = new #[Projection('dummy')] + class { public static Message|null $handledMessage = null; - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - #[Subscribe(ProfileCreated::class)] public function handleProfileCreated(Message $message): void { @@ -56,11 +51,8 @@ public function handleProfileCreated(Message $message): void public function testNotResolveHandleMethod(): void { - $projection = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } + $projection = new #[Projection('dummy')] + class { }; $message = new Message( @@ -77,14 +69,10 @@ public function targetProjection(): ProjectionId public function testResolveCreateMethod(): void { - $projection = new class implements Projector { + $projection = new #[Projection('dummy')] + class { public static bool $called = false; - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - #[Create] public function method(): void { @@ -104,11 +92,8 @@ public function method(): void public function testNotResolveCreateMethod(): void { - $projection = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } + $projection = new #[Projection('dummy')] + class { }; $resolver = new MetadataProjectorResolver(); @@ -119,14 +104,10 @@ public function targetProjection(): ProjectionId public function testResolveDropMethod(): void { - $projection = new class implements Projector { + $projection = new #[Projection('dummy')] + class { public static bool $called = false; - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - #[Drop] public function method(): void { @@ -146,11 +127,8 @@ public function method(): void public function testNotResolveDropMethod(): void { - $projection = new class implements Projector { - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } + $projection = new #[Projection('dummy')] + class { }; $resolver = new MetadataProjectorResolver(); @@ -158,4 +136,17 @@ public function targetProjection(): ProjectionId self::assertNull($result); } + + public function testProjectionId(): void + { + $projection = new #[Projection('dummy', 1)] + class { + }; + + $resolver = new MetadataProjectorResolver(); + $result = $resolver->projectionId($projection); + + self::assertEquals('dummy', $result->name()); + self::assertEquals(1, $result->version()); + } } diff --git a/tests/Unit/Projection/Projector/ProjectorHelperTest.php b/tests/Unit/Projection/Projector/ProjectorHelperTest.php index 257bfb23e..01721982b 100644 --- a/tests/Unit/Projection/Projector/ProjectorHelperTest.php +++ b/tests/Unit/Projection/Projector/ProjectorHelperTest.php @@ -6,10 +6,10 @@ use Patchlevel\EventSourcing\Attribute\Create; use Patchlevel\EventSourcing\Attribute\Drop; +use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\Projector; use Patchlevel\EventSourcing\Projection\Projector\ProjectorHelper; use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; @@ -22,14 +22,10 @@ final class ProjectorHelperTest extends TestCase { public function testHandle(): void { - $projector = new class implements Projector { + $projector = new #[Projection('dummy')] + class { public static Message|null $handledMessage = null; - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - #[Subscribe(ProfileCreated::class)] public function handleProfileCreated(Message $message): void { @@ -54,7 +50,8 @@ public function handleProfileCreated(Message $message): void public function testHandleNotSupportedEvent(): void { - $projector = new class implements Projector { + $projector = new #[Projection('dummy')] + class { public static Message|null $handledMessage = null; public function targetProjection(): ProjectionId @@ -85,7 +82,8 @@ public function handleProfileCreated(Message $message): void public function testCreate(): void { - $projector = new class implements Projector { + $projector = new #[Projection('dummy')] + class { public static bool $called = false; public function targetProjection(): ProjectionId @@ -108,7 +106,8 @@ public function method(): void public function testDrop(): void { - $projector = new class implements Projector { + $projector = new #[Projection('dummy')] + class { public static bool $called = false; public function targetProjection(): ProjectionId From 6b0014c9abb755e34e16c120357aaa532fd40a08 Mon Sep 17 00:00:00 2001 From: David Badura Date: Mon, 25 Dec 2023 11:55:22 +0100 Subject: [PATCH 4/4] add projector helper/util to create table/collection name --- docs/pages/getting_started.md | 12 ++- docs/pages/projection.md | 17 ++-- .../Target/ProjectorRepositoryTarget.php | 13 ++- src/Pipeline/Target/ProjectorTarget.php | 11 ++- src/Projection/Projector/ProjectorHelper.php | 45 +++------ src/Projection/Projector/ProjectorUtil.php | 43 ++++++++ .../Projection/ProfileProjection.php | 10 +- .../Projector/ProjectorHelperTest.php | 99 ++----------------- 8 files changed, 108 insertions(+), 142 deletions(-) create mode 100644 src/Projection/Projector/ProjectorUtil.php diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index 57f8313ad..53e5f0fff 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -160,11 +160,13 @@ use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; -use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; +use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; #[Projection('hotel')] -final class HotelProjector extends BasicProjector +final class HotelProjector { + use ProjectorUtil; + public function __construct( private readonly Connection $db ) { @@ -184,7 +186,7 @@ final class HotelProjector extends BasicProjector $event = $message->event(); $this->db->insert( - $this->slug(), + $this->table(), [ 'id' => $event->hotelId, 'name' => $event->hotelName, @@ -227,8 +229,8 @@ final class HotelProjector extends BasicProjector { return sprintf( 'projection_%s_%s', - $this->targetProjection()->name(), - $this->targetProjection()->version() + $this->projectionName(), + $this->projectionVersion() ); } } diff --git a/docs/pages/projection.md b/docs/pages/projection.md index 800275170..c71c6c987 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -20,11 +20,13 @@ use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; +use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; #[Projection('profile')] -final class ProfileProjector extends BasicProjector +final class ProfileProjector { + use ProjectorUtil; + public function __construct( private readonly Connection $connection ) { @@ -70,8 +72,8 @@ final class ProfileProjector extends BasicProjector { return sprintf( 'projection_%s_%s', - $this->targetProjection()->name(), - $this->targetProjection()->version() + $this->projectionName(), + $this->projectionVersion() ); } } @@ -121,10 +123,9 @@ use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Handle; use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\EventBus\Message; -use Patchlevel\EventSourcing\Projection\Projector\BasicProjector; #[Projection('profile', version: 1)] -final class ProfileProjector extends BasicProjector +final class ProfileProjector { // ... } @@ -136,7 +137,7 @@ final class ProfileProjector extends BasicProjector !!! tip - You can also use the `ProjectionId` from `targetProjection` to build the table/collection name. + You can also use the `ProjectorUtil` to build the table/collection name. ## Projector Repository @@ -174,7 +175,7 @@ It can be defined using the `Projection` attribute. use Patchlevel\EventSourcing\Attribute\Projection; #[Projection('profile', version: 1)] -final class ProfileProjector extends BasicProjector +final class ProfileProjector { // ... } diff --git a/src/Pipeline/Target/ProjectorRepositoryTarget.php b/src/Pipeline/Target/ProjectorRepositoryTarget.php index db8f81328..e2a5a73da 100644 --- a/src/Pipeline/Target/ProjectorRepositoryTarget.php +++ b/src/Pipeline/Target/ProjectorRepositoryTarget.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorResolver; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorHelper; use Patchlevel\EventSourcing\Projection\Projector\ProjectorRepository; use Patchlevel\EventSourcing\Projection\Projector\ProjectorResolver; @@ -20,10 +19,18 @@ public function __construct( public function save(Message ...$messages): void { - $helper = new ProjectorHelper($this->projectorResolver); + $projectors = $this->projectorRepository->projectors(); foreach ($messages as $message) { - $helper->handleMessage($message, ...$this->projectorRepository->projectors()); + foreach ($projectors as $projector) { + $subscribeMethod = $this->projectorResolver->resolveSubscribeMethod($projector, $message); + + if (!$subscribeMethod) { + continue; + } + + $subscribeMethod($message); + } } } } diff --git a/src/Pipeline/Target/ProjectorTarget.php b/src/Pipeline/Target/ProjectorTarget.php index bfadd3701..4ed1c2535 100644 --- a/src/Pipeline/Target/ProjectorTarget.php +++ b/src/Pipeline/Target/ProjectorTarget.php @@ -6,7 +6,6 @@ use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projector\MetadataProjectorResolver; -use Patchlevel\EventSourcing\Projection\Projector\ProjectorHelper; use Patchlevel\EventSourcing\Projection\Projector\ProjectorResolver; final class ProjectorTarget implements Target @@ -19,10 +18,14 @@ public function __construct( public function save(Message ...$messages): void { - $helper = new ProjectorHelper($this->projectorResolver); - foreach ($messages as $message) { - $helper->handleMessage($message, $this->projector); + $subscribeMethod = $this->projectorResolver->resolveSubscribeMethod($this->projector, $message); + + if (!$subscribeMethod) { + continue; + } + + $subscribeMethod($message); } } } diff --git a/src/Projection/Projector/ProjectorHelper.php b/src/Projection/Projector/ProjectorHelper.php index d11fcdfc4..47d8f3e9e 100644 --- a/src/Projection/Projector/ProjectorHelper.php +++ b/src/Projection/Projector/ProjectorHelper.php @@ -4,51 +4,36 @@ namespace Patchlevel\EventSourcing\Projection\Projector; -use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Metadata\Projector\AttributeProjectorMetadataFactory; +use Patchlevel\EventSourcing\Metadata\Projector\ProjectorMetadataFactory; +use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; final class ProjectorHelper { public function __construct( - private readonly ProjectorResolver $projectorResolver = new MetadataProjectorResolver(), + private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(), ) { } - public function handleMessage(Message $message, object ...$projectors): void + public function name(object $projector): string { - foreach ($projectors as $projector) { - $subscribeMethod = $this->projectorResolver->resolveSubscribeMethod($projector, $message); + $metadata = $this->metadataFactory->metadata($projector::class); - if (!$subscribeMethod) { - continue; - } - - $subscribeMethod($message); - } + return $metadata->name; } - public function createProjection(object ...$projectors): void + public function version(object $projector): int { - foreach ($projectors as $projector) { - $createMethod = $this->projectorResolver->resolveCreateMethod($projector); - - if (!$createMethod) { - continue; - } + $metadata = $this->metadataFactory->metadata($projector::class); - $createMethod(); - } + return $metadata->version; } - public function dropProjection(object ...$projectors): void + public function projectionId(object $projector): ProjectionId { - foreach ($projectors as $projector) { - $dropMethod = $this->projectorResolver->resolveDropMethod($projector); - - if (!$dropMethod) { - continue; - } - - $dropMethod(); - } + return new ProjectionId( + $this->name($projector), + $this->version($projector), + ); } } diff --git a/src/Projection/Projector/ProjectorUtil.php b/src/Projection/Projector/ProjectorUtil.php new file mode 100644 index 000000000..898563d1a --- /dev/null +++ b/src/Projection/Projector/ProjectorUtil.php @@ -0,0 +1,43 @@ +name($this); + } + + private function projectionVersion(): int + { + return (new ProjectorHelper(self::metadataFactory()))->version($this); + } + + private function projectionId(): ProjectionId + { + return (new ProjectorHelper(self::metadataFactory()))->projectionId($this); + } +} diff --git a/tests/Integration/Projectionist/Projection/ProfileProjection.php b/tests/Integration/Projectionist/Projection/ProfileProjection.php index 16230abec..98a75b1d8 100644 --- a/tests/Integration/Projectionist/Projection/ProfileProjection.php +++ b/tests/Integration/Projectionist/Projection/ProfileProjection.php @@ -11,13 +11,17 @@ use Patchlevel\EventSourcing\Attribute\Projection; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Projection\Projector\ProjectorUtil; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; use function assert; +use function sprintf; #[Projection('profile', 1)] final class ProfileProjection { + use ProjectorUtil; + public function __construct( private Connection $connection, ) { @@ -58,6 +62,10 @@ public function handleProfileCreated(Message $message): void private function tableName(): string { - return 'projection_profile_1'; + return sprintf( + 'projection_%s_%s', + $this->projectionName(), + $this->projectionVersion(), + ); } } diff --git a/tests/Unit/Projection/Projector/ProjectorHelperTest.php b/tests/Unit/Projection/Projector/ProjectorHelperTest.php index 01721982b..51c29ac95 100644 --- a/tests/Unit/Projection/Projector/ProjectorHelperTest.php +++ b/tests/Unit/Projection/Projector/ProjectorHelperTest.php @@ -4,127 +4,44 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projector; -use Patchlevel\EventSourcing\Attribute\Create; -use Patchlevel\EventSourcing\Attribute\Drop; use Patchlevel\EventSourcing\Attribute\Projection; -use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\EventBus\Message; use Patchlevel\EventSourcing\Projection\Projection\ProjectionId; use Patchlevel\EventSourcing\Projection\Projector\ProjectorHelper; -use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email; -use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; -use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileId; -use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileVisited; use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\Projector\ProjectorHelper */ final class ProjectorHelperTest extends TestCase { - public function testHandle(): void + public function testProjectionName(): void { $projector = new #[Projection('dummy')] class { - public static Message|null $handledMessage = null; - - #[Subscribe(ProfileCreated::class)] - public function handleProfileCreated(Message $message): void - { - self::$handledMessage = $message; - } - }; - - $event = new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('profile@test.com'), - ); - - $message = new Message( - $event, - ); - - $helper = new ProjectorHelper(); - $helper->handleMessage($message, $projector); - - self::assertSame($message, $projector::$handledMessage); - } - - public function testHandleNotSupportedEvent(): void - { - $projector = new #[Projection('dummy')] - class { - public static Message|null $handledMessage = null; - - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - - #[Subscribe(ProfileCreated::class)] - public function handleProfileCreated(Message $message): void - { - self::$handledMessage = $message; - } }; - $event = new ProfileVisited( - ProfileId::fromString('1'), - ); - - $message = new Message( - $event, - ); - $helper = new ProjectorHelper(); - $helper->handleMessage($message, $projector); - self::assertNull($projector::$handledMessage); + self::assertSame('dummy', $helper->name($projector)); } - public function testCreate(): void + public function testProjectionVersion(): void { - $projector = new #[Projection('dummy')] + $projector = new #[Projection('dummy', 1)] class { - public static bool $called = false; - - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - - #[Create] - public function method(): void - { - self::$called = true; - } }; $helper = new ProjectorHelper(); - $helper->createProjection($projector); - self::assertTrue($projector::$called); + self::assertSame(1, $helper->version($projector)); } - public function testDrop(): void + public function testProjectionId(): void { - $projector = new #[Projection('dummy')] + $projector = new #[Projection('dummy', 1)] class { - public static bool $called = false; - - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - - #[Drop] - public function method(): void - { - self::$called = true; - } }; $helper = new ProjectorHelper(); - $helper->dropProjection($projector); - self::assertTrue($projector::$called); + self::assertEquals(new ProjectionId('dummy', 1), $helper->projectionId($projector)); } }