diff --git a/composer.json b/composer.json index 29e865f..a4d37c9 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "symfony/property-info": "^5.0 || ^6.0", "phpdocumentor/reflection-docblock": "^5.0", "phpstan/phpdoc-parser": "^1.24", - "webmozart/assert": "^1.11" + "webmozart/assert": "^1.11", + "jolicode/automapper": "^9.2" }, "require-dev": { "phpunit/phpunit": "^9.5", diff --git a/src/Attribute/Event.php b/src/Attribute/Event.php index b14b545..307acbb 100644 --- a/src/Attribute/Event.php +++ b/src/Attribute/Event.php @@ -14,6 +14,7 @@ final class Event extends Attribute * @param array $dispatch * @param array|string|null $handler * @param array|string|null $transport + * @param class-string|null $messageClass */ public function __construct( array|string|null $handler = null, @@ -21,6 +22,7 @@ public function __construct( public readonly ?string $name = null, public readonly ?string $queue = null, public readonly array $dispatch = [On::POST], + public readonly ?string $messageClass = null, ) { parent::__construct(); Assert::allIsInstanceOf($dispatch, On::class); diff --git a/src/Handler/Contract/EventHandler.php b/src/Handler/Contract/EventHandler.php index 7dd6acd..71f6403 100644 --- a/src/Handler/Contract/EventHandler.php +++ b/src/Handler/Contract/EventHandler.php @@ -5,12 +5,11 @@ namespace OpenClassrooms\ServiceProxy\Handler\Contract; use OpenClassrooms\ServiceProxy\Attribute\Event\Transport; -use OpenClassrooms\ServiceProxy\Model\Event; use OpenClassrooms\ServiceProxy\Model\Request\Instance; interface EventHandler extends AnnotationHandler { - public function dispatch(Event $event, ?string $queue = null): void; + public function dispatch(object $event, ?string $queue = null): void; public function listen(Instance $instance, string $name, Transport $transport = null, int $priority = 0): void; } diff --git a/src/Handler/Impl/Event/HttpEventHandler.php b/src/Handler/Impl/Event/HttpEventHandler.php index 82cd45c..69f4a05 100644 --- a/src/Handler/Impl/Event/HttpEventHandler.php +++ b/src/Handler/Impl/Event/HttpEventHandler.php @@ -39,8 +39,11 @@ public function __construct( * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface */ - public function dispatch(Event $event, ?string $queue = null): void + public function dispatch(object $event, ?string $queue = null): void { + if (!$event instanceof Event) { + throw new \InvalidArgumentException('Event must be an instance of ' . Event::class); + } $message = $this->createMessage($event, $queue); $response = $this->httpClient->request('POST', $this->config->brokerEndpoint, [ 'body' => $this->serializer->serialize([ diff --git a/src/Handler/Impl/Event/SymfonyEventDispatcherEventHandler.php b/src/Handler/Impl/Event/SymfonyEventDispatcherEventHandler.php index 707cc1f..85f7c5d 100644 --- a/src/Handler/Impl/Event/SymfonyEventDispatcherEventHandler.php +++ b/src/Handler/Impl/Event/SymfonyEventDispatcherEventHandler.php @@ -22,8 +22,11 @@ public function __construct( ) { } - public function dispatch(Event $event, ?string $queue = null): void + public function dispatch(object $event, ?string $queue = null): void { + if (!$event instanceof Event) { + throw new \InvalidArgumentException('Event must be an instance of ' . Event::class); + } $this->eventDispatcher->dispatch( $event, $event->name, diff --git a/src/Handler/Impl/Event/SymfonyMessengerEventHandler.php b/src/Handler/Impl/Event/SymfonyMessengerEventHandler.php index 935a23e..6db01fd 100644 --- a/src/Handler/Impl/Event/SymfonyMessengerEventHandler.php +++ b/src/Handler/Impl/Event/SymfonyMessengerEventHandler.php @@ -29,9 +29,14 @@ public function __construct( $this->logger = $logger ?? new NullLogger(); } - public function dispatch(Event $event, ?string $queue = null): void + public function dispatch(object $event, ?string $queue = null): void { - $message = $this->createMessage($event, $queue); + if ($event instanceof Event) { + $message = $this->createMessage($event, $queue); + } else { + $message = $event; + } + try { $this->bus->dispatch($message); } catch (\Throwable $exception) { diff --git a/src/Interceptor/Config/EventInterceptorConfig.php b/src/Interceptor/Config/EventInterceptorConfig.php index 3a80e43..0fbc406 100644 --- a/src/Interceptor/Config/EventInterceptorConfig.php +++ b/src/Interceptor/Config/EventInterceptorConfig.php @@ -13,6 +13,7 @@ final class EventInterceptorConfig */ public function __construct( public readonly string $eventInstanceClassName = Event::class, + public readonly ?string $mapperCacheDir = null, ) { } } diff --git a/src/Interceptor/Impl/EventInterceptor.php b/src/Interceptor/Impl/EventInterceptor.php index 846daa4..5dcc248 100644 --- a/src/Interceptor/Impl/EventInterceptor.php +++ b/src/Interceptor/Impl/EventInterceptor.php @@ -4,6 +4,8 @@ namespace OpenClassrooms\ServiceProxy\Interceptor\Impl; +use AutoMapper\AutoMapper; +use AutoMapper\AutoMapperInterface; use OpenClassrooms\ServiceProxy\Attribute\Event; use OpenClassrooms\ServiceProxy\Handler\Contract\EventHandler; use OpenClassrooms\ServiceProxy\Interceptor\Config\EventInterceptorConfig; @@ -17,12 +19,16 @@ final class EventInterceptor extends AbstractInterceptor implements SuffixInterceptor, PrefixInterceptor { + private AutoMapperInterface $mapper; + public function __construct( private readonly EventFactory $eventFactory, private readonly EventInterceptorConfig $config, iterable $handlers = [], ) { parent::__construct($handlers); + $tmpDir = $this->config->mapperCacheDir ?? sys_get_temp_dir() . '/mapper-cache'; + $this->mapper = AutoMapper::create(cacheDirectory: $tmpDir); } public function getPrefixPriority(): int @@ -68,12 +74,19 @@ public function suffix(Instance $instance): Response $handlers = $this->getHandlers(EventHandler::class, $attribute); foreach ($handlers as $handler) { if ($attribute->isPost() && !$instance->getMethod()->threwException()) { - $event = $this->eventFactory->createFromSenderInstance( - $instance, - Moment::SUFFIX, - $attribute->name, - $this->config->eventInstanceClassName, - ); + if ($attribute->messageClass) { + $event = $this->createMessage( + $attribute->messageClass, + $instance->getMethod()->getReturnedValue(), + ); + } else { + $event = $this->eventFactory->createFromSenderInstance( + $instance, + Moment::SUFFIX, + $attribute->name, + $this->config->eventInstanceClassName, + ); + } $handler->dispatch($event, $attribute->queue); } @@ -102,4 +115,26 @@ public function supportsPrefix(Instance $instance): bool return $instance->getMethod() ->hasAttribute(Event::class); } + + /** + * @template T of object + * @param class-string $messageClass + * @return T + * + * @throws \InvalidArgumentException + */ + private function createMessage(string $messageClass, mixed $response): object + { + if (!\is_object($response) && !\is_array($response)) { + throw new \InvalidArgumentException( + sprintf( + 'The response must be an object to guess arguments for message class "%s".', + $messageClass + ) + ); + } + + /** @var T */ + return $this->mapper->map($response, $messageClass); + } } diff --git a/tests/Double/Mock/Event/EventHandlerMock.php b/tests/Double/Mock/Event/EventHandlerMock.php index fa59e05..99795f8 100644 --- a/tests/Double/Mock/Event/EventHandlerMock.php +++ b/tests/Double/Mock/Event/EventHandlerMock.php @@ -6,7 +6,6 @@ use OpenClassrooms\ServiceProxy\Attribute\Event\Transport; use OpenClassrooms\ServiceProxy\Handler\Contract\EventHandler; -use OpenClassrooms\ServiceProxy\Model\Event; use OpenClassrooms\ServiceProxy\Model\Request\Instance; final class EventHandlerMock implements EventHandler @@ -26,7 +25,7 @@ public function getName(): string return 'array'; } - public function dispatch(Event $event, ?string $queue = null): void + public function dispatch(object $event, ?string $queue = null): void { $this->events[] = $event; } diff --git a/tests/Double/Stub/Event/InvalidResponseMessageClassAnnotatedClass.php b/tests/Double/Stub/Event/InvalidResponseMessageClassAnnotatedClass.php new file mode 100644 index 0000000..49a7457 --- /dev/null +++ b/tests/Double/Stub/Event/InvalidResponseMessageClassAnnotatedClass.php @@ -0,0 +1,16 @@ +proxyFactory->createProxy(new ObjectResponseMessageClassAnnotatedClass()); + $result = $proxy->handle('world'); + + $this->assertInstanceOf(ResponseObject::class, $result); + $this->assertSame('world', $result->content); + $this->assertIsInt($result->id); + $this->assertInstanceOf(Metadata::class, $result->meta); + $this->assertInstanceOf(\DateTimeImmutable::class, $result->createdAt); + + $this->assertEventsCount(1); + $event = $this->handler->getEvents()[0]; + $this->assertInstanceOf(CustomMessage::class, $event); + $this->assertSame('world', $event->content); + $this->assertSame($result->id, $event->id); + $this->assertInstanceOf(\DateTimeImmutable::class, $event->createdAt); + $this->assertSame( + $result->createdAt->format(\DateTimeInterface::ATOM), + $event->createdAt->format(\DateTimeInterface::ATOM) + ); + $this->assertInstanceOf(Metadata::class, $event->meta); + $this->assertTrue($event->meta->active); + } + + public function testInvalidResponseForMessageClassThrowsException(): void + { + $proxy = $this->proxyFactory->createProxy(new InvalidResponseMessageClassAnnotatedClass()); + + $this->expectException(\InvalidArgumentException::class); + $proxy->invalid('test'); + } + private function assertEventsCount(int $count): void { $this->assertCount($count, $this->handler->getEvents());