From c2b6adab7b1b6ff61c33a4f2e5eb16fe741f3496 Mon Sep 17 00:00:00 2001 From: David Kurka Date: Tue, 15 Aug 2023 09:39:55 +0200 Subject: [PATCH] add support for multiple messages in one handler --- .docs/README.md | 62 ++++- src/DI/Pass/HandlerPass.php | 140 ++++++---- src/DI/Utils/Reflector.php | 31 ++- .../Cases/DI/MessengerExtension.handler.phpt | 263 ++++++++++++++++-- .../Handler/MultipleAttributesHandler.php | 12 - .../MultipleMethodsWithAttributesHandler.php | 24 ++ ...ultipleMethodsWithoutAttributesHandler.php | 21 ++ .../NonDefaultMethodWithAttributeHandler.php | 25 ++ ...onDefaultMethodWithoutAttributeHandler.php | 17 ++ tests/Mocks/Message/BarMessage.php | 15 + tests/Mocks/Message/FooMessage.php | 15 + 11 files changed, 521 insertions(+), 104 deletions(-) delete mode 100644 tests/Mocks/Handler/MultipleAttributesHandler.php create mode 100644 tests/Mocks/Handler/MultipleMethodsWithAttributesHandler.php create mode 100644 tests/Mocks/Handler/MultipleMethodsWithoutAttributesHandler.php create mode 100644 tests/Mocks/Handler/NonDefaultMethodWithAttributeHandler.php create mode 100644 tests/Mocks/Handler/NonDefaultMethodWithoutAttributeHandler.php create mode 100644 tests/Mocks/Message/BarMessage.php create mode 100644 tests/Mocks/Message/FooMessage.php diff --git a/.docs/README.md b/.docs/README.md index 2c8f636..5d78ae8 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -222,30 +222,75 @@ final class SimpleMessage ### Handlers -All handlers must be registered to your [DIC container](https://doc.nette.org/en/dependency-injection) via [Neon files](https://doc.nette.org/en/neon/format). All handlers must -have [`#[AsMessageHandler]`](https://github.com/symfony/messenger/blob/6e749550d539f787023878fad675b744411db003/Attribute/AsMessageHandler.php) attribute. - +All handlers must be registered to your [DIC container](https://doc.nette.org/en/dependency-injection) via [Neon files](https://doc.nette.org/en/neon/format).
+All handlers must also be marked as message handlers to handle messages. +There are 2 different ways to mark your handlers: +1. with the neon tag [`contributte.messenger.handler`]: ```neon services: - - App\Domain\SimpleMessageHandler + - + class: App\SimpleMessageHandler + tags: + contributte.messenger.handler: # the configuration below is optional + bus: event + alias: simple + method: __invoke + handles: App\SimpleMessage + priority: 0 + from_transport: sync ``` +2. with the attribute [`#[AsMessageHandler]`] (https://github.com/symfony/messenger/blob/6e749550d539f787023878fad675b744411db003/Attribute/AsMessageHandler.php). ```php getTag(MessengerExtension::HANDLER_TAG); - $tagOptions = [ - 'bus' => $tag['bus'] ?? null, - 'alias' => $tag['alias'] ?? null, - 'method' => $tag['method'] ?? null, - 'handles' => $tag['handles'] ?? null, - 'priority' => $tag['priority'] ?? null, - 'from_transport' => $tag['from_transport'] ?? null, - ]; - - // Drain service attribute - /** @var array> $attributes */ - $attributes = $rc->getAttributes(AsMessageHandler::class); - /** @var AsMessageHandler $attributeHandler */ - $attributeHandler = isset($attributes[0]) ? $attributes[0]->getArguments() : new stdClass(); - $attributeOptions = [ - 'bus' => $attributeHandler->bus ?? null, - 'method' => $attributeHandler->method ?? null, - 'priority' => $attributeHandler->priority ?? null, - 'handles' => $attributeHandler->handles ?? null, - 'from_transport' => $attributeHandler->fromTransport ?? null, - ]; - - // Complete final options - $options = [ - 'service' => $serviceName, - 'bus' => $tagOptions['bus'] ?? $attributeOptions['bus'] ?? $busName, - 'alias' => $tagOptions['alias'] ?? null, - 'method' => $tagOptions['method'] ?? $attributeOptions['method'] ?? '__invoke', - 'handles' => $tagOptions['handles'] ?? $attributeOptions['handles'] ?? null, - 'priority' => $tagOptions['priority'] ?? $attributeOptions['priority'] ?? 0, - 'from_transport' => $tagOptions['from_transport'] ?? $attributeOptions['from_transport'] ?? null, - ]; - - // Autodetect handled message - if (!isset($options['handles'])) { - $options['handles'] = Reflector::getMessageHandlerMessage($serviceClass, $options); - } + $tagsOptions = $this->getTagsOptions($serviceDef, $serviceName, $busName); + $attributesOptions = $this->getAttributesOptions($serviceClass, $serviceName, $busName); - // If handler is not for current bus, then skip it - if (($tagOptions['bus'] ?? $attributeOptions['bus'] ?? $busName) !== $busName) { - continue; - } + foreach (array_merge($tagsOptions, $attributesOptions) as $options) { + // Autodetect handled message + if (!isset($options['handles'])) { + $options['handles'] = Reflector::getMessageHandlerMessage($serviceClass, $options); + } - $handlers[$options['handles']][$options['priority']][] = $options; + // If handler is not for current bus, then skip it + if ($options['bus'] !== $busName) { + continue; + } + + $handlers[$options['handles']][$options['priority']][] = $options; + } } // Sort handlers by priority @@ -140,7 +114,7 @@ private function getMessageHandlers(): array } // Skip services without attribute - if (Reflector::getMessageHandler($class) === null) { + if (Reflector::getMessageHandlers($class) === []) { continue; } @@ -151,4 +125,72 @@ private function getMessageHandlers(): array return array_unique($serviceHandlers); } + /** + * @return list + */ + private function getTagsOptions(Definition $serviceDefinition, string $serviceName, string $defaultBusName): array + { + // Drain service tag + $tags = (array) $serviceDefinition->getTag(MessengerExtension::HANDLER_TAG); + $isList = $tags === [] || array_keys($tags) === range(0, count($tags) - 1); + /** @var list> $tags */ + $tags = $isList ? $tags : [$tags]; + $tagsOptions = []; + + foreach ($tags as $tag) { + $tagsOptions[] = [ + 'service' => $serviceName, + 'bus' => isset($tag['bus']) && is_string($tag['bus']) ? $tag['bus'] : $defaultBusName, + 'alias' => isset($tag['alias']) && is_string($tag['alias']) ? $tag['alias'] : null, + 'method' => isset($tag['method']) && is_string($tag['method']) ? $tag['method'] : self::DEFAULT_METHOD_NAME, + 'handles' => isset($tag['handles']) && is_string($tag['handles']) ? $tag['handles'] : null, + 'priority' => isset($tag['priority']) && is_numeric($tag['priority']) ? (int) $tag['priority'] : self::DEFAULT_PRIORITY, + 'from_transport' => isset($tag['from_transport']) && is_string($tag['from_transport']) ? $tag['from_transport'] : null, + ]; + } + + return $tagsOptions; + } + + /** + * @param class-string $serviceClass + * @return list + */ + private function getAttributesOptions(string $serviceClass, string $serviceName, string $defaultBusName): array + { + // Drain service attribute + $attributes = Reflector::getMessageHandlers($serviceClass); + $attributesOptions = []; + + foreach ($attributes as $attribute) { + $attributesOptions[] = [ + 'service' => $serviceName, + 'bus' => $attribute->bus ?? $defaultBusName, + 'alias' => null, + 'method' => $attribute->method ?? self::DEFAULT_METHOD_NAME, + 'priority' => $attribute->priority ?? self::DEFAULT_PRIORITY, + 'handles' => $attribute->handles ?? null, + 'from_transport' => $attribute->fromTransport ?? null, + ]; + } + + return $attributesOptions; + } + } diff --git a/src/DI/Utils/Reflector.php b/src/DI/Utils/Reflector.php index ba974c6..543814f 100644 --- a/src/DI/Utils/Reflector.php +++ b/src/DI/Utils/Reflector.php @@ -3,6 +3,7 @@ namespace Contributte\Messenger\DI\Utils; use Contributte\Messenger\Exception\LogicalException; +use ReflectionAttribute; use ReflectionClass; use ReflectionException; use ReflectionIntersectionType; @@ -11,30 +12,40 @@ use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Handler\Acknowledger; use Symfony\Component\Messenger\Handler\BatchHandlerInterface; +use function array_map; +use function array_merge; final class Reflector { /** * @param class-string $class + * @return array */ - public static function getMessageHandler(string $class): ?AsMessageHandler + public static function getMessageHandlers(string $class): array { $rc = new ReflectionClass($class); - $attributes = $rc->getAttributes(AsMessageHandler::class); + $classAttributes = array_map( + static fn (ReflectionAttribute $attribute): AsMessageHandler => $attribute->newInstance(), + $rc->getAttributes(AsMessageHandler::class), + ); - // No #[AsMessageHandler] attribute - if (count($attributes) <= 0) { - return null; - } + $methodAttributes = []; + + foreach ($rc->getMethods() as $method) { + $methodAttributes[] = array_map( + static function (ReflectionAttribute $reflectionAttribute) use ($method): AsMessageHandler { + $attribute = $reflectionAttribute->newInstance(); + $attribute->method = $method->getName(); - // Validate multi-usage of #[AsMessageHandler] - if (count($attributes) > 1) { - throw new LogicalException(sprintf('Only attribute #[AsMessageHandler] can be used on class "%s"', $class)); + return $attribute; + }, + $method->getAttributes(AsMessageHandler::class), + ); } - return $attributes[0]->newInstance(); + return array_merge($classAttributes, ...$methodAttributes); } /** diff --git a/tests/Cases/DI/MessengerExtension.handler.phpt b/tests/Cases/DI/MessengerExtension.handler.phpt index bc56698..3dc62c5 100644 --- a/tests/Cases/DI/MessengerExtension.handler.phpt +++ b/tests/Cases/DI/MessengerExtension.handler.phpt @@ -5,15 +5,22 @@ namespace Tests\Cases\DI; use Contributte\Messenger\Exception\LogicalException; use Contributte\Tester\Toolkit; use Nette\DI\Compiler; +use Nette\DI\Container as NetteContainer; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Handler\HandlerDescriptor; +use Symfony\Component\Messenger\Handler\HandlersLocatorInterface; use Symfony\Component\Messenger\Transport\TransportInterface; use Tester\Assert; use Tests\Mocks\Handler\SimpleHandler; +use Tests\Mocks\Message\BarMessage; +use Tests\Mocks\Message\FooMessage; +use Tests\Mocks\Message\SimpleMessage; use Tests\Toolkit\Container; use Tests\Toolkit\Helpers; require_once __DIR__ . '/../../bootstrap.php'; -// Handler +// successful DIC registration Toolkit::test(function (): void { $container = Container::of() ->withDefaults() @@ -38,6 +45,228 @@ Toolkit::test(function (): void { Assert::count(1, $container->findByType(SimpleHandler::class)); }); +// default attribute values +Toolkit::test(function (): void { + $container = Container::of() + ->withDefaults() + ->withCompiler(function (Compiler $compiler): void { + $compiler->addConfig(Helpers::neon(<<<'NEON' + messenger: + transport: + memory: + dsn: in-memory:// + + routing: + Tests\Mocks\Message\SimpleMessage: [memory] + + services: + - Tests\Mocks\Handler\SimpleHandler + NEON + )); + }) + ->build(); + + $descriptor = getHandlerDescriptor($container, new SimpleMessage('test')); + Assert::same('messageBus', $descriptor->getOption('bus')); + Assert::same(null, $descriptor->getOption('alias')); + Assert::same('__invoke', $descriptor->getOption('method')); + Assert::same(SimpleMessage::class, $descriptor->getOption('handles')); + Assert::same(0, $descriptor->getOption('priority')); + Assert::same(null, $descriptor->getOption('from_transport')); +}); + +// non-default attribute values +Toolkit::test(function (): void { + $container = Container::of() + ->withDefaults() + ->withCompiler(function (Compiler $compiler): void { + $compiler->addConfig(Helpers::neon(<<<'NEON' + messenger: + transport: + memory: + dsn: in-memory:// + + routing: + Tests\Mocks\Message\SimpleMessage: [memory] + + bus: + command: [] + services: + - Tests\Mocks\Handler\NonDefaultMethodWithAttributeHandler + NEON + )); + }) + ->build(); + + $descriptor = getHandlerDescriptor($container, new SimpleMessage('test'), 'command'); + Assert::same('command', $descriptor->getOption('bus')); + Assert::same(null, $descriptor->getOption('alias')); + Assert::same('nonDefaultMethod', $descriptor->getOption('method')); + Assert::same(SimpleMessage::class, $descriptor->getOption('handles')); + Assert::same(10, $descriptor->getOption('priority')); + Assert::same('sync', $descriptor->getOption('from_transport')); +}); + +// default tag values +Toolkit::test(function (): void { + $container = Container::of() + ->withDefaults() + ->withCompiler(function (Compiler $compiler): void { + $compiler->addConfig(Helpers::neon(<<<'NEON' + messenger: + transport: + memory: + dsn: in-memory:// + + routing: + Tests\Mocks\Message\SimpleMessage: [memory] + + services: + - + class: Tests\Mocks\Handler\NoAttributeHandler + tags: + - contributte.messenger.handler + NEON + )); + }) + ->build(); + + $descriptor = getHandlerDescriptor($container, new SimpleMessage('test')); + Assert::same('messageBus', $descriptor->getOption('bus')); + Assert::same(null, $descriptor->getOption('alias')); + Assert::same('__invoke', $descriptor->getOption('method')); + Assert::same(SimpleMessage::class, $descriptor->getOption('handles')); + Assert::same(0, $descriptor->getOption('priority')); + Assert::same(null, $descriptor->getOption('from_transport')); +}); + +// non-default tag values +Toolkit::test(function (): void { + $container = Container::of() + ->withDefaults() + ->withCompiler(function (Compiler $compiler): void { + $compiler->addConfig(Helpers::neon(<<<'NEON' + messenger: + transport: + memory: + dsn: in-memory:// + + routing: + Tests\Mocks\Message\SimpleMessage: [memory] + + bus: + command: [] + + services: + - + class: Tests\Mocks\Handler\NonDefaultMethodWithoutAttributeHandler + tags: + contributte.messenger.handler: + bus: command + alias: simple + method: nonDefaultMethod + handles: Tests\Mocks\Message\SimpleMessage + priority: 10 + from_transport: sync + NEON + )); + }) + ->build(); + $descriptor = getHandlerDescriptor($container, new SimpleMessage('test'), 'command'); + Assert::same('command', $descriptor->getOption('bus')); + Assert::same('simple', $descriptor->getOption('alias')); + Assert::same('nonDefaultMethod', $descriptor->getOption('method')); + Assert::same(SimpleMessage::class, $descriptor->getOption('handles')); + Assert::same(10, $descriptor->getOption('priority')); + Assert::same('sync', $descriptor->getOption('from_transport')); +}); + +// handling of multiple messages in a single handler with attributes +Toolkit::test(function (): void { + $container = Container::of() + ->withDefaults() + ->withCompiler(function (Compiler $compiler): void { + $compiler->addConfig(Helpers::neon(<<<'NEON' + messenger: + transport: + memory: + dsn: in-memory:// + + routing: + Tests\Mocks\Message\FooMessage: [memory] + Tests\Mocks\Message\BarMessage: [memory] + + services: + - Tests\Mocks\Handler\MultipleMethodsWithAttributesHandler + NEON + )); + }) + ->build(); + + $descriptor = getHandlerDescriptor($container, new FooMessage('test')); + Assert::same('messageBus', $descriptor->getOption('bus')); + Assert::same(null, $descriptor->getOption('alias')); + Assert::same('whenFooMessageReceived', $descriptor->getOption('method')); + Assert::same(FooMessage::class, $descriptor->getOption('handles')); + Assert::same(0, $descriptor->getOption('priority')); + Assert::same(null, $descriptor->getOption('from_transport')); + + $descriptor = getHandlerDescriptor($container, new BarMessage('test')); + Assert::same('messageBus', $descriptor->getOption('bus')); + Assert::same(null, $descriptor->getOption('alias')); + Assert::same('whenBarMessageReceived', $descriptor->getOption('method')); + Assert::same(BarMessage::class, $descriptor->getOption('handles')); + Assert::same(0, $descriptor->getOption('priority')); + Assert::same(null, $descriptor->getOption('from_transport')); +}); + +// handling of multiple messages in a single handler with tags +Toolkit::test(function (): void { + $container = Container::of() + ->withDefaults() + ->withCompiler(function (Compiler $compiler): void { + $compiler->addConfig(Helpers::neon(<<<'NEON' + messenger: + transport: + memory: + dsn: in-memory:// + + routing: + Tests\Mocks\Message\FooMessage: [memory] + Tests\Mocks\Message\BarMessage: [memory] + + services: + - + class: Tests\Mocks\Handler\MultipleMethodsWithoutAttributesHandler + tags: + contributte.messenger.handler: + - + method: whenFooMessageReceived + - + method: whenBarMessageReceived + + NEON + )); + }) + ->build(); + + $descriptor = getHandlerDescriptor($container, new FooMessage('test')); + Assert::same('messageBus', $descriptor->getOption('bus')); + Assert::same(null, $descriptor->getOption('alias')); + Assert::same('whenFooMessageReceived', $descriptor->getOption('method')); + Assert::same(FooMessage::class, $descriptor->getOption('handles')); + Assert::same(0, $descriptor->getOption('priority')); + Assert::same(null, $descriptor->getOption('from_transport')); + + $descriptor = getHandlerDescriptor($container, new BarMessage('test')); + Assert::same('messageBus', $descriptor->getOption('bus')); + Assert::same(null, $descriptor->getOption('alias')); + Assert::same('whenBarMessageReceived', $descriptor->getOption('method')); + Assert::same(BarMessage::class, $descriptor->getOption('handles')); + Assert::same(0, $descriptor->getOption('priority')); + Assert::same(null, $descriptor->getOption('from_transport')); +}); + // Error: no invoke method Toolkit::test(function (): void { Assert::exception( @@ -59,27 +288,6 @@ Toolkit::test(function (): void { ); }); -// Error: multiple attributes -Toolkit::test(function (): void { - Assert::exception( - static function (): void { - Container::of() - ->withDefaults() - ->withCompiler(function (Compiler $compiler): void { - $compiler->addConfig(Helpers::neon(<<<'NEON' - messenger: - services: - - Tests\Mocks\Handler\MultipleAttributesHandler - NEON - )); - }) - ->build(); - }, - LogicalException::class, - 'Only attribute #[AsMessageHandler] can be used on class "Tests\Mocks\Handler\MultipleAttributesHandler"' - ); -}); - // Error: multiple parameters handler Toolkit::test(function (): void { Assert::exception( @@ -100,3 +308,14 @@ Toolkit::test(function (): void { 'Only one parameter is allowed in "Tests\Mocks\Handler\MultipleParametersHandler::__invoke()."' ); }); + +function getHandlerDescriptor(NetteContainer $container, object $message, string $busName = 'messageBus'): HandlerDescriptor +{ + /** @var HandlersLocatorInterface $handlerLocator */ + $handlerLocator = $container->getByName(sprintf('messenger.bus.%s.locator', $busName)); + /** @var HandlerDescriptor $handlerDescriptor */ + $handlerDescriptor = $handlerLocator->getHandlers(new Envelope($message))[0] ?? null; + Assert::notNull($handlerDescriptor); + + return $handlerDescriptor; +} diff --git a/tests/Mocks/Handler/MultipleAttributesHandler.php b/tests/Mocks/Handler/MultipleAttributesHandler.php deleted file mode 100644 index 799e9c1..0000000 --- a/tests/Mocks/Handler/MultipleAttributesHandler.php +++ /dev/null @@ -1,12 +0,0 @@ -message = $message; + } + +} diff --git a/tests/Mocks/Handler/NonDefaultMethodWithoutAttributeHandler.php b/tests/Mocks/Handler/NonDefaultMethodWithoutAttributeHandler.php new file mode 100644 index 0000000..fbd939f --- /dev/null +++ b/tests/Mocks/Handler/NonDefaultMethodWithoutAttributeHandler.php @@ -0,0 +1,17 @@ +message = $message; + } + +} diff --git a/tests/Mocks/Message/BarMessage.php b/tests/Mocks/Message/BarMessage.php new file mode 100644 index 0000000..77193fb --- /dev/null +++ b/tests/Mocks/Message/BarMessage.php @@ -0,0 +1,15 @@ +text = $text; + } + +} diff --git a/tests/Mocks/Message/FooMessage.php b/tests/Mocks/Message/FooMessage.php new file mode 100644 index 0000000..6979011 --- /dev/null +++ b/tests/Mocks/Message/FooMessage.php @@ -0,0 +1,15 @@ +text = $text; + } + +}