From b681fcb1c0fdd5bf8f269fe319478a1624a8dc29 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 12 Apr 2024 12:57:49 +0200 Subject: [PATCH] improve aggregate id argument resolver --- docs/pages/getting_started.md | 22 ++--- docs/pages/subscription.md | 91 +++++++++++++++++++ .../AggregateIdArgumentResolver.php | 15 ++- .../Projection/ProfileProjector.php | 5 +- .../Subscriber/ErrorProducerSubscriber.php | 3 +- .../Subscriber/ProfileNewProjection.php | 9 +- .../Subscriber/ProfileProcessor.php | 9 +- .../Subscriber/ProfileProjection.php | 9 +- .../AggregateIdArgumentResolverTest.php | 14 +-- 9 files changed, 126 insertions(+), 51 deletions(-) diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index 63e4eb9c..ff492aea 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -157,12 +157,11 @@ Each projector is then responsible for a specific projection. ```php use Doctrine\DBAL\Connection; -use Patchlevel\EventSourcing\Aggregate\AggregateHeader; +use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; #[Projector('hotel')] @@ -182,14 +181,12 @@ final class HotelProjector } #[Subscribe(HotelCreated::class)] - public function handleHotelCreated(Message $message): void + public function handleHotelCreated(HotelCreated $event, Uuid $aggregateId): void { - $event = $message->event(); - $this->db->insert( $this->table(), [ - 'id' => $message->header(AggregateHeader::class)->aggregateId, + 'id' => $aggregateId->toString(), 'name' => $event->hotelName, 'guests' => 0, ], @@ -197,20 +194,20 @@ final class HotelProjector } #[Subscribe(GuestIsCheckedIn::class)] - public function handleGuestIsCheckedIn(Message $message): void + public function handleGuestIsCheckedIn(Uuid $aggregateId): void { $this->db->executeStatement( "UPDATE {$this->table()} SET guests = guests + 1 WHERE id = ?;", - [$message->header(AggregateHeader::class)->aggregateId], + [$aggregateId->toString()], ); } #[Subscribe(GuestIsCheckedOut::class)] - public function handleGuestIsCheckedOut(Message $message): void + public function handleGuestIsCheckedOut(Uuid $aggregateId): void { $this->db->executeStatement( "UPDATE {$this->table()} SET guests = guests - 1 WHERE id = ?;", - [$message->header(AggregateHeader::class)->aggregateId], + [$aggregateId->toString()], ); } @@ -243,7 +240,6 @@ In our example we also want to email the head office as soon as a guest is check ```php use Patchlevel\EventSourcing\Attribute\Processor; use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\Message\Message; #[Processor('admin_emails')] final class SendCheckInEmailProcessor @@ -254,12 +250,12 @@ final class SendCheckInEmailProcessor } #[Subscribe(GuestIsCheckedIn::class)] - public function onGuestIsCheckedIn(Message $message): void + public function onGuestIsCheckedIn(GuestIsCheckedIn $event): void { $this->mailer->send( 'hq@patchlevel.de', 'Guest is checked in', - sprintf('A new guest named "%s" is checked in', $message->event()->guestName), + sprintf('A new guest named "%s" is checked in', $event->guestName), ); } } diff --git a/docs/pages/subscription.md b/docs/pages/subscription.md index 1f0c9d76..1cb9e786 100644 --- a/docs/pages/subscription.md +++ b/docs/pages/subscription.md @@ -165,6 +165,97 @@ final class DoStuffSubscriber 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. +#### Argument Resolver + +The library analyses the method signature and tries to resolve the arguments. +The order of the arguments doesn't matter, you can use multiple arguments and mix them. + +##### Message Resolver + +The message resolver resolves the `Message` object. +It looks for a parameter with the type `Message`. + +```php +use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Subscription\RunMode; + +#[Subscriber('do_stuff', RunMode::Once)] +final class DoStuffSubscriber +{ + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(Message $message): void + { + // do something + } +} +``` +##### Event Resolver + +The event resolver resolves the event object. +It looks for a parameter with the type of the event. + +```php +use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Subscription\RunMode; + +#[Subscriber('do_stuff', RunMode::Once)] +final class DoStuffSubscriber +{ + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(ProfileCreated $profileCreated): void + { + // do something + } +} +``` +##### Aggregate Id Resolver + +The aggregate id resolver resolves the aggregate id. +It looks for a parameter with the instance of the `AggregateRootId`. + +```php +use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Subscription\RunMode; + +#[Subscriber('do_stuff', RunMode::Once)] +final class DoStuffSubscriber +{ + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(ProfileId $profileId): void + { + // do something + } +} +``` +!!! warning + + The resolver argument doesn't know if you're using the correct aggregate id class and doesn't check it. + It gets the Aggregate ID as a string, takes the class and instantiates it with the method `fromString`. + +##### Recorded On Resolver + +The recorded on resolver resolves the recorded on date. +It looks for a parameter with the instance of the `DateTimeImmutable`. + +```php +use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\Attribute\Subscriber; +use Patchlevel\EventSourcing\Subscription\RunMode; + +#[Subscriber('do_stuff', RunMode::Once)] +final class DoStuffSubscriber +{ + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(DateTimeImmutable $recordedOn): void + { + // do something + } +} +``` ### Setup and Teardown Subscribers can have one `setup` and `teardown` method that is executed when the subscription is created or deleted. diff --git a/src/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolver.php b/src/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolver.php index 3814fb17..9472624d 100644 --- a/src/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolver.php +++ b/src/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolver.php @@ -5,20 +5,27 @@ namespace Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver; use Patchlevel\EventSourcing\Aggregate\AggregateHeader; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Metadata\Subscriber\ArgumentMetadata; -use function in_array; +use function class_exists; +use function is_a; final class AggregateIdArgumentResolver implements ArgumentResolver { - public function resolve(ArgumentMetadata $argument, Message $message): string + public function resolve(ArgumentMetadata $argument, Message $message): AggregateRootId { - return $message->header(AggregateHeader::class)->aggregateId; + /** @var class-string $class */ + $class = $argument->type; + + $id = $message->header(AggregateHeader::class)->aggregateId; + + return $class::fromString($id); } public function support(ArgumentMetadata $argument, string $eventClass): bool { - return $argument->type === 'string' && in_array($argument->name, ['aggregateId', 'aggregateRootId']); + return class_exists($argument->type) && is_a($argument->type, AggregateRootId::class, true); } } diff --git a/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php index b4883cc8..ef100be3 100644 --- a/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php +++ b/tests/Benchmark/BasicImplementation/Projection/ProfileProjector.php @@ -12,6 +12,7 @@ use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\NameChanged; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\ProfileCreated; +use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId; #[Projector('profile')] final class ProfileProjector @@ -48,12 +49,12 @@ public function onProfileCreated(ProfileCreated $profileCreated): void } #[Subscribe(NameChanged::class)] - public function onNameChanged(NameChanged $nameChanged, string $aggregateRootId): void + public function onNameChanged(NameChanged $nameChanged, ProfileId $profileId): void { $this->connection->update( $this->table(), ['name' => $nameChanged->name], - ['id' => $aggregateRootId], + ['id' => $profileId->toString()], ); } diff --git a/tests/Integration/Subscription/Subscriber/ErrorProducerSubscriber.php b/tests/Integration/Subscription/Subscriber/ErrorProducerSubscriber.php index e64f3d4b..50059b97 100644 --- a/tests/Integration/Subscription/Subscriber/ErrorProducerSubscriber.php +++ b/tests/Integration/Subscription/Subscriber/ErrorProducerSubscriber.php @@ -8,7 +8,6 @@ use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Subscription\RunMode; use RuntimeException; @@ -36,7 +35,7 @@ public function teardown(): void } #[Subscribe('*')] - public function subscribe(Message $message): void + public function subscribe(): void { if ($this->subscribeError) { throw new RuntimeException('subscribe error'); diff --git a/tests/Integration/Subscription/Subscriber/ProfileNewProjection.php b/tests/Integration/Subscription/Subscriber/ProfileNewProjection.php index 40f6a1fd..f2bae566 100644 --- a/tests/Integration/Subscription/Subscriber/ProfileNewProjection.php +++ b/tests/Integration/Subscription/Subscriber/ProfileNewProjection.php @@ -10,12 +10,9 @@ use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; use Patchlevel\EventSourcing\Tests\Integration\Subscription\Events\ProfileCreated; -use function assert; - #[Projector('profile_2')] final class ProfileNewProjection { @@ -44,12 +41,8 @@ public function drop(): void } #[Subscribe(ProfileCreated::class)] - public function handleProfileCreated(Message $message): void + public function handleProfileCreated(ProfileCreated $profileCreated): void { - $profileCreated = $message->event(); - - assert($profileCreated instanceof ProfileCreated); - $this->connection->executeStatement( 'INSERT INTO ' . $this->tableName() . ' (id, firstname) VALUES(:id, :firstname);', [ diff --git a/tests/Integration/Subscription/Subscriber/ProfileProcessor.php b/tests/Integration/Subscription/Subscriber/ProfileProcessor.php index 78bfc31a..f1ee8faa 100644 --- a/tests/Integration/Subscription/Subscriber/ProfileProcessor.php +++ b/tests/Integration/Subscription/Subscriber/ProfileProcessor.php @@ -6,13 +6,10 @@ use Patchlevel\EventSourcing\Attribute\Processor; use Patchlevel\EventSourcing\Attribute\Subscribe; -use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Repository\RepositoryManager; use Patchlevel\EventSourcing\Tests\Integration\Subscription\Aggregate\Profile; use Patchlevel\EventSourcing\Tests\Integration\Subscription\Events\ProfileCreated; -use function assert; - #[Processor('profile')] final class ProfileProcessor { @@ -22,12 +19,8 @@ public function __construct( } #[Subscribe(ProfileCreated::class)] - public function handleProfileCreated(Message $message): void + public function handleProfileCreated(ProfileCreated $profileCreated): void { - $profileCreated = $message->event(); - - assert($profileCreated instanceof ProfileCreated); - $repository = $this->repositoryManager->get(Profile::class); $profile = $repository->load($profileCreated->profileId); diff --git a/tests/Integration/Subscription/Subscriber/ProfileProjection.php b/tests/Integration/Subscription/Subscriber/ProfileProjection.php index 934b4c22..3df60854 100644 --- a/tests/Integration/Subscription/Subscriber/ProfileProjection.php +++ b/tests/Integration/Subscription/Subscriber/ProfileProjection.php @@ -10,12 +10,9 @@ use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberUtil; use Patchlevel\EventSourcing\Tests\Integration\Subscription\Events\ProfileCreated; -use function assert; - #[Projector('profile_1')] final class ProfileProjection { @@ -44,12 +41,8 @@ public function drop(): void } #[Subscribe(ProfileCreated::class)] - public function handleProfileCreated(Message $message): void + public function handleProfileCreated(ProfileCreated $profileCreated): void { - $profileCreated = $message->event(); - - assert($profileCreated instanceof ProfileCreated); - $this->connection->executeStatement( 'INSERT INTO ' . $this->tableName() . ' (id, name) VALUES(:id, :name);', [ diff --git a/tests/Unit/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolverTest.php b/tests/Unit/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolverTest.php index 00db10f2..10a39c6b 100644 --- a/tests/Unit/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolverTest.php +++ b/tests/Unit/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolverTest.php @@ -6,6 +6,8 @@ use DateTimeImmutable; use Patchlevel\EventSourcing\Aggregate\AggregateHeader; +use Patchlevel\EventSourcing\Aggregate\CustomId; +use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Metadata\Subscriber\ArgumentMetadata; use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\AggregateIdArgumentResolver; @@ -23,21 +25,21 @@ public function testSupport(): void self::assertTrue( $resolver->support( - new ArgumentMetadata('aggregateId', 'string'), + new ArgumentMetadata('aggregateId', Uuid::class), ProfileCreated::class, ), ); self::assertTrue( $resolver->support( - new ArgumentMetadata('aggregateRootId', 'string'), + new ArgumentMetadata('aggregateRootId', ProfileId::class), ProfileCreated::class, ), ); self::assertFalse( $resolver->support( - new ArgumentMetadata('foo', 'string'), + new ArgumentMetadata('foo', ProfileCreated::class), ProfileCreated::class, ), ); @@ -52,10 +54,10 @@ public function testResolve(): void new AggregateHeader('foo', 'bar', 1, new DateTimeImmutable()), ); - self::assertSame( - 'bar', + self::assertEquals( + new CustomId('bar'), $resolver->resolve( - new ArgumentMetadata('foo', 'string'), + new ArgumentMetadata('foo', CustomId::class), $message, ), );