Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit dd1aaa7

Browse files
authored
Support for Messenger HandleTrait return types
1 parent c7b7e7f commit dd1aaa7

12 files changed

+483
-2
lines changed

‎extension.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,13 @@ services:
140140
-
141141
factory: @symfony.parameterMapFactory::create()
142142

143+
# message map
144+
symfony.messageMapFactory:
145+
class: PHPStan\Symfony\MessageMapFactory
146+
factory: PHPStan\Symfony\MessageMapFactory
147+
-
148+
factory: @symfony.messageMapFactory::create()
149+
143150
# ControllerTrait::get()/has() return type
144151
-
145152
factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface)
@@ -203,6 +210,11 @@ services:
203210
factory: PHPStan\Type\Symfony\EnvelopeReturnTypeExtension
204211
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
205212

213+
# Messenger HandleTrait::handle() return type
214+
-
215+
class: PHPStan\Type\Symfony\MessengerHandleTraitReturnTypeExtension
216+
tags: [phpstan.broker.expressionTypeResolverExtension]
217+
206218
# InputInterface::getArgument() return type
207219
-
208220
factory: PHPStan\Type\Symfony\InputInterfaceGetArgumentDynamicReturnTypeExtension

‎src/Symfony/MessageMap.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\Type\Type;
6+
7+
final class MessageMap
8+
{
9+
10+
/** @var array<string, Type> */
11+
private $messageMap;
12+
13+
/** @param array<string, Type> $messageMap */
14+
public function __construct(array $messageMap)
15+
{
16+
$this->messageMap = $messageMap;
17+
}
18+
19+
public function getTypeForClass(string $class): ?Type
20+
{
21+
return $this->messageMap[$class] ?? null;
22+
}
23+
24+
}

‎src/Symfony/MessageMapFactory.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
use PHPStan\Reflection\ReflectionProvider;
7+
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
8+
use function class_exists;
9+
use function count;
10+
use function is_array;
11+
use function is_int;
12+
use function is_string;
13+
14+
final class MessageMapFactory
15+
{
16+
17+
private const MESSENGER_HANDLER_TAG = 'messenger.message_handler';
18+
private const DEFAULT_HANDLER_METHOD = '__invoke';
19+
20+
/** @var ReflectionProvider */
21+
private $reflectionProvider;
22+
23+
/** @var ServiceMap */
24+
private $serviceMap;
25+
26+
public function __construct(ServiceMap $symfonyServiceMap, ReflectionProvider $reflectionProvider)
27+
{
28+
$this->serviceMap = $symfonyServiceMap;
29+
$this->reflectionProvider = $reflectionProvider;
30+
}
31+
32+
public function create(): MessageMap
33+
{
34+
$returnTypesMap = [];
35+
36+
foreach ($this->serviceMap->getServices() as $service) {
37+
$serviceClass = $service->getClass();
38+
39+
if ($serviceClass === null) {
40+
continue;
41+
}
42+
43+
foreach ($service->getTags() as $tag) {
44+
if ($tag->getName() !== self::MESSENGER_HANDLER_TAG) {
45+
continue;
46+
}
47+
48+
if (!$this->reflectionProvider->hasClass($serviceClass)) {
49+
continue;
50+
}
51+
52+
$reflectionClass = $this->reflectionProvider->getClass($serviceClass);
53+
54+
/** @var array{handles?: class-string, method?: string} $tagAttributes */
55+
$tagAttributes = $tag->getAttributes();
56+
57+
if (isset($tagAttributes['handles'])) {
58+
$handles = [$tagAttributes['handles'] => ['method' => $tagAttributes['method'] ?? self::DEFAULT_HANDLER_METHOD]];
59+
} else {
60+
$handles = $this->guessHandledMessages($reflectionClass);
61+
}
62+
63+
foreach ($handles as $messageClassName => $options) {
64+
$methodName = $options['method'] ?? self::DEFAULT_HANDLER_METHOD;
65+
66+
if (!$reflectionClass->hasNativeMethod($methodName)) {
67+
continue;
68+
}
69+
70+
$methodReflection = $reflectionClass->getNativeMethod($methodName);
71+
72+
foreach ($methodReflection->getVariants() as $variant) {
73+
$returnTypesMap[$messageClassName][] = $variant->getReturnType();
74+
}
75+
}
76+
}
77+
}
78+
79+
$messageMap = [];
80+
foreach ($returnTypesMap as $messageClassName => $returnTypes) {
81+
if (count($returnTypes) !== 1) {
82+
continue;
83+
}
84+
85+
$messageMap[$messageClassName] = $returnTypes[0];
86+
}
87+
88+
return new MessageMap($messageMap);
89+
}
90+
91+
/** @return iterable<string, array<string, string>> */
92+
private function guessHandledMessages(ClassReflection $reflectionClass): iterable
93+
{
94+
if ($reflectionClass->implementsInterface(MessageSubscriberInterface::class)) {
95+
$className = $reflectionClass->getName();
96+
97+
foreach ($className::getHandledMessages() as $index => $value) {
98+
$containOptions = self::containOptions($index, $value);
99+
if ($containOptions === true) {
100+
yield $index => $value;
101+
} elseif ($containOptions === false) {
102+
yield $value => ['method' => self::DEFAULT_HANDLER_METHOD];
103+
}
104+
}
105+
106+
return;
107+
}
108+
109+
if (!$reflectionClass->hasNativeMethod(self::DEFAULT_HANDLER_METHOD)) {
110+
return;
111+
}
112+
113+
$methodReflection = $reflectionClass->getNativeMethod(self::DEFAULT_HANDLER_METHOD);
114+
115+
$variants = $methodReflection->getVariants();
116+
if (count($variants) !== 1) {
117+
return;
118+
}
119+
120+
$parameters = $variants[0]->getParameters();
121+
122+
if (count($parameters) !== 1) {
123+
return;
124+
}
125+
126+
$classNames = $parameters[0]->getType()->getObjectClassNames();
127+
128+
if (count($classNames) !== 1) {
129+
return;
130+
}
131+
132+
yield $classNames[0] => ['method' => self::DEFAULT_HANDLER_METHOD];
133+
}
134+
135+
/**
136+
* @param mixed $index
137+
* @param mixed $value
138+
* @phpstan-assert-if-true =class-string $index
139+
* @phpstan-assert-if-true =array<string, mixed> $value
140+
* @phpstan-assert-if-false =int $index
141+
* @phpstan-assert-if-false =class-string $value
142+
*/
143+
private static function containOptions($index, $value): ?bool
144+
{
145+
if (is_string($index) && class_exists($index) && is_array($value)) {
146+
return true;
147+
} elseif (is_int($index) && is_string($value) && class_exists($value)) {
148+
return false;
149+
}
150+
151+
return null;
152+
}
153+
154+
}

‎src/Symfony/Service.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,25 @@ final class Service implements ServiceDefinition
2020
/** @var string|null */
2121
private $alias;
2222

23+
/** @var ServiceTag[] */
24+
private $tags;
25+
26+
/** @param ServiceTag[] $tags */
2327
public function __construct(
2428
string $id,
2529
?string $class,
2630
bool $public,
2731
bool $synthetic,
28-
?string $alias
32+
?string $alias,
33+
array $tags = []
2934
)
3035
{
3136
$this->id = $id;
3237
$this->class = $class;
3338
$this->public = $public;
3439
$this->synthetic = $synthetic;
3540
$this->alias = $alias;
41+
$this->tags = $tags;
3642
}
3743

3844
public function getId(): string
@@ -60,4 +66,9 @@ public function getAlias(): ?string
6066
return $this->alias;
6167
}
6268

69+
public function getTags(): array
70+
{
71+
return $this->tags;
72+
}
73+
6374
}

‎src/Symfony/ServiceDefinition.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ public function isSynthetic(): bool;
1818

1919
public function getAlias(): ?string;
2020

21+
/** @return ServiceTag[] */
22+
public function getTags(): array;
23+
2124
}

‎src/Symfony/ServiceTag.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
final class ServiceTag implements ServiceTagDefinition
6+
{
7+
8+
/** @var string */
9+
private $name;
10+
11+
/** @var array<string, string> */
12+
private $attributes;
13+
14+
/** @param array<string, string> $attributes */
15+
public function __construct(string $name, array $attributes = [])
16+
{
17+
$this->name = $name;
18+
$this->attributes = $attributes;
19+
}
20+
21+
public function getName(): string
22+
{
23+
return $this->name;
24+
}
25+
26+
public function getAttributes(): array
27+
{
28+
return $this->attributes;
29+
}
30+
31+
}

‎src/Symfony/ServiceTagDefinition.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
interface ServiceTagDefinition
6+
{
7+
8+
public function getName(): string;
9+
10+
/** @return array<string, string> */
11+
public function getAttributes(): array;
12+
13+
}

‎src/Symfony/XmlServiceMapFactory.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,22 @@ public function create(): ServiceMap
4747
continue;
4848
}
4949

50+
$serviceTags = [];
51+
foreach ($def->tag as $tag) {
52+
$tagAttrs = ((array) $tag->attributes())['@attributes'] ?? [];
53+
$tagName = $tagAttrs['name'];
54+
unset($tagAttrs['name']);
55+
56+
$serviceTags[] = new ServiceTag($tagName, $tagAttrs);
57+
}
58+
5059
$service = new Service(
5160
$this->cleanServiceId((string) $attrs->id),
5261
isset($attrs->class) ? (string) $attrs->class : null,
5362
isset($attrs->public) && (string) $attrs->public === 'true',
5463
isset($attrs->synthetic) && (string) $attrs->synthetic === 'true',
55-
isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null
64+
isset($attrs->alias) ? $this->cleanServiceId((string) $attrs->alias) : null,
65+
$serviceTags
5666
);
5767

5868
if ($service->getAlias() !== null) {

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /