Skip to content

Commit fa54bd1

Browse files
committed
Merge remote-tracking branch 'origin/1.4.x' into 2.0.x
2 parents 1ef4dce + dd1aaa7 commit fa54bd1

12 files changed

+482
-1
lines changed

extension.neon

+12
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ services:
122122
-
123123
factory: @symfony.parameterMapFactory::create()
124124

125+
# message map
126+
symfony.messageMapFactory:
127+
class: PHPStan\Symfony\MessageMapFactory
128+
factory: PHPStan\Symfony\MessageMapFactory
129+
-
130+
factory: @symfony.messageMapFactory::create()
131+
125132
# ControllerTrait::get()/has() return type
126133
-
127134
factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface)
@@ -185,6 +192,11 @@ services:
185192
factory: PHPStan\Type\Symfony\EnvelopeReturnTypeExtension
186193
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
187194

195+
# Messenger HandleTrait::handle() return type
196+
-
197+
class: PHPStan\Type\Symfony\MessengerHandleTraitReturnTypeExtension
198+
tags: [phpstan.broker.expressionTypeResolverExtension]
199+
188200
# InputInterface::getArgument() return type
189201
-
190202
factory: PHPStan\Type\Symfony\InputInterfaceGetArgumentDynamicReturnTypeExtension

src/Symfony/MessageMap.php

+24
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

+154
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

+12-1
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,25 @@ final class Service implements ServiceDefinition
1515

1616
private ?string $alias = null;
1717

18+
/** @var ServiceTag[] */
19+
private $tags;
20+
21+
/** @param ServiceTag[] $tags */
1822
public function __construct(
1923
string $id,
2024
?string $class,
2125
bool $public,
2226
bool $synthetic,
23-
?string $alias
27+
?string $alias,
28+
array $tags = []
2429
)
2530
{
2631
$this->id = $id;
2732
$this->class = $class;
2833
$this->public = $public;
2934
$this->synthetic = $synthetic;
3035
$this->alias = $alias;
36+
$this->tags = $tags;
3137
}
3238

3339
public function getId(): string
@@ -55,4 +61,9 @@ public function getAlias(): ?string
5561
return $this->alias;
5662
}
5763

64+
public function getTags(): array
65+
{
66+
return $this->tags;
67+
}
68+
5869
}

src/Symfony/ServiceDefinition.php

+3
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

+31
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

+13
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

+10
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,22 @@ public function create(): ServiceMap
4646
continue;
4747
}
4848

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

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

0 commit comments

Comments
 (0)