diff --git a/baseline.xml b/baseline.xml index 029540042..22ff0c1fe 100644 --- a/baseline.xml +++ b/baseline.xml @@ -115,15 +115,14 @@ + + + - - subscriber->$method(...)]]> - - @@ -134,7 +133,6 @@ - @@ -142,7 +140,6 @@ - @@ -151,7 +148,6 @@ - @@ -163,7 +159,6 @@ id]]> - @@ -171,7 +166,6 @@ - @@ -202,13 +196,6 @@ - - - - - - - @@ -291,13 +278,6 @@ - - - - - - - diff --git a/deptrac.yaml b/deptrac.yaml index 9f94d591b..95e5c60a1 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -143,6 +143,7 @@ deptrac: - Metadata - Subscription Subscription: + - Aggregate - Attribute - Clock - Message diff --git a/src/Metadata/Subscriber/ArgumentMetadata.php b/src/Metadata/Subscriber/ArgumentMetadata.php new file mode 100644 index 000000000..56576fdba --- /dev/null +++ b/src/Metadata/Subscriber/ArgumentMetadata.php @@ -0,0 +1,14 @@ +newInstance(); $eventClass = $instance->eventClass; - $subscribeMethods[$eventClass][] = $method->getName(); + $subscribeMethods[$eventClass][] = $this->subscribeMethod($method); } if ($method->getAttributes(Setup::class)) { @@ -91,4 +94,27 @@ public function metadata(string $subscriber): SubscriberMetadata return $metadata; } + + private function subscribeMethod(ReflectionMethod $method): SubscribeMethodMetadata + { + $arguments = []; + + foreach ($method->getParameters() as $parameter) { + $type = $parameter->getType(); + + if (!$type instanceof ReflectionNamedType) { + throw new RuntimeException('parameter type is required'); + } + + $arguments[] = new ArgumentMetadata( + $parameter->getName(), + $type->getName(), + ); + } + + return new SubscribeMethodMetadata( + $method->getName(), + $arguments, + ); + } } diff --git a/src/Metadata/Subscriber/SubscribeMethodMetadata.php b/src/Metadata/Subscriber/SubscribeMethodMetadata.php new file mode 100644 index 000000000..aafcd7277 --- /dev/null +++ b/src/Metadata/Subscriber/SubscribeMethodMetadata.php @@ -0,0 +1,15 @@ + $arguments */ + public function __construct( + public readonly string $name, + public readonly array $arguments = [], + ) { + } +} diff --git a/src/Metadata/Subscriber/SubscriberMetadata.php b/src/Metadata/Subscriber/SubscriberMetadata.php index e6335d795..8c26b1942 100644 --- a/src/Metadata/Subscriber/SubscriberMetadata.php +++ b/src/Metadata/Subscriber/SubscriberMetadata.php @@ -13,7 +13,7 @@ public function __construct( public readonly string $id, public readonly string $group = Subscription::DEFAULT_GROUP, public readonly RunMode $runMode = RunMode::FromBeginning, - /** @var array> */ + /** @var array> */ public readonly array $subscribeMethods = [], public readonly string|null $setupMethod = null, public readonly string|null $teardownMethod = null, diff --git a/src/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolver.php b/src/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolver.php new file mode 100644 index 000000000..3814fb172 --- /dev/null +++ b/src/Subscription/Subscriber/ArgumentResolver/AggregateIdArgumentResolver.php @@ -0,0 +1,24 @@ +header(AggregateHeader::class)->aggregateId; + } + + public function support(ArgumentMetadata $argument, string $eventClass): bool + { + return $argument->type === 'string' && in_array($argument->name, ['aggregateId', 'aggregateRootId']); + } +} diff --git a/src/Subscription/Subscriber/ArgumentResolver/ArgumentResolver.php b/src/Subscription/Subscriber/ArgumentResolver/ArgumentResolver.php new file mode 100644 index 000000000..2f0411af2 --- /dev/null +++ b/src/Subscription/Subscriber/ArgumentResolver/ArgumentResolver.php @@ -0,0 +1,15 @@ +event(); + } + + public function support(ArgumentMetadata $argument, string $eventClass): bool + { + return class_exists($argument->type) && is_a($eventClass, $argument->type, true); + } +} diff --git a/src/Subscription/Subscriber/ArgumentResolver/MessageArgumentResolver.php b/src/Subscription/Subscriber/ArgumentResolver/MessageArgumentResolver.php new file mode 100644 index 000000000..ceabce7c8 --- /dev/null +++ b/src/Subscription/Subscriber/ArgumentResolver/MessageArgumentResolver.php @@ -0,0 +1,21 @@ +type === Message::class; + } +} diff --git a/src/Subscription/Subscriber/ArgumentResolver/RecordedOnArgumentResolver.php b/src/Subscription/Subscriber/ArgumentResolver/RecordedOnArgumentResolver.php new file mode 100644 index 000000000..2a2e7dc45 --- /dev/null +++ b/src/Subscription/Subscriber/ArgumentResolver/RecordedOnArgumentResolver.php @@ -0,0 +1,23 @@ +header(AggregateHeader::class)->recordedOn; + } + + public function support(ArgumentMetadata $argument, string $eventClass): bool + { + return $argument->type === DateTimeImmutable::class; + } +} diff --git a/src/Subscription/Subscriber/MetadataSubscriberAccessor.php b/src/Subscription/Subscriber/MetadataSubscriberAccessor.php index c55f6c571..32cd948c6 100644 --- a/src/Subscription/Subscriber/MetadataSubscriberAccessor.php +++ b/src/Subscription/Subscriber/MetadataSubscriberAccessor.php @@ -7,8 +7,10 @@ use Closure; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Metadata\Subscriber\SubscribeMethodMetadata; use Patchlevel\EventSourcing\Metadata\Subscriber\SubscriberMetadata; use Patchlevel\EventSourcing\Subscription\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\ArgumentResolver; use function array_key_exists; use function array_map; @@ -19,9 +21,11 @@ final class MetadataSubscriberAccessor implements SubscriberAccessor /** @var array> */ private array $subscribeCache = []; + /** @param list $argumentResolvers */ public function __construct( private readonly object $subscriber, private readonly SubscriberMetadata $metadata, + private readonly array $argumentResolvers, ) { } @@ -79,11 +83,59 @@ public function subscribeMethods(string $eventClass): array ); $this->subscribeCache[$eventClass] = array_map( - /** @return Closure(Message):void */ - fn (string $method) => $this->subscriber->$method(...), + fn (SubscribeMethodMetadata $method): Closure => $this->createClosure($eventClass, $method), $methods, ); return $this->subscribeCache[$eventClass]; } + + /** + * @param class-string $eventClass + * + * @return Closure(Message):void + */ + private function createClosure(string $eventClass, SubscribeMethodMetadata $method): Closure + { + $resolvers = $this->resolvers($eventClass, $method); + $methodName = $method->name; + + return function (Message $message) use ($methodName, $resolvers): void { + $arguments = []; + + foreach ($resolvers as $resolver) { + $arguments[] = $resolver($message); + } + + $this->subscriber->$methodName(...$arguments); + }; + } + + /** + * @param class-string $eventClass + * + * @return list + */ + private function resolvers(string $eventClass, SubscribeMethodMetadata $method): array + { + $resolvers = []; + + foreach ($method->arguments as $argument) { + foreach ($this->argumentResolvers as $resolver) { + if (!$resolver->support($argument, $eventClass)) { + continue; + } + + $resolvers[] = static function (Message $message) use ($resolver, $argument): mixed { + return $resolver->resolve($argument, $message); + }; + + continue 2; + } + + throw new NoSuitableResolver($this->subscriber::class, $method->name, $argument->name); + } + + return $resolvers; + } } diff --git a/src/Subscription/Subscriber/MetadataSubscriberAccessorRepository.php b/src/Subscription/Subscriber/MetadataSubscriberAccessorRepository.php index 324c1eada..d38c6c974 100644 --- a/src/Subscription/Subscriber/MetadataSubscriberAccessorRepository.php +++ b/src/Subscription/Subscriber/MetadataSubscriberAccessorRepository.php @@ -6,7 +6,13 @@ use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory; use Patchlevel\EventSourcing\Metadata\Subscriber\SubscriberMetadataFactory; +use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\AggregateIdArgumentResolver; +use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\ArgumentResolver; +use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\EventArgumentResolver; +use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\MessageArgumentResolver; +use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\RecordedOnArgumentResolver; +use function array_merge; use function array_values; final class MetadataSubscriberAccessorRepository implements SubscriberAccessorRepository @@ -14,11 +20,27 @@ final class MetadataSubscriberAccessorRepository implements SubscriberAccessorRe /** @var array */ private array $subscribersMap = []; - /** @param iterable $subscribers */ + /** @var list $argumentResolvers */ + private readonly array $argumentResolvers; + + /** + * @param iterable $subscribers + * @param list $argumentResolvers + */ public function __construct( private readonly iterable $subscribers, private readonly SubscriberMetadataFactory $metadataFactory = new AttributeSubscriberMetadataFactory(), + array $argumentResolvers = [], ) { + $this->argumentResolvers = array_merge( + $argumentResolvers, + [ + new MessageArgumentResolver(), + new EventArgumentResolver(), + new AggregateIdArgumentResolver(), + new RecordedOnArgumentResolver(), + ], + ); } /** @return iterable */ @@ -43,7 +65,11 @@ private function subscriberAccessorMap(): array foreach ($this->subscribers as $subscriber) { $metadata = $this->metadataFactory->metadata($subscriber::class); - $this->subscribersMap[$metadata->id] = new MetadataSubscriberAccessor($subscriber, $metadata); + $this->subscribersMap[$metadata->id] = new MetadataSubscriberAccessor( + $subscriber, + $metadata, + $this->argumentResolvers, + ); } return $this->subscribersMap; diff --git a/src/Subscription/Subscriber/NoSuitableResolver.php b/src/Subscription/Subscriber/NoSuitableResolver.php new file mode 100644 index 000000000..a7a096410 --- /dev/null +++ b/src/Subscription/Subscriber/NoSuitableResolver.php @@ -0,0 +1,24 @@ +event(); - - assert($profileCreated instanceof ProfileCreated); - $this->connection->insert( $this->table(), [ @@ -56,16 +48,12 @@ public function onProfileCreated(Message $message): void } #[Subscribe(NameChanged::class)] - public function onNameChanged(Message $message): void + public function onNameChanged(NameChanged $nameChanged, string $aggregateRootId): void { - $nameChanged = $message->event(); - - assert($nameChanged instanceof NameChanged); - $this->connection->update( $this->table(), ['name' => $nameChanged->name], - ['id' => $message->header(AggregateHeader::class)->aggregateId], + ['id' => $aggregateRootId], ); } diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php index d782339e8..6e505a784 100644 --- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php +++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php @@ -19,8 +19,8 @@ use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository; use Patchlevel\EventSourcing\Tests\DbalManager; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Aggregate\Profile; +use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Listener\SendEmailListener; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\MessageDecorator\FooMessageDecorator; -use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Processor\SendEmailProcessor; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Projection\ProfileProjector; use PHPUnit\Framework\TestCase; @@ -59,8 +59,7 @@ public function testSuccessful(): void ); $eventBus = DefaultEventBus::create([ - new SendEmailProcessor(), - $profileProjector, + new SendEmailListener(), ]); $manager = new DefaultRepositoryManager( @@ -85,6 +84,8 @@ public function testSuccessful(): void $profile = Profile::create($profileId, 'John'); $repository->save($profile); + $engine->run(); + $result = $this->connection->fetchAssociative('SELECT * FROM projection_profile WHERE id = ?', ['1']); self::assertIsArray($result); @@ -126,8 +127,7 @@ public function testSnapshot(): void ); $eventBus = DefaultEventBus::create([ - new SendEmailProcessor(), - $profileProjection, + new SendEmailListener(), ]); $manager = new DefaultRepositoryManager( @@ -152,6 +152,8 @@ public function testSnapshot(): void $profile = Profile::create($profileId, 'John'); $repository->save($profile); + $engine->run(); + $result = $this->connection->fetchAssociative('SELECT * FROM projection_profile WHERE id = ?', ['1']); self::assertIsArray($result); diff --git a/tests/Integration/BasicImplementation/Processor/SendEmailProcessor.php b/tests/Integration/BasicImplementation/Listener/SendEmailListener.php similarity index 90% rename from tests/Integration/BasicImplementation/Processor/SendEmailProcessor.php rename to tests/Integration/BasicImplementation/Listener/SendEmailListener.php index d68809df7..2e10d2b9b 100644 --- a/tests/Integration/BasicImplementation/Processor/SendEmailProcessor.php +++ b/tests/Integration/BasicImplementation/Listener/SendEmailListener.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Processor; +namespace Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Listener; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\SendEmailMock; -final class SendEmailProcessor +final class SendEmailListener { #[Subscribe(ProfileCreated::class)] public function onProfileCreated(Message $message): void diff --git a/tests/Integration/BasicImplementation/Projection/ProfileProjector.php b/tests/Integration/BasicImplementation/Projection/ProfileProjector.php index 6add789b4..87f80777a 100644 --- a/tests/Integration/BasicImplementation/Projection/ProfileProjector.php +++ b/tests/Integration/BasicImplementation/Projection/ProfileProjector.php @@ -10,11 +10,8 @@ use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Teardown; -use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; -use function assert; - #[Projector('profile-1')] final class ProfileProjector { @@ -41,12 +38,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 projection_profile (id, name) VALUES(:id, :name);', [ diff --git a/tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest.php b/tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest.php index 770e8bb97..d8a917ec7 100644 --- a/tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest.php +++ b/tests/Unit/Metadata/Subscriber/AttributeSubscriberMetadataFactoryTest.php @@ -14,6 +14,7 @@ use Patchlevel\EventSourcing\Metadata\Subscriber\ClassIsNotASubscriber; use Patchlevel\EventSourcing\Metadata\Subscriber\DuplicateSetupMethod; use Patchlevel\EventSourcing\Metadata\Subscriber\DuplicateTeardownMethod; +use Patchlevel\EventSourcing\Metadata\Subscriber\SubscribeMethodMetadata; use Patchlevel\EventSourcing\Subscription\RunMode; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileVisited; @@ -106,7 +107,11 @@ public function drop(): void $metadata = $metadataFactory->metadata($subscriber::class); self::assertEquals( - [ProfileVisited::class => ['handle']], + [ + ProfileVisited::class => [ + new SubscribeMethodMetadata('handle', []), + ], + ], $metadata->subscribeMethods, ); @@ -130,8 +135,8 @@ public function handle(): void self::assertEquals( [ - ProfileVisited::class => ['handle'], - ProfileCreated::class => ['handle'], + ProfileVisited::class => [new SubscribeMethodMetadata('handle', [])], + ProfileCreated::class => [new SubscribeMethodMetadata('handle', [])], ], $metadata->subscribeMethods, ); @@ -152,7 +157,7 @@ public function handle(): void self::assertEquals( [ - '*' => ['handle'], + '*' => [new SubscribeMethodMetadata('handle', [])], ], $metadata->subscribeMethods, ); diff --git a/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorRepositoryTest.php b/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorRepositoryTest.php index 160cb902c..96d02c610 100644 --- a/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorRepositoryTest.php +++ b/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorRepositoryTest.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory; use Patchlevel\EventSourcing\Subscription\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver; use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor; use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository; use PHPUnit\Framework\TestCase; @@ -39,6 +40,12 @@ class { $accessor = new MetadataSubscriberAccessor( $subscriber, $metadataFactory->metadata($subscriber::class), + [ + new ArgumentResolver\MessageArgumentResolver(), + new ArgumentResolver\EventArgumentResolver(), + new ArgumentResolver\AggregateIdArgumentResolver(), + new ArgumentResolver\RecordedOnArgumentResolver(), + ], ); self::assertEquals([$accessor], $repository->all()); diff --git a/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorTest.php b/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorTest.php index 6e19f2053..dff93706e 100644 --- a/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorTest.php +++ b/tests/Unit/Subscription/Subscriber/MetadataSubscriberAccessorTest.php @@ -11,6 +11,7 @@ use Patchlevel\EventSourcing\Message\Message; use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory; use Patchlevel\EventSourcing\Subscription\RunMode; +use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\MessageArgumentResolver; use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; use PHPUnit\Framework\TestCase; @@ -27,6 +28,7 @@ class { $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [], ); self::assertEquals('profile', $accessor->id()); @@ -41,6 +43,7 @@ class { $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [], ); self::assertEquals('default', $accessor->group()); @@ -55,6 +58,7 @@ class { $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [], ); self::assertEquals(RunMode::FromBeginning, $accessor->runMode()); @@ -73,6 +77,9 @@ public function onProfileCreated(Message $message): void $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [ + new MessageArgumentResolver(), + ], ); $result = $accessor->subscribeMethods(ProfileCreated::class); @@ -100,6 +107,9 @@ public function onFoo(Message $message): void $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [ + new MessageArgumentResolver(), + ], ); $result = $accessor->subscribeMethods(ProfileCreated::class); @@ -123,6 +133,9 @@ public function onProfileCreated(Message $message): void $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [ + new MessageArgumentResolver(), + ], ); $result = $accessor->subscribeMethods(ProfileCreated::class); @@ -145,6 +158,7 @@ public function method(): void $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [], ); $result = $accessor->setupMethod(); @@ -161,6 +175,7 @@ class { $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [], ); $result = $accessor->setupMethod(); @@ -181,6 +196,7 @@ public function method(): void $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [], ); $result = $accessor->teardownMethod(); @@ -197,6 +213,7 @@ class { $accessor = new MetadataSubscriberAccessor( $subscriber, (new AttributeSubscriberMetadataFactory())->metadata($subscriber::class), + [], ); $result = $accessor->teardownMethod();