Skip to content

Commit 8a0a58d

Browse files
Bartłomiej Nowakbnowak
authored andcommitted
implementation
1 parent 5d0d321 commit 8a0a58d

3 files changed

Lines changed: 132 additions & 0 deletions

File tree

extension.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ parameters:
88
containerXmlPath: null
99
constantHassers: true
1010
consoleApplicationLoader: null
11+
messenger:
12+
handleTraitWrappers:
13+
# move that params to tests only
14+
- MessengerHandleTrait\QueryBus::dispatch
15+
- MessengerHandleTrait\QueryBus2::dispatch
1116
stubFiles:
1217
- stubs/Psr/Cache/CacheException.stub
1318
- stubs/Psr/Cache/CacheItemInterface.stub
@@ -96,6 +101,9 @@ parametersSchema:
96101
containerXmlPath: schema(string(), nullable())
97102
constantHassers: bool()
98103
consoleApplicationLoader: schema(string(), nullable())
104+
messenger: structure([
105+
handleTraitWrappers: listOf(string())
106+
])
99107
])
100108

101109
services:
@@ -203,6 +211,11 @@ services:
203211
class: PHPStan\Type\Symfony\MessengerHandleTraitReturnTypeExtension
204212
tags: [phpstan.broker.expressionTypeResolverExtension]
205213

214+
# Messenger HandleTrait wrappers return type
215+
-
216+
class: PHPStan\Type\Symfony\MessengerHandleTraitWrapperReturnTypeExtension
217+
tags: [phpstan.broker.expressionTypeResolverExtension]
218+
206219
# InputInterface::getArgument() return type
207220
-
208221
factory: PHPStan\Type\Symfony\InputInterfaceGetArgumentDynamicReturnTypeExtension
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Symfony;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\Node\Identifier;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Symfony\MessageMap;
10+
use PHPStan\Symfony\MessageMapFactory;
11+
use PHPStan\Type\ExpressionTypeResolverExtension;
12+
use PHPStan\Type\Type;
13+
use function count;
14+
use function in_array;
15+
use function is_null;
16+
17+
/**
18+
* Configurable extension for resolving return types of methods that internally use HandleTrait.
19+
*
20+
* Configured via PHPStan parameters under symfony.messenger.handleTraitWrappers with
21+
* "Class::method" patterns, e.g.:
22+
* - App\Bus\QueryBus::dispatch
23+
* - App\Bus\QueryBus::query
24+
* - App\Bus\CommandBus::execute
25+
* - App\Bus\CommandBus::handle
26+
*/
27+
final class MessengerHandleTraitWrapperReturnTypeExtension implements ExpressionTypeResolverExtension
28+
{
29+
30+
private MessageMapFactory $messageMapFactory;
31+
32+
private ?MessageMap $messageMap = null;
33+
34+
/** @var array<string> */
35+
private array $wrappers;
36+
37+
/** @param array{handleTraitWrappers: array<string>}|null $messenger */
38+
public function __construct(MessageMapFactory $messageMapFactory, ?array $messenger)
39+
{
40+
$this->messageMapFactory = $messageMapFactory;
41+
$this->wrappers = $messenger['handleTraitWrappers'] ?? [];
42+
}
43+
44+
public function getType(Expr $expr, Scope $scope): ?Type
45+
{
46+
if (!$this->isSupported($expr, $scope)) {
47+
return null;
48+
}
49+
50+
$args = $expr->getArgs();
51+
if (count($args) !== 1) {
52+
return null;
53+
}
54+
55+
$arg = $args[0]->value;
56+
$argClassNames = $scope->getType($arg)->getObjectClassNames();
57+
58+
if (count($argClassNames) === 1) {
59+
$messageMap = $this->getMessageMap();
60+
$returnType = $messageMap->getTypeForClass($argClassNames[0]);
61+
62+
if (!is_null($returnType)) {
63+
return $returnType;
64+
}
65+
}
66+
67+
return null;
68+
}
69+
70+
/**
71+
* @phpstan-assert-if-true =MethodCall $expr
72+
*/
73+
private function isSupported(Expr $expr, Scope $scope): bool
74+
{
75+
if ($this->wrappers === []) {
76+
return false;
77+
}
78+
79+
if (!($expr instanceof MethodCall) || !($expr->name instanceof Identifier)) {
80+
return false;
81+
}
82+
83+
$methodName = $expr->name->name;
84+
$varType = $scope->getType($expr->var);
85+
$classNames = $varType->getObjectClassNames();
86+
87+
if (count($classNames) !== 1) {
88+
return false;
89+
}
90+
91+
$className = $classNames[0];
92+
$classMethodCombination = $className . '::' . $methodName;
93+
94+
// Check if this class::method combination is configured
95+
return in_array($classMethodCombination, $this->wrappers, true);
96+
}
97+
98+
private function getMessageMap(): MessageMap
99+
{
100+
if ($this->messageMap === null) {
101+
$this->messageMap = $this->messageMapFactory->create();
102+
}
103+
104+
return $this->messageMap;
105+
}
106+
107+
}

tests/Type/Symfony/data/messenger_handle_trait.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ public function dispatch(object $query): mixed
6464
}
6565
}
6666

67+
class QueryBus2 {
68+
use HandleTrait;
69+
70+
public function dispatch(object $query): mixed
71+
{
72+
return $this->handle($query);
73+
}
74+
}
75+
6776
class Controller {
6877
public function action()
6978
{
@@ -81,5 +90,8 @@ public function action()
8190
// HandleTrait will throw exception in fact due to multiple handle methods/handlers per single query
8291
assertType('mixed', $queryBus->dispatch(new MultiHandlesForInTheSameHandlerQuery()));
8392
assertType('mixed', $queryBus->dispatch(new MultiHandlersForTheSameMessageQuery()));
93+
94+
$queryBus2 = new QueryBus2();
95+
assertType(TaggedResult::class, $queryBus2->dispatch(new TaggedQuery()));
8496
}
8597
}

0 commit comments

Comments
 (0)