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/docs/pages/getting_started.md b/docs/pages/getting_started.md index 34392f30b..53e5f0fff 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -157,28 +157,27 @@ 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\ProjectorUtil; -final class HotelProjection implements Projector +#[Projection('hotel')] +final class HotelProjector { + use ProjectorUtil; + public function __construct( private readonly Connection $db ) { } - public function targetProjection(): ProjectionId - { - return new ProjectionId('hotel', 1); - } - /** * @return list */ 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)] @@ -187,7 +186,7 @@ final class HotelProjection implements Projector $event = $message->event(); $this->db->insert( - 'hotel', + $this->table(), [ 'id' => $event->hotelId, 'name' => $event->hotelName, @@ -200,7 +199,7 @@ final class HotelProjection implements Projector 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()] ); } @@ -209,7 +208,7 @@ final class HotelProjection implements Projector 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()] ); } @@ -217,13 +216,22 @@ final class HotelProjection implements Projector #[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->projectionName(), + $this->projectionVersion() + ); } } ``` @@ -234,7 +242,7 @@ final class HotelProjection implements Projector ## 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; @@ -296,10 +304,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 61065ef84..c71c6c987 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. @@ -18,49 +18,40 @@ 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\ProjectorUtil; -final class ProfileProjection implements Projector +#[Projection('profile')] +final class ProfileProjector { + use ProjectorUtil; + public function __construct( private readonly Connection $connection ) { } - public function targetProjection(): ProjectionId - { - return new ProjectionId('profile', 1) - } - /** * @return list */ 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)] @@ -69,9 +60,9 @@ final class ProfileProjection implements Projector $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 ] ); @@ -81,21 +72,25 @@ final class ProfileProjection implements Projector { return sprintf( 'projection_%s_%s', - $this->targetProjection()->name(), - $this->targetProjection()->version() + $this->projectionName(), + $this->projectionVersion() ); } } ``` -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. @@ -107,7 +102,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 @@ -115,9 +110,38 @@ 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; + +#[Projection('profile', version: 1)] +final class ProfileProjector +{ + // ... +} +``` + +!!! warning + + If you change the version, you must also change the table/collection name. + +!!! tip + + You can also use the `ProjectorUtil` 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; @@ -145,8 +169,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 +{ + // ... +} +``` + 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 @@ -175,7 +216,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. @@ -188,7 +229,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 @@ -197,7 +238,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. @@ -224,7 +265,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; 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/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/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/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 bdb02b909..4ed1c2535 100644 --- a/src/Pipeline/Target/ProjectorTarget.php +++ b/src/Pipeline/Target/ProjectorTarget.php @@ -6,24 +6,26 @@ 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(), ) { } 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/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/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/Projector.php b/src/Projection/Projector/Projector.php deleted file mode 100644 index 2c01830a1..000000000 --- a/src/Projection/Projector/Projector.php +++ /dev/null @@ -1,12 +0,0 @@ -projectorResolver->resolveSubscribeMethod($projector, $message); + $metadata = $this->metadataFactory->metadata($projector::class); - if (!$subscribeMethod) { - continue; - } - - $subscribeMethod($message); - } + return $metadata->name; } - public function createProjection(Projector ...$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(Projector ...$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/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/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/Benchmark/BasicImplementation/Projection/ProfileProjector.php b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php index 4f73768fc..0376c17d7 100644 --- a/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php +++ b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php @@ -7,26 +7,21 @@ use Doctrine\DBAL\Connection; 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\Benchmark\BasicImplementation\Events\ProfileCreated; use function assert; -final class ProfileProjector implements Projector +#[Projection('dummy', 1)] +final class ProfileProjector { 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/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 c421ff722..927c8271a 100644 --- a/tests/Integration/BasicImplementation/Projection/ProfileProjection.php +++ b/tests/Integration/BasicImplementation/Projection/ProfileProjection.php @@ -8,26 +8,21 @@ 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\BasicImplementation\Events\ProfileCreated; use function assert; -final class ProfileProjection implements Projector +#[Projection('profile', 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/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..98a75b1d8 100644 --- a/tests/Integration/Projectionist/Projection/ProfileProjection.php +++ b/tests/Integration/Projectionist/Projection/ProfileProjection.php @@ -8,17 +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\Projection\Projector\ProjectorUtil; 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 { + use ProjectorUtil; + public function __construct( private Connection $connection, ) { @@ -61,13 +64,8 @@ private function tableName(): string { return sprintf( 'projection_%s_%s', - $this->targetProjection()->name(), - $this->targetProjection()->version(), + $this->projectionName(), + $this->projectionVersion(), ); } - - public function targetProjection(): ProjectionId - { - return new ProjectionId('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/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..51c29ac95 100644 --- a/tests/Unit/Projection/Projector/ProjectorHelperTest.php +++ b/tests/Unit/Projection/Projector/ProjectorHelperTest.php @@ -4,128 +4,44 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projector; -use Patchlevel\EventSourcing\Attribute\Create; -use Patchlevel\EventSourcing\Attribute\Drop; -use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\EventBus\Message; +use Patchlevel\EventSourcing\Attribute\Projection; 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; -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 class implements Projector { - 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; - } + $projector = new #[Projection('dummy')] + class { }; - $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); + self::assertSame('dummy', $helper->name($projector)); } - public function testHandleNotSupportedEvent(): void + public function testProjectionVersion(): void { - $projector = new class implements Projector { - 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; - } + $projector = new #[Projection('dummy', 1)] + class { }; - $event = new ProfileVisited( - ProfileId::fromString('1'), - ); - - $message = new Message( - $event, - ); - $helper = new ProjectorHelper(); - $helper->handleMessage($message, $projector); - self::assertNull($projector::$handledMessage); + self::assertSame(1, $helper->version($projector)); } - public function testCreate(): void + public function testProjectionId(): void { - $projector = new class implements Projector { - 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); - } - - public function testDrop(): void - { - $projector = new class implements Projector { - public static bool $called = false; - - public function targetProjection(): ProjectionId - { - return new ProjectionId('dummy', 1); - } - - #[Drop] - public function method(): void - { - self::$called = true; - } + $projector = new #[Projection('dummy', 1)] + class { }; $helper = new ProjectorHelper(); - $helper->dropProjection($projector); - self::assertTrue($projector::$called); + self::assertEquals(new ProjectionId('dummy', 1), $helper->projectionId($projector)); } }