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 authored and f3l1x committed Jan 31, 2024
1 parent 77826be commit c2b6ada
Show file tree
Hide file tree
Showing 11 changed files with 521 additions and 104 deletions.
62 changes: 51 additions & 11 deletions .docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).<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 @@ -299,11 +344,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
140 changes: 91 additions & 49 deletions src/DI/Pass/HandlerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
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 ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use stdClass;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use function array_merge;
use function is_numeric;
use function is_string;

class HandlerPass extends AbstractPass
{

private const DEFAULT_METHOD_NAME = '__invoke';
private const DEFAULT_PRIORITY = 0;

/**
* Register services
*/
Expand Down Expand Up @@ -47,57 +51,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 array<ReflectionAttribute<AsMessageHandler>> $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
Expand Down Expand Up @@ -140,7 +114,7 @@ private function getMessageHandlers(): array
}

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

Expand All @@ -151,4 +125,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,
'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<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;
}

}
31 changes: 21 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 @@ -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<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(
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);
}

/**
Expand Down
Loading

0 comments on commit c2b6ada

Please sign in to comment.