8000 feature #57618 [TypeInfo] Add `PhpDocAwareReflectionTypeResolver` (mt… · symfony/symfony@0234e05 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0234e05

Browse files
committed
feature #57618 [TypeInfo] Add PhpDocAwareReflectionTypeResolver (mtarld)
This PR was merged into the 7.2 branch. Discussion ---------- [TypeInfo] Add `PhpDocAwareReflectionTypeResolver` | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | | License | MIT Add a `PhpDocAwareReflectionTypeResolver` that resolves type on reflections prioritizing PHP documentation. The same feature already exists in the `PropertyInfo` component and improves DX a lot. Installing `phpstan/phpdoc-parser` automatically enables this feature both using the type-info component standalone and the fullstack framework. Commits ------- c6698ce [TypeInfo] Add PhpDocAwareReflectionTypeResolver
2 parents 0d879fc + c6698ce commit 0234e05

File tree

9 files changed

+223
-41
lines changed

9 files changed

+223
-41
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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
public function __construct(
38+
private TypeResolverInterface $reflectionTypeResolver,
39+
private TypeResolverInterface $stringTypeResolver,
40+
private TypeContextFactory $typeContextFactory,
41+
private PhpDocParser $phpDocParser = new PhpDocParser(new TypeParser(), new ConstExprParser()),
42+
private Lexer $lexer = new Lexer(),
43+
) {
44+
}
45+
46+
public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type
47+
{
48+
if (!$subject instanceof \ReflectionProperty && !$subject instanceof \ReflectionParameter && !$subject instanceof \ReflectionFunctionAbstract) {
49+
throw new UnsupportedException(sprintf('Expected subject to be a "ReflectionProperty", a "ReflectionParameter" or a "ReflectionFunctionAbstract", "%s" given.', get_debug_type($subject)), $subject);
50+
}
51+
52+
$docComment = match (true) {
53+
$subject instanceof \ReflectionProperty => $subject->getDocComment(),
54+
$subject instanceof \ReflectionParameter => $subject->getDeclaringFunction()->getDocComment(),
55+
$subject instanceof \ReflectionFunctionAbstract => $subject->getDocComment(),
56+
};
57+
58+
if (!$docComment) {
59+
return $this->reflectionTypeResolver->resolve($subject);
60+
}
61+
62+
$typeContext ??= $this->typeContextFactory->createFromReflection($subject);
63+
64+
$tagName = match (true) {
65+
$subject instanceof \ReflectionProperty => '@var',
66+
$subject instanceof \ReflectionParameter => '@param',
67+
$subject instanceof \ReflectionFunctionAbstract => '@return',
68+
};
69+
70+
$tokens = new TokenIterator($this->lexer->tokenize($docComment));
71+
$docNode = $this->phpDocParser->parse($tokens);
72+
73+
foreach ($docNode->getTagsByName($tagName) as $tag) {
74+
$tagValue = $tag->value;
75+
76+
if (
77+
$tagValue instanceof VarTagValueNode
78+
|| $tagValue instanceof ParamTagValueNode && $tagName && '$'.$subject->getName() === $tagValue->parameterName
79+
|| $tagValue instanceof ReturnTagValueNode
80+
) {
81+
return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext);
82+
}
83+
}
84+
85+
return $this->reflectionTypeResolver->resolve($subject);
86+
}
87+
}

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,10 @@ final class StringTypeResolver implements TypeResolverInterface
5858
*/
5959
private static array $classExistCache = [];
6060

61-
private readonly Lexer $lexer;
62-
private readonly TypeParser $parser;
63-
64-
< C2EE span class=pl-k>public function __construct()
65-
{
66-
$this->lexer = new Lexer();
67-
$this->parser = new TypeParser(new ConstExprParser());
61+
public function __construct(
62+
private Lexer $lexer = new Lexer(),
63+
private TypeParser $parser = new TypeParser(new ConstExprParser()),
64+
) {
6865
}
6966

7067
public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type

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)
0