Skip to content

Commit 0d3480f

Browse files
author
Bartłomiej Nowak
committed
base version of handling query result
1 parent ee88a01 commit 0d3480f

13 files changed

+418
-2
lines changed

extension.neon

+12
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ services:
137137
-
138138
factory: @symfony.parameterMapFactory::create()
139139

140+
# message map
141+
symfony.messageMapFactory:
142+
class: PHPStan\Symfony\MessageMapFactory
143+
factory: PHPStan\Symfony\MessageMapFactory
144+
-
145+
factory: @symfony.messageMapFactory::create()
146+
140147
# ControllerTrait::get()/has() return type
141148
-
142149
factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface)
@@ -200,6 +207,11 @@ services:
200207
factory: PHPStan\Type\Symfony\EnvelopeReturnTypeExtension
201208
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
202209

210+
# Messenger HandleTrait::handle() return type
211+
-
212+
factory: PHPStan\Type\Symfony\MessengerHandleTraitDynamicReturnTypeExtension
213+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
214+
203215
# InputInterface::getArgument() return type
204216
-
205217
factory: PHPStan\Type\Symfony\InputInterfaceGetArgumentDynamicReturnTypeExtension

src/Symfony/Message.php

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPStan\Symfony;
6+
7+
final class Message
8+
{
9+
/** @var string */
10+
private $class;
11+
12+
/** @var array */
13+
private $returnTypes;
14+
15+
public function __construct(string $class, array $returnTypes)
16+
{
17+
$this->class = $class;
18+
$this->returnTypes = $returnTypes;
19+
}
20+
21+
public function getClass(): string
22+
{
23+
return $this->class;
24+
}
25+
26+
public function getReturnTypes(): array
27+
{
28+
return $this->returnTypes;
29+
}
30+
31+
public function countReturnTypes(): int
32+
{
33+
return count($this->returnTypes);
34+
}
35+
}

src/Symfony/MessageMap.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPStan\Symfony;
6+
7+
final class MessageMap
8+
{
9+
/** @var Message[] */
10+
private $messages = [];
11+
12+
/**
13+
* @param Message[] $messages
14+
*/
15+
public function __construct(array $messages)
16+
{
17+
foreach ($messages as $message) {
18+
$this->messages[$message->getClass()] = $message;
19+
}
20+
}
21+
22+
public function getMessageForClass(string $class): ?Message
23+
{
24+
return $this->messages[$class] ?? null;
25+
}
26+
27+
public function hasMessageForClass(string $class): bool
28+
{
29+
return array_key_exists($class, $this->messages);
30+
}
31+
}

src/Symfony/MessageMapFactory.php

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
use PHPStan\Reflection\ParametersAcceptorSelector;
7+
use PHPStan\Reflection\ReflectionProvider;
8+
use PHPStan\Type\ObjectType;
9+
use PHPStan\Type\UnionType;
10+
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
11+
12+
// todo add tests
13+
final class MessageMapFactory
14+
{
15+
/** @var ReflectionProvider */
16+
private $reflectionProvider;
17+
18+
/** @var ServiceMap */
19+
private $serviceMap;
20+
21+
public function __construct(ServiceMap $symfonyServiceMap, ReflectionProvider $reflectionProvider)
22+
{
23+
$this->serviceMap = $symfonyServiceMap;
24+
$this->reflectionProvider = $reflectionProvider;
25+
}
26+
27+
public function create(): MessageMap
28+
{
29+
$returnTypesMap = [];
30+
31+
foreach ($this->serviceMap->getServices() as $service) {
32+
$serviceClass = $service->getClass();
33+
34+
// todo handle abstract/parent services somehow?
35+
if (is_null($serviceClass)) {
36+
continue;
37+
}
38+
39+
foreach ($service->getTags() as $tag) {
40+
// todo could there be more tags with the same name for the same service?
41+
// todo check if message handler tag name is constant or configurable
42+
if ($tag->getName() !== 'messenger.message_handler') {
43+
continue;
44+
}
45+
46+
$tagAttributes = $tag->getAttributes();
47+
$reflectionClass = $this->reflectionProvider->getClass($serviceClass);
48+
49+
if (isset($tagAttributes['handles'])) {
50+
$handles = isset($tag['method']) ? [$tag['handles'] => $tag['method']] : [$tag['handles']];
51+
} else {
52+
$handles = $this->guessHandledMessages($reflectionClass);
53+
}
54+
55+
foreach ($handles as $messageClassName => $options) {
56+
if (\is_int($messageClassName) && \is_string($options)) {
57+
$messageClassName = $options;
58+
$options = [];
59+
}
60+
61+
$options['method'] = $options['method'] ?? '__invoke';
62+
63+
$methodReflection = $reflectionClass->getNativeMethod($options['method']);
64+
$variant = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
65+
66+
$returnTypesMap[$messageClassName][] = $variant->getReturnType();
67+
}
68+
}
69+
}
70+
71+
$messages = [];
72+
foreach ($returnTypesMap as $messageClassName => $returnTypes) {
73+
$messages[] = new Message($messageClassName, $returnTypes);
74+
}
75+
76+
return new MessageMap($messages);
77+
}
78+
79+
private function guessHandledMessages(ClassReflection $reflectionClass): iterable
80+
{
81+
if ($reflectionClass->implementsInterface(MessageSubscriberInterface::class)) {
82+
// todo handle different return formats
83+
return $reflectionClass->getName()::getHandledMessages();
84+
}
85+
86+
// todo handle if doesn't exists
87+
$methodReflection = $reflectionClass->getNativeMethod('__invoke');
88+
89+
$variant = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
90+
$parameters = $variant->getParameters();
91+
92+
if (1 !== count($parameters)) {
93+
// todo handle error
94+
throw new \RuntimeException('invalid handler');
95+
}
96+
97+
$type = $parameters[0]->getType();
98+
99+
if ($type instanceof UnionType) {
100+
$types = [];
101+
foreach ($type->getTypes() as $type) {
102+
if (!$type instanceof ObjectType) {
103+
// todo handle error
104+
throw new \RuntimeException('invalid handler');
105+
}
106+
107+
$types[] = $type->getClassName();
108+
}
109+
110+
if ($types) {
111+
return $types;
112+
}
113+
114+
// todo handle error
115+
throw new \RuntimeException('invalid handler');
116+
}
117+
118+
if (!$type instanceof ObjectType) {
119+
throw new \RuntimeException('invalid handler');
120+
}
121+
122+
return [$type->getClassName()];
123+
}
124+
}

src/Symfony/Service.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,24 @@ final class Service implements ServiceDefinition
2020
/** @var string|null */
2121
private $alias;
2222

23+
/** @var array */
24+
private $tags;
25+
2326
public function __construct(
2427
string $id,
2528
?string $class,
2629
bool $public,
2730
bool $synthetic,
28-
?string $alias
31+
?string $alias,
32+
array $tags = []
2933
)
3034
{
3135
$this->id = $id;
3236
$this->class = $class;
3337
$this->public = $public;
3438
$this->synthetic = $synthetic;
3539
$this->alias = $alias;
40+
$this->tags = $tags;
3641
}
3742

3843
public function getId(): string
@@ -60,4 +65,8 @@ public function getAlias(): ?string
6065
return $this->alias;
6166
}
6267

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

src/Symfony/ServiceDefinition.php

+1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ public function isSynthetic(): bool;
1515

1616
public function getAlias(): ?string;
1717

18+
public function getTags(): array;
1819
}

src/Symfony/ServiceTag.php

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

src/Symfony/ServiceTagDefinition.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
interface ServiceTagDefinition
6+
{
7+
public function getName(): string;
8+
9+
public function getAttributes(): array;
10+
}

src/Symfony/XmlServiceMapFactory.php

+11-1
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)