Skip to content

Instantly share code, notes, and snippets.

@ephrin
Created August 30, 2024 11:20
Show Gist options
  • Save ephrin/a8fc4c572a60d083b5143239979a3236 to your computer and use it in GitHub Desktop.
Save ephrin/a8fc4c572a60d083b5143239979a3236 to your computer and use it in GitHub Desktop.
bus with smart handling
<?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