-
Notifications
You must be signed in to change notification settings - Fork 92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Using messenger HandleTrait as QueryBus with appropriate result typing #424
Comments
@ondrejmirtes it's the following part of your request here 😉 |
anyone? any idea please? 😅 /cc @ondrejmirtes @shish @lookyman @VincentLanglet @ruudk @JanTvrdik @staabm |
This is how I do it in our project, where we have our own CommandBus interface (that has a Symfony Messenger implementation). You can use this as inspiration 😉 Our handlers implement a custom private function configureAttributes(ContainerBuilder $builder) : void
{
$builder->registerAttributeForAutoconfiguration(
AsCommandHandler::class,
function (ChildDefinition $definition, AsCommandHandler $attribute, ReflectionMethod $reflector) : void {
$methodParameters = $this->getMethodParameterTypes($reflector);
foreach ($methodParameters[0] as $commandClassName) {
$definition->addTag('command_handler', $tagAttributes); // This is the magic!
}
},
);
}
/**
* Returns a list of parameter types for the passed method. Some types can be union types,
* so each type is represented by a list.
*
* Example return values:
*
* [ [ UserCreatedEvent::class ] ]
* [ [ UserCreatedEvent::class, WhateverEvent::class, OtherEvent::class ] ]
* [ [ CreateUserCommand::class ] ]
* [ [ CreateUserCommand::class ], [ Actor::class ] ]
*
* Throws if any of the parameters does not have a type, or has a type that is neither simple nor union.
*
* @throws Exception
* @return list<list<class-string>>
*/
private function getMethodParameterTypes(ReflectionMethod $reflector) : array
{
$parameters = $reflector->getParameters();
$types = [];
foreach ($parameters as $parameter) {
if ($parameter->isOptional()) {
throw new Exception(sprintf(
'Method %s() must not have optional parameters',
Reflector::stringify($reflector),
));
}
$type = $parameter->getType();
if ($type === null) {
throw new Exception(sprintf(
'Parameter $%s of method %s() must have a type hint',
$parameters[0]->getName(),
Reflector::stringify($reflector),
));
}
if ( ! $type instanceof ReflectionNamedType && ! $type instanceof ReflectionUnionType) {
throw new Exception(sprintf(
'Parameter $%s of method %s() has invalid type hint',
$parameters[0]->getName(),
Reflector::stringify($reflector),
));
}
if ($type instanceof ReflectionUnionType) {
$types[] = ListHelper::map(fn($type) => $type->getName(), $type->getTypes());
} else {
$types[] = [$type->getName()];
}
}
return $types;
} Then we have a CompilerPass that writes the mapping to a parameter: final readonly class CollectCommandHandlersPass implements CompilerPassInterface
{
#[Override]
public function process(ContainerBuilder $container) : void
{
$commandToHandlerMapping = [];
foreach ($container->findTaggedServiceIds('command_handlers') as $id => $tags) {
foreach ($tags as $tag) {
$commandToHandlerMapping[$tag['handles']] = [$id, $tag['method']];
}
}
$container->setParameter('command_handlers', $commandToHandlerMapping);
}
} We have the following files for PHPStan. This reads the mapping from the container. <?php // src-dev/PHPStan/command-bus-mapping.php
declare(strict_types=1);
use TicketSwap\Kernel;
use TicketSwap\Shared\Infrastructure\Config\EnvironmentName;
require_once __DIR__ . '/../../autoload.php';
$env = EnvironmentName::Dev;
if (isset($_SERVER['APP_ENV'])) {
$env = EnvironmentName::create($_SERVER['APP_ENV']);
}
$kernel = new Kernel($env, true);
$kernel->boot();
return $kernel->getContainer()->getParameter('command_handlers'); <?php // src-dev/PHPStan/Extension/CommandBusMapping.php
declare(strict_types=1);
namespace Dev\PHPStan\Extension;
final readonly class CommandBusMapping
{
/**
* @return null|array{string, string}
*/
public function get(string $commandName) : ?array
{
static $mapping = include __DIR__ . '/../command-bus-mapping.php';
return $mapping[$commandName] ?? null;
}
} This is a return type extension that is able to figure out what <?php // src-dev/PHPStan/Extension/CommandBusReturnTypeExtension.php
declare(strict_types=1);
namespace Dev\PHPStan\Extension;
use Override;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;
use TicketSwap\Shared\Infrastructure\Messaging\MessageBus\CommandBus;
final readonly class CommandBusReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(
private ReflectionProvider $reflectionProvider,
private CommandBusMapping $commandBusMapping,
) {}
#[Override]
public function getClass() : string
{
return CommandBus::class;
}
#[Override]
public function isMethodSupported(MethodReflection $methodReflection) : bool
{
return $methodReflection->getName() === 'handle';
}
#[Override]
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
) : ?Type {
if ( ! $methodCall->args[0] instanceof Arg) {
return null;
}
$classNames = $scope->getType($methodCall->args[0]->value)->getObjectClassNames();
if (count($classNames) !== 1) {
return null;
}
$handlerAndMethod = $this->commandBusMapping->get($classNames[0]);
if ($handlerAndMethod === null) {
return null;
}
[$handler, $method] = $handlerAndMethod;
if ( ! $this->reflectionProvider->hasClass($handler)) {
return null;
}
$handlerReflection = $this->reflectionProvider->getClass($handler);
if ( ! $handlerReflection->hasMethod($method)) {
return null;
}
$methodReflection = $handlerReflection->getMethod($method, $scope);
return ParametersAcceptorSelector::selectFromArgs(
$scope,
$methodCall->getArgs(),
$methodReflection->getVariants(),
)->getReturnType();
}
} <?php // src-dev/PHPStan/Extension/CommandBusThrowTypeExtension.php
declare(strict_types=1);
namespace Dev\PHPStan\Extension;
use Override;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\DynamicMethodThrowTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use TicketSwap\Shared\Infrastructure\Messaging\MessageBus\Attribute\IgnoreException;
use TicketSwap\Shared\Infrastructure\Messaging\MessageBus\CommandBus;
final readonly class CommandBusThrowTypeExtension implements DynamicMethodThrowTypeExtension
{
public function __construct(
private ReflectionProvider $reflectionProvider,
private CommandBusMapping $commandBusMapping,
) {}
#[Override]
public function isMethodSupported(MethodReflection $methodReflection) : bool
{
return $methodReflection->getDeclaringClass()->is(CommandBus::class) && $methodReflection->getName() === 'handle';
}
#[Override]
public function getThrowTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope,
) : ?Type {
$defaultThrowType = $methodReflection->getThrowType();
if ( ! $methodCall->args[0] instanceof Arg) {
return $defaultThrowType;
}
$commandNames = $scope->getType($methodCall->args[0]->value)->getObjectClassNames();
if ($commandNames === []) {
return $defaultThrowType;
}
$throwTypes = [];
if ($defaultThrowType !== null) {
$throwTypes[] = $defaultThrowType;
}
foreach ($commandNames as $commandName) {
$throwType = $this->getThrowTypeForCommand($commandName, $scope);
if ($throwType === null) {
continue;
}
$throwTypes[] = $throwType;
}
return TypeCombinator::union(...$throwTypes);
}
private function getThrowTypeForCommand(string $commandName, Scope $scope) : ?Type
{
$handlerAndMethod = $this->commandBusMapping->get($commandName);
if ($handlerAndMethod === null) {
return null;
}
[$handler, $method] = $handlerAndMethod;
if ( ! $this->reflectionProvider->hasClass($handler)) {
return null;
}
$handlerReflection = $this->reflectionProvider->getClass($handler);
if ( ! $handlerReflection->hasMethod($method)) {
return null;
}
$methodReflection = $handlerReflection->getMethod($method, $scope);
$throwType = $methodReflection->getThrowType();
if ($throwType === null) {
return null;
}
$ignoreExceptions = $handlerReflection
->getNativeReflection()
->getMethod($method)
->getAttributes(IgnoreException::class);
foreach ($ignoreExceptions as $ignoreException) {
if ( ! isset($ignoreException->getArguments()[0])) {
continue;
}
if ( ! is_string($ignoreException->getArguments()[0])) {
continue;
}
$throwType = TypeCombinator::remove($throwType, new ObjectType($ignoreException->getArguments()[0]));
}
return $throwType;
}
} |
Thank you @ruudk for your detailed response and sorry for my late reply. In fact, your solution is pretty similar to what I already done here. Ideally, I'd like to reuse somehow already created map (internally in that plugin) to "learn" any bus classes (which return some single result using |
Recently, my PR about support for Messenger HandleTrait return types was merged. I would like to use that work done to ability for QueryBus classes to "learn them" how to determine their results correctly, wherever they are called.
There's related PR to this missing functionality, which demonstrate what I want to achieve. The most reasonably (and the easiest way of doing it) from my understanding would be getting query-result mapping (which
HandleTrait::handle
method is aware now), and reusing it as the same input-output values forQueryBus::dispatch
method (from the PR's example). Perhaps, using some annotations?Maybe it is trivial to complete, but at the moment I do not know how to connect these dots 😅
I will try to push this topic forward, but any thoughts/ideas are welcome :)
The text was updated successfully, but these errors were encountered: