Created
August 30, 2024 11:20
-
-
Save ephrin/a8fc4c572a60d083b5143239979a3236 to your computer and use it in GitHub Desktop.
bus with smart handling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class MessageBus | |
{ | |
private LoggerInterface $logger; | |
public function __construct(LoggerInterface $logger = null) | |
{ | |
$this->logger = $logger ?? new NullLogger(); | |
} | |
const ALL_EVENTS = '*'; | |
private array $handlers = []; | |
public function register($handler) | |
{ | |
$this->logger->info('Registering handler', ['handler' => $this->callableName($handler)]); | |
// regular object | |
if (!$handler instanceof \Closure && is_object($handler)) { | |
$reflectionClass = new ReflectionClass(get_class($handler)); | |
if ($handler instanceof MessageHandlerInterface) { | |
$reflectionCallable = $reflectionClass->getMethod('handle'); | |
} elseif ($reflectionClass->hasMethod('__invoke')) { | |
$reflectionCallable = $reflectionClass->getMethod('__invoke'); | |
} else { | |
throw new InvalidArgumentException( | |
sprintf('Handler object must have __invoke method or implement %s', MessageHandlerInterface::class) | |
); | |
} | |
} elseif (is_callable($handler)) { // \Closure and other callables | |
$reflectionCallable = new ReflectionFunction($handler); | |
} else { | |
throw new InvalidArgumentException( | |
sprintf( | |
'Handler must be a callable or an object implementing "%s" or have __invoke method', | |
MessageHandlerInterface::class | |
) | |
); | |
} | |
$handles = $this->extractHandles($reflectionCallable, $this->callableName($handler)); | |
foreach ($handles as $handlesType) { | |
$this->handlers[$handlesType][] = $handler; | |
$this->logger->info('Handler registered for type', ['type' => $handlesType]); | |
} | |
} | |
/** | |
* @throws ReflectionException | |
*/ | |
private static function callableName($handler): string | |
{ | |
if (is_array($handler)) { | |
if (is_object($handler[0])) { | |
return get_class($handler[0]) . '::' . $handler[1]; | |
} | |
return $handler[0] . '::' . $handler[1]; | |
} | |
if (is_string($handler)) { | |
return $handler; | |
} | |
if ($handler instanceof Closure) { | |
$r = new ReflectionFunction($handler); | |
if (false !== strpos($r->name, '{closure}')) { | |
return 'Closure'; | |
} | |
if ($class = $r->getClosureScopeClass()) { | |
return $class->name . '::' . $r->name; | |
} | |
return $r->name; | |
} | |
if ($handler instanceof MessageHandlerInterface) { | |
return get_class($handler) . '::handle'; | |
} | |
return get_class($handler) . '::__invoke'; | |
} | |
private function extractHandles(ReflectionFunctionAbstract $function, string $reference): array | |
{ | |
if (0 === $function->getNumberOfRequiredParameters()) { | |
throw new RuntimeException( | |
sprintf( | |
'Invalid handler "%s": callable requires at least one argument being the message it handles.', | |
$reference | |
) | |
); | |
} | |
$parameters = $function->getParameters(); | |
if (!$type = $parameters[0]->getType()) { | |
return [self::ALL_EVENTS]; | |
} | |
if ($type->isBuiltin()) { | |
throw new RuntimeException( | |
sprintf( | |
'Invalid handler "%s": type-hint of argument "$%s" in callable must be a class, "%s" given.', | |
$reference, | |
$parameters[0]->getName(), | |
$type instanceof ReflectionNamedType ? $type->getName() : (string)$type | |
) | |
); | |
} | |
return [$type->getName()]; | |
} | |
/** | |
* @throws ReflectionException | |
* @throws NotHandledMessageException | |
*/ | |
public function dispatch(object $message) | |
{ | |
$this->logger->info('Dispatching message', ['message' => get_class($message)]); | |
$seen = []; | |
foreach (self::listTypes($message) as $type) { | |
foreach ($this->handlers[$type] ?? [] as $handler) { | |
if (in_array($handler, $seen)) { | |
continue; | |
} | |
$seen[] = $handler; | |
if ($handler instanceof MessageHandlerInterface) { | |
if ($handler->supports($message)) { | |
$this->logger->debug('Handling message with handler', ['handler' => $this->callableName($handler)]); | |
$handler->handle($message); | |
} | |
} else { | |
$this->logger->debug('Handling message with callable', ['callable' => $this->callableName($handler)]); | |
$handler($message); | |
} | |
} | |
} | |
if (count($seen) === 0) { | |
$this->logger->error('No handler found for message type', ['message_type' => get_class($message)]); | |
throw new NotHandledMessageException('No handler found for message type ' . get_class($message)); | |
} | |
} | |
private static function listTypes(object $message): array | |
{ | |
$class = \get_class($message); | |
return [$class => $class] | |
+ class_parents($class) | |
+ class_implements($class) | |
+ [self::ALL_EVENTS => self::ALL_EVENTS]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment