Skip to content

Commit

Permalink
add support for multiple messages in one handler
Browse files Browse the repository at this point in the history
  • Loading branch information
David Kurka committed Aug 16, 2023
1 parent f99f6aa commit 350d59f
Show file tree
Hide file tree
Showing 12 changed files with 509 additions and 103 deletions.
62 changes: 51 additions & 11 deletions .docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,30 +217,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).<br>
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
<?php declare(strict_types = 1);

namespace App\Domain;
namespace App;

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class SimpleMessageHandler
{

public function __invoke(SimpleMessage $message): void
{
// Do your magic
}
}
```

It is also possible to handle multiple different kinds of messages in a single handler by defining 2 or more methods in it.
Both the neon tag [`contributte.messenger.handler`] and symfony attribute [`#[AsMessageHandler]`] support this kind of handler setup.
```neon
services:
-
class: App\MultipleMessagesHandler
tags:
contributte.messenger.handler:
-
method: whenFooMessageReceived
-
method: whenBarMessageReceived
```
```php
<?php declare(strict_types = 1);

namespace App;

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

final class MultipleMessagesHandler
{
#[AsMessageHandler]
public function whenFooMessageReceived(FooMessage $message): void
{
// Do your magic
}

#[AsMessageHandler]
public function whenBarMessageReceived(BarMessage $message): void
{
// Do your magic
}
}
```

Expand Down Expand Up @@ -294,11 +339,6 @@ extensions:
- No fallbackBus in RoutableMessageBus.
- No debug console commands.

**No ETA**

- MessageHandler can handle only 1 message.
- MessageHandler can have only `__invoke` method.

## Examples

### 1. Manual example
Expand Down
135 changes: 88 additions & 47 deletions src/DI/Pass/HandlerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
use Contributte\Messenger\DI\MessengerExtension;
use Contributte\Messenger\DI\Utils\Reflector;
use Contributte\Messenger\Exception\LogicalException;
use Nette\DI\Definitions\Definition;
use Nette\DI\Definitions\ServiceDefinition;
use ReflectionClass;
use ReflectionException;
use stdClass;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Tracy\Debugger;

Check failure on line 13 in src/DI/Pass/HandlerPass.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Type Tracy\Debugger is not used in this file.

class HandlerPass extends AbstractPass
{

Check failure on line 16 in src/DI/Pass/HandlerPass.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

There must be one empty line after class opening brace.
private const DEFAULT_METHOD_NAME = '__invoke';
private const DEFAULT_PRIORITY = 0;

/**
* Register services
Expand Down Expand Up @@ -46,56 +48,27 @@ public function beforePassCompile(): void

// Ensure handler class exists
try {
$rc = new ReflectionClass($serviceClass);
new ReflectionClass($serviceClass);
} catch (ReflectionException $e) {
throw new LogicalException(sprintf('Handler "%s" class not found', $serviceClass), 0, $e);
}

// Drain service tag
$tag = (array) $serviceDef->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 AsMessageHandler[] $attributes */
$attributes = $rc->getAttributes(AsMessageHandler::class);
$attributeHandler = $attributes[0] ?? 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) {

Check failure on line 59 in src/DI/Pass/HandlerPass.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Function \array_merge() should not be referenced via a fully qualified name, but via a use statement.
// Autodetect handled message
if (!isset($options['handles'])) {
$options['handles'] = Reflector::getMessageHandlerMessage($serviceClass, $options);
}

// If handler is not for current bus, then skip it
if ($options['bus'] !== $busName) {
continue;
}

$handlers[$options['handles']][$options['priority']][] = $options;
$handlers[$options['handles']][$options['priority']][] = $options;
}
}

// Sort handlers by priority
Expand Down Expand Up @@ -138,7 +111,7 @@ private function getMessageHandlers(): array
}

// Skip services without attribute
if (Reflector::getMessageHandler($class) === null) {
if (Reflector::getMessageHandlers($class) === []) {
continue;
}

Expand All @@ -149,4 +122,72 @@ private function getMessageHandlers(): array
return array_unique($serviceHandlers);
}

/**
* @return list<array{
* service: string,
* bus: string,
* alias: string|null,
* method: string,
* handles: string|null,
* priority: int,
* from_transport: string|null
* }>
*/
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<array<mixed>> $tags */
$tags = $isList ? $tags : [$tags];
$tagsOptions = [];

foreach ($tags as $tag) {
$tagsOptions[] = [
'service' => $serviceName,
'bus' => isset($tag['bus']) && \is_string($tag['bus']) ? $tag['bus'] : $defaultBusName,

Check failure on line 148 in src/DI/Pass/HandlerPass.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Function \is_string() should not be referenced via a fully qualified name, but via a use statement.
'alias' => isset($tag['alias']) && \is_string($tag['alias']) ? $tag['alias'] : null,

Check failure on line 149 in src/DI/Pass/HandlerPass.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Function \is_string() should not be referenced via a fully qualified name, but via a use statement.
'method' => isset($tag['method']) && \is_string($tag['method']) ? $tag['method'] : self::DEFAULT_METHOD_NAME,

Check failure on line 150 in src/DI/Pass/HandlerPass.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Function \is_string() should not be referenced via a fully qualified name, but via a use statement.
'handles' => isset($tag['handles']) && \is_string($tag['handles']) ? $tag['handles'] : null,

Check failure on line 151 in src/DI/Pass/HandlerPass.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Function \is_string() should not be referenced via a fully qualified name, but via a use statement.
'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<array{
* service: string,
* bus: string,
* alias: null,
* method: string,
* priority: int,
* handles: string|null,
* from_transport: string|null
* }>
*/
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;
}
}
29 changes: 19 additions & 10 deletions src/DI/Utils/Reflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Contributte\Messenger\DI\Utils;

use Contributte\Messenger\Exception\LogicalException;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionIntersectionType;
Expand All @@ -15,24 +16,32 @@ final class Reflector

/**
* @param class-string $class
* @return array<AsMessageHandler>
*/
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(

Check failure on line 25 in src/DI/Utils/Reflector.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Function \array_map() should not be referenced via a fully qualified name, but via a use statement.
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 {

Check failure on line 34 in src/DI/Utils/Reflector.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Expected 1 space after USE keyword; found 0
$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);

Check failure on line 44 in src/DI/Utils/Reflector.php

View workflow job for this annotation

GitHub Actions / Codesniffer / Codesniffer (8.1)

Function \array_merge() should not be referenced via a fully qualified name, but via a use statement.
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/Handler/ContainerServiceHandlersLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public function __construct(array $map, Container $context)
public function getHandlers(Envelope $envelope): iterable
{
$handlers = [];

$class = get_class($envelope->getMessage());

foreach ($this->map[$class] ?? [] as $mapConfig) {
Expand Down
Loading

0 comments on commit 350d59f

Please sign in to comment.