Skip to content

Commit 115785a

Browse files
author
Bartłomiej Nowak
committed
implementation
1 parent 9b62bd8 commit 115785a

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed

extension.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ parameters:
1111
constantHassers: true
1212
console_application_loader: null
1313
consoleApplicationLoader: null
14+
messenger:
15+
handleTraitWrappers:
16+
# move that params to tests only
17+
- MessengerHandleTrait\QueryBus::dispatch
18+
- MessengerHandleTrait\QueryBus2::dispatch
1419
featureToggles:
1520
skipCheckGenericClasses:
1621
- Symfony\Component\Form\AbstractType
@@ -115,6 +120,9 @@ parametersSchema:
115120
constantHassers: bool()
116121
console_application_loader: schema(string(), nullable())
117122
consoleApplicationLoader: schema(string(), nullable())
123+
messenger: structure([
124+
handleTraitWrappers: listOf(string())
125+
])
118126
])
119127

120128
services:
@@ -214,6 +222,11 @@ services:
214222
class: PHPStan\Type\Symfony\MessengerHandleTraitReturnTypeExtension
215223
tags: [phpstan.broker.expressionTypeResolverExtension]
216224

225+
# Messenger HandleTrait wrappers return type
226+
-
227+
class: PHPStan\Type\Symfony\MessengerHandleTraitWrapperReturnTypeExtension
228+
tags: [phpstan.broker.expressionTypeResolverExtension]
229+
217230
# InputInterface::getArgument() return type
218231
-
219232
factory: PHPStan\Type\Symfony\InputInterfaceGetArgumentDynamicReturnTypeExtension

src/Symfony/Configuration.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,12 @@ public function getConsoleApplicationLoader(): ?string
3131
return $this->parameters['consoleApplicationLoader'] ?? $this->parameters['console_application_loader'] ?? null;
3232
}
3333

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

tests/Type/Symfony/data/messenger_handle_trait.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ public function dispatch(object $query): mixed
121121
}
122122
}
123123

124+
class QueryBus2 {
125+
use HandleTrait;
126+
127+
public function dispatch(object $query): mixed
128+
{
129+
return $this->handle($query);
130+
}
131+
}
132+
124133
class Controller {
125134
public function action()
126135
{
@@ -138,5 +147,8 @@ public function action()
138147
// HandleTrait will throw exception in fact due to multiple handle methods/handlers per single query
139148
assertType('mixed', $queryBus->dispatch(new MultiHandlesForInTheSameHandlerQuery()));
140149
assertType('mixed', $queryBus->dispatch(new MultiHandlersForTheSameMessageQuery()));
150+
151+
$queryBus2 = new QueryBus2();
152+
assertType(TaggedResult::class, $queryBus2->dispatch(new TaggedQuery()));
141153
}
142154
}

0 commit comments

Comments
 (0)