8000 [TypeInfo] Add PhpDocAwareReflectionTypeResolver · symfony/symfony@00d8043 · GitHub
[go: up one dir, main page]

Skip to content

Commit 00d8043

Browse files
committed
[TypeInfo] Add PhpDocAwareReflectionTypeResolver
1 parent 0f4cf9b commit 00d8043

File tree

6 files changed

+176
-2
lines changed

6 files changed

+176
-2
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/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+
}
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 priorizing 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ public function __construct()
8181

8282
if (null !== $stringTypeResolver) {
8383
$resolvers['string'] = $stringTypeResolver;
84+
$resolvers[\ReflectionParameter::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionParameter::class], $stringTypeResolver, $typeContextFactory);
85+
$resolvers[\ReflectionProperty::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionProperty::class], $stringTypeResolver, $typeContextFactory);
86+
$resolvers[\ReflectionFunctionAbstract::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionFunctionAbstract::class], $stringTypeResolver, $typeContextFactory);
8487
}
8588

8689
$this->resolvers = $resolvers;

0 commit comments

Comments
 (0)
0