Skip to content

Commit 10fc1c4

Browse files
committed
[TypeInfo] Add PhpDocAwareReflectionTypeResolver
1 parent 0f4cf9b commit 10fc1c4

File tree

8 files changed

+222
-34
lines changed

8 files changed

+222
-34
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Composer\InstalledVersions;
1515
use Http\Client\HttpAsyncClient;
1616
use Http\Client\HttpClient;
17+
use Symfony\Component\TypeInfo\TypeResolver\PhpDocAwareReflectionTypeResolver;
1718
use phpDocumentor\Reflection\DocBlockFactoryInterface;
1819
use phpDocumentor\Reflection\Types\ContextFactory;
1920
use PhpParser\Parser;
@@ -1974,11 +1975,21 @@ private function registerTypeInfoConfiguration(ContainerBuilder $container, PhpF
19741975
if (ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/type-info'])) {
19751976
$container->register('type_info.resolver.string', StringTypeResolver::class);
19761977

1978+
$container->register('type_info.resolver.reflection_parameter.phpdoc_aware', PhpDocAwareReflectionTypeResolver::class)
1979+
->setArguments([new Reference('type_info.resolver.reflection_parameter'), new Reference('type_info.resolver.string'), new Reference('type_info.type_context_factory')]);
1980+
$container->register('type_info.resolver.reflection_property.phpdoc_aware', PhpDocAwareReflectionTypeResolver::class)
1981+
->setArguments([new Reference('type_info.resolver.reflection_property'), new Reference('type_info.resolver.string'), new Reference('type_info.type_context_factory')]);
1982+
$container->register('type_info.resolver.reflection_return.phpdoc_aware', PhpDocAwareReflectionTypeResolver::class)
1983+
->setArguments([new Reference('type_info.resolver.reflection_return'), new Reference('type_info.resolver.string'), new Reference('type_info.type_context_factory')]);
1984+
19771985
/** @var ServiceLocatorArgument $resolversLocator */
19781986
$resolversLocator = $container->getDefinition('type_info.resolver')->getArgument(0);
1979-
$resolversLocator->setValues($resolversLocator->getValues() + [
1987+
$resolversLocator->setValues([
19801988
'string' => new Reference('type_info.resolver.string'),
1981-
]);
1989+
\ReflectionParameter::class => new Reference('type_info.resolver.reflection_parameter.phpdoc_aware'),
1990+
\ReflectionProperty::class => new Reference('type_info.resolver.reflection_property.phpdoc_aware'),
1991+
\ReflectionFunctionAbstract::class => new Reference('type_info.resolver.reflection_return.phpdoc_aware'),
1992+
] + $resolversLocator->getValues());
19821993
}
19831994
}
19841995

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
use Symfony\Component\String\Inflector\InflectorInterface;
2525
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
2626
use Symfony\Component\TypeInfo\Type;
27+
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
28+
use Symfony\Component\TypeInfo\TypeResolver\ReflectionParameterTypeResolver;
29+
use Symfony\Component\TypeInfo\TypeResolver\ReflectionPropertyTypeResolver;
30+
use Symfony\Component\TypeInfo\TypeResolver\ReflectionReturnTypeResolver;
31+
use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver;
2732
use Symfony\Component\TypeInfo\Type\CollectionType;
2833
use Symfony\Component\TypeInfo\TypeIdentifier;
2934
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
@@ -102,7 +107,14 @@ public function __construct(
102107
$this->methodReflectionFlags = $this->getMethodsFlags($accessFlags);
103108
$this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags);
104109
$this->inflector = $inflector ?? new EnglishInflector();
105-
$this->typeResolver = TypeResolver::create();
110+
111+
$typeContextFactory = new TypeContextFactory();
112+
$this->typeResolver = TypeResolver::create([
113+
\ReflectionType::class => $reflectionTypeResolver = new ReflectionTypeResolver(),
114+
\ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory),
115+
\ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory),
116+
\ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory),
117+
]);
106118

107119
$this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes));
108120
$this->arrayMutatorPrefixesLast = array_reverse($this->arrayMutatorPrefixesFirst);

src/Symfony/Component/TypeInfo/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.2
5+
---
6+
7+
* Add `PhpDocAwareReflectionTypeResolver` resolver
8+
49
7.1
510
---
611

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Symfony\Component\TypeInfo\Tests\Fixtures;
4+
5+
final class DummyWithPhpDoc
6+
{
7+
/**
8+
* @var array<Dummy>
9+
*/
10+
public mixed $arrayOfDummies = [];
11+
12+
/**
13+
* @param Dummy $dummy
14+
*
15+
* @return Dummy
16+
*/
17+
public function getNextDummy(mixed $dummy): mixed
18+
{
19+
throw new \BadMethodCallException(sprintf('"%s" is not implemented.', __METHOD__));
20+
}
21+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
16+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithPhpDoc;
17+
use Symfony\Component\TypeInfo\Type;
18+
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
19+
use Symfony\Component\TypeInfo\TypeResolver\PhpDocAwareReflectionTypeResolver;
20+
use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
21+
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
22+
23+
class PhpDocAwareReflectionTypeResolverTest extends TestCase
24+
{
25+
public function testReadPhpDoc()
26+
{
27+
$resolver = new PhpDocAwareReflectionTypeResolver(TypeResolver::create(), new StringTypeResolver(), new TypeContextFactory());
28+
$reflection = new \ReflectionClass(DummyWithPhpDoc::class);
29+
30+
$this->assertEquals(Type::array(Type::object(Dummy::class)), $resolver->resolve($reflection->getProperty('arrayOfDummies')));
31+
$this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy')));
32+
$this->assertEquals(Type::object(Dummy::class), $resolver->resolve($reflection->getMethod('getNextDummy')->getParameters()[0]));
33+
}
34+
35+
public function testFallbackWhenNoPhpDoc()
36+
{
37+
$resolver = new PhpDocAwareReflectionTypeResolver(TypeResolver::create(), new StringTypeResolver(), new TypeContextFactory());
38+
$reflection = new \ReflectionClass(Dummy::class);
39+
40+
$this->assertEquals(Type::int(), $resolver->resolve($reflection->getProperty('id')));
41+
$this->assertEquals(Type::int(), $resolver->resolve($reflection->getMethod('getId')));
42+
$this->assertEquals(Type::int(), $resolver->resolve($reflection->getMethod('setId')->getParameters()[0]));
43+
}
44+
}

src/Symfony/Component/TypeInfo/Tests/TypeResolver/TypeResolverTest.php

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
1313

1414
use PHPUnit\Framework\TestCase;
15-
use Symfony\Component\DependencyInjection\ServiceLocator;
1615
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
1716
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
1817
use Symfony\Component\TypeInfo\Type;
@@ -38,7 +37,7 @@ public function testCannotFindResolver()
3837
$this->expectException(UnsupportedException::class);
3938
$this->expectExceptionMessage('Cannot find any resolver for "int" type.');
4039

41-
$resolver = new TypeResolver(new ServiceLocator([]));
40+
$resolver = TypeResolver::create([]);
4241
$resolver->resolve(1);
4342
}
4443

@@ -59,13 +58,13 @@ public function testUseProperResolver()
5958
$reflectionReturnTypeResolver = $this->createMock(TypeResolverInterface::class);
6059
$reflectionReturnTypeResolver->method('resolve')->willReturn(Type::template('REFLECTION_RETURN_TYPE'));
6160

62-
$resolver = new TypeResolver(new ServiceLocator([
63-
'string' => fn () => $stringResolver,
64-
\ReflectionType::class => fn () => $reflectionTypeResolver,
65-
\ReflectionParameter::class => fn () => $reflectionParameterResolver,
66-
\ReflectionProperty::class => fn () => $reflectionPropertyResolver,
67-
\ReflectionFunctionAbstract::class => fn () => $reflectionReturnTypeResolver,
68-
]));
61+
$resolver = TypeResolver::create([
62+
'string' => $stringResolver,
63+
\ReflectionType::class => $reflectionTypeResolver,
64+
\ReflectionParameter::class => $reflectionParameterResolver,
65+
\ReflectionProperty::class => $reflectionPropertyResolver,
66+
\ReflectionFunctionAbstract::class => $reflectionReturnTypeResolver,
67+
]);
6968

7069
$this->assertEquals(Type::template('STRING'), $resolver->resolve('foo'));
7170
$this->assertEquals(
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\TypeInfo\TypeResolver;
13+
14+
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
15+
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
16+
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
17+
use PHPStan\PhpDocParser\Lexer\Lexer;
18+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
19+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
20+
use PHPStan\PhpDocParser\Parser\TokenIterator;
21+
use PHPStan\PhpDocParser\Parser\TypeParser;
22+
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
23+
use Symfony\Component\TypeInfo\Type;
24+
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
25+
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
26+
use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
27+
28+
/**
29+
* Resolves type on reflection prioriziting PHP documentation.
30+
*
31+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
32+
*
33+
* @internal
34+
*/
35+
final readonly class PhpDocAwareReflectionTypeResolver implements TypeResolverInterface
36+
{
37+
private PhpDocParser $phpDocParser;
38+
private Lexer $lexer;
39+
40+
public function __construct(
41+
private TypeResolverInterface $reflectionTypeResolver,
42+
private TypeResolverInterface $stringTypeResolver,
43+
private TypeContextFactory $typeContextFactory,
44+
) {
45+
$this->phpDocParser = new PhpDocParser(new TypeParser(), new ConstExprParser());
46+
$this->lexer = new Lexer();
47+
}
48+
49+
public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type
50+
{
51+
if (!$subject instanceof \ReflectionProperty && !$subject instanceof \ReflectionParameter && !$subject instanceof \ReflectionFunctionAbstract) {
52+
throw new UnsupportedException(sprintf('Expected subject to be a "ReflectionProperty", a "ReflectionParameter" or a "ReflectionFunctionAbstract", "%s" given.', get_debug_type($subject)), $subject);
53+
}
54+
55+
$docComment = match (true) {
56+
$subject instanceof \ReflectionProperty => $subject->getDocComment(),
57+
$subject instanceof \ReflectionParameter => $subject->getDeclaringFunction()->getDocComment(),
58+
$subject instanceof \ReflectionFunctionAbstract => $subject->getDocComment(),
59+
};
60+
61+
if (!$docComment) {
62+
return $this->reflectionTypeResolver->resolve($subject);
63+
}
64+
65+
$typeContext ??= $this->typeContextFactory->createFromReflection($subject);
66+
67+
$tagName = match (true) {
68+
$subject instanceof \ReflectionProperty => '@var',
69+
$subject instanceof \ReflectionParameter => '@param',
70+
$subject instanceof \ReflectionFunctionAbstract => '@return',
71+
};
72+
73+
$tokens = new TokenIterator($this->lexer->tokenize($docComment));
74+
$docNode = $this->phpDocParser->parse($tokens);
75+
76+
foreach ($docNode->getTagsByName($tagName) as $tag) {
77+
$tagValue = $tag->value;
78+
79+
if (
80+
$tagValue instanceof VarTagValueNode
81+
|| $tagValue instanceof ParamTagValueNode && $tagName && '$'.$subject->getName() === $tagValue->parameterName
82+
|| $tagValue instanceof ReturnTagValueNode
83+
) {
84+
return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext);
85+
}
86+
}
87+
88+
return $this->reflectionTypeResolver->resolve($subject);
89+
}
90+
}

src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,29 +61,35 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type
6161
return $resolver->resolve($subject, $typeContext);
6262
}
6363

64-
public static function create(): self
64+
/**
65+
* @param array<string, TypeResolverInterface>|null $resolvers
66+
*/
67+
public static function create(?array $resolvers = null): self
6568
{
66-
$resolvers = new class() implements ContainerInterface {
67-
private readonly array $resolvers;
69+
if (null === $resolvers) {
70+
$stringTypeResolver = class_exists(PhpDocParser::class) ? new StringTypeResolver() : null;
71+
$typeContextFactory = new TypeContextFactory($stringTypeResolver);
72+
$reflectionTypeResolver = new ReflectionTypeResolver();
73+
74+
$resolvers = [
75+
\ReflectionType::class => $reflectionTypeResolver,
76+
\ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory),
77+
\ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory),
78+
\ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory),
79+
];
80+
81+
if (null !== $stringTypeResolver) {
82+
$resolvers['string'] = $stringTypeResolver;
83+
$resolvers[\ReflectionParameter::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionParameter::class], $stringTypeResolver, $typeContextFactory);
84+
$resolvers[\ReflectionProperty::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionProperty::class], $stringTypeResolver, $typeContextFactory);
85+
$resolvers[\ReflectionFunctionAbstract::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionFunctionAbstract::class], $stringTypeResolver, $typeContextFactory);
86+
}
87+
}
6888

69-
public function __construct()
70-
{
71-
$stringTypeResolver = class_exists(PhpDocParser::class) ? new StringTypeResolver() : null;
72-
$typeContextFactory = new TypeContextFactory($stringTypeResolver);
73-
$reflectionTypeResolver = new ReflectionTypeResolver();
74-
75-
$resolvers = [
76-
\ReflectionType::class => $reflectionTypeResolver,
77-
\ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory),
78-
\ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory),
79-
\ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory),
80-
];
81-
82-
if (null !== $stringTypeResolver) {
83-
$resolvers['string'] = $stringTypeResolver;
84-
}
85-
86-
$this->resolvers = $resolvers;
89+
$resolversContainer = new class($resolvers) implements ContainerInterface {
90+
public function __construct(
91+
private readonly array $resolvers,
92+
) {
8793
}
8894

8995
public function has(string $id): bool
@@ -97,6 +103,6 @@ public function get(string $id): TypeResolverInterface
97103
}
98104
};
99105

100-
return new self($resolvers);
106+
return new self($resolversContainer);
101107
}
102108
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy