Thanks to visit codestin.com
Credit goes to github.com

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

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

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

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

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+
}
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 <[email protected]>
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

+8-9
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(
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 <[email protected]>
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 <[email protected]>
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

+28-22
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)