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

Skip to content

Commit 4a89215

Browse files
committed
feature #30335 [PropertyInfo] ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor (karser)
This PR was merged into the 5.2-dev branch. Discussion ---------- [PropertyInfo] ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor | Q | A | ------------- | --- | Branch? | master | Bug fix? | yes | New feature? | yes | BC breaks? | hopefully no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #30053 | License | MIT Supersedes #30056 #30128 In short, when using PhpDocExtractor, it ignores the constructor argument type, although `argument types from the constructor are the only types that are valid for the class instantiation`. This PR adds a separate extractor - `ConstructorExtractor` which is called first (-999) and it attempts to extract the type from constructor only, either from PhpDoc or using reflection. I added `getTypesFromConstructor` to `PhpDocExtractor` and `ReflectionExtractor` - they implement `ConstructorArgumentTypeExtractorInterface` interface. `ConstructorExtractor` aggregates those extractors using compiler pass. So the flow of control looks like this: ``` PropertyInfoExtractor::getTypes: - ConstructorExtractor::getTypes - PhpDocExtractor::getTypesFromConstructor - ReflectionExtractor::getTypesFromConstructor - PhpDocExtractor::getTypes - ReflectionExtractor::getTypes ``` Commits ------- 5049e25 Added ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor
2 parents 12330e8 + 5049e25 commit 4a89215

11 files changed

+409
-3
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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\PropertyInfo\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
19+
/**
20+
* Adds extractors to the property_info.constructor_extractor service.
21+
*
22+
* @author Dmitrii Poddubnyi <[email protected]>
23+
*/
24+
final class PropertyInfoConstructorPass implements CompilerPassInterface
25+
{
26+
use PriorityTaggedServiceTrait;
27+
28+
private $service;
29+
private $tag;
30+
31+
public function __construct(string $service = 'property_info.constructor_extractor', string $tag = 'property_info.constructor_extractor')
32+
{
33+
$this->service = $service;
34+
$this->tag = $tag;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function process(ContainerBuilder $container)
41+
{
42+
if (!$container->hasDefinition($this->service)) {
43+
return;
44+
}
45+
$definition = $container->getDefinition($this->service);
46+
47+
$listExtractors = $this->findAndSortTaggedServices($this->tag, $container);
48+
$definition->replaceArgument(0, new IteratorArgument($listExtractors));
49+
}
50+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\PropertyInfo\Extractor;
13+
14+
use Symfony\Component\PropertyInfo\Type;
15+
16+
/**
17+
* Infers the constructor argument type.
18+
*
19+
* @author Dmitrii Poddubnyi <[email protected]>
20+
*
21+
* @internal
22+
*/
23+
interface ConstructorArgumentTypeExtractorInterface
24+
{
25+
/**
26+
* Gets types of an argument from constructor.
27+
*
28+
* @return Type[]|null
29+
*
30+
* @internal
31+
*/
32+
public function getTypesFromConstructor(string $class, string $property): ?array;
33+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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\PropertyInfo\Extractor;
13+
14+
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
15+
16+
/**
17+
* Extracts the constructor argument type using ConstructorArgumentTypeExtractorInterface implementations.
18+
*
19+
* @author Dmitrii Poddubnyi <[email protected]>
20+
*/
21+
final class ConstructorExtractor implements PropertyTypeExtractorInterface
22+
{
23+
/** @var iterable|ConstructorArgumentTypeExtractorInterface[] */
24+
private $extractors;
25+
26+
/**
27+
* @param iterable|ConstructorArgumentTypeExtractorInterface[] $extractors
28+
*/
29+
public function __construct(iterable $extractors = [])
30+
{
31+
$this->extractors = $extractors;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function getTypes($class, $property, array $context = [])
38+
{
39+
foreach ($this->extractors as $extractor) {
40+
$value = $extractor->getTypesFromConstructor($class, $property);
41+
if (null !== $value) {
42+
return $value;
43+
}
44+
}
45+
46+
return null;
47+
}
48+
}

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

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*
3030
* @final
3131
*/
32-
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface
32+
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
3333
{
3434
const PROPERTY = 0;
3535
const ACCESSOR = 1;
@@ -161,6 +161,63 @@ public function getTypes(string $class, string $property, array $context = []):
161161
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
162162
}
163163

164+
/**
165+
* {@inheritdoc}
166+
*/
167+
public function getTypesFromConstructor(string $class, string $property): ?array
168+
{
169+
$docBlock = $this->getDocBlockFromConstructor($class, $property);
170+
171+
if (!$docBlock) {
172+
return null;
173+
}
174+
175+
$types = [];
176+
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
177+
foreach ($docBlock->getTagsByName('param') as $tag) {
178+
if ($tag && null !== $tag->getType()) {
179+
$types = array_merge($types, $this->phpDocTypeHelper->getTypes($tag->getType()));
180+
}
181+
}
182+
183+
if (!isset($types[0])) {
184+
return null;
185+
}
186+
187+
return $types;
188+
}
189+
190+
private function getDocBlockFromConstructor(string $class, string $property): ?DocBlock
191+
{
192+
try {
193+
$reflectionClass = new \ReflectionClass($class);
194+
} catch (\ReflectionException $e) {
195+
return null;
196+
}
197+
$reflectionConstructor = $reflectionClass->getConstructor();
198+
if (!$reflectionConstructor) {
199+
return null;
200+
}
201+
202+
try {
203+
$docBlock = $this->docBlockFactory->create($reflectionConstructor, $this->contextFactory->createFromReflector($reflectionConstructor));
204+
205+
return $this->filterDocBlockParams($docBlock, $property);
206+
} catch (\InvalidArgumentException $e) {
207+
return null;
208+
}
209+
}
210+
211+
private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam): DocBlock
212+
{
213+
$tags = array_values(array_filter($docBlock->getTagsByName('param'), function ($tag) use ($allowedParam) {
214+
return $tag instanceof DocBlock\Tags\Param && $allowedParam === $tag->getVariableName();
215+
}));
216+
217+
return new DocBlock($docBlock->getSummary(), $docBlock->getDescription(), $tags, $docBlock->getContext(),
218+
$docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd());
219+
}
220+
164221
private function getDocBlock(string $class, string $property): array
165222
{
166223
$propertyHash = sprintf('%s::%s', $class, $property);

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
*
3131
* @final
3232
*/
33-
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface
33+
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface
3434
{
3535
/**
3636
* @internal
@@ -175,6 +175,44 @@ public function getTypes(string $class, string $property, array $context = []):
175175
return null;
176176
}
177177

178+
/**
179+
* {@inheritdoc}
180+
*/
181+
public function getTypesFromConstructor(string $class, string $property): ?array
182+
{
183+
try {
184+
$reflection = new \ReflectionClass($class);
185+
} catch (\ReflectionException $e) {
186+
return null;
187+
}
188+
if (!$reflectionConstructor = $reflection->getConstructor()) {
189+
return null;
190+
}
191+
if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) {
192+
return null;
193+
}
194+
if (!$reflectionType = $reflectionParameter->getType()) {
195+
return null;
196+
}
197+
if (!$type = $this->extractFromReflectionType($reflectionType, $reflectionConstructor)) {
198+
return null;
199+
}
200+
201+
return [$type];
202+
}
203+
204+
private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter
205+
{
206+
$reflectionParameter = null;
207+
foreach ($reflectionConstructor->getParameters() as $reflectionParameter) {
208+
if ($reflectionParameter->getName() === $property) {
209+
return $reflectionParameter;
210+
}
211+
}
212+
213+
return null;
214+
}
215+
178216
/**
179217
* {@inheritdoc}
180218
*/
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\PropertyInfo\Tests\DependencyInjection;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass;
19+
20+
class PropertyInfoConstructorPassTest extends TestCase
21+
{
22+
public function testServicesAreOrderedAccordingToPriority()
23+
{
24+
$container = new ContainerBuilder();
25+
26+
$tag = 'property_info.constructor_extractor';
27+
$definition = $container->register('property_info.constructor_extractor')->setArguments([null, null]);
28+
$container->register('n2')->addTag($tag, ['priority' => 100]);
29+
$container->register('n1')->addTag($tag, ['priority' => 200]);
30+
$container->register('n3')->addTag($tag);
31+
32+
$pass = new PropertyInfoConstructorPass();
33+
$pass->process($container);
34+
35+
$expected = new IteratorArgument([
36+
new Reference('n1'),
37+
new Reference('n2'),
38+
new Reference('n3'),
39+
]);
40+
$this->assertEquals($expected, $definition->getArgument(0));
41+
}
42+
43+
public function testReturningEmptyArrayWhenNoService()
44+
{
45+
$container = new ContainerBuilder();
46+
$propertyInfoExtractorDefinition = $container->register('property_info.constructor_extractor')
47+
->setArguments([[]]);
48+
49+
$pass = new PropertyInfoConstructorPass();
50+
$pass->process($container);
51+
52+
$this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(0));
53+
}
54+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\PropertyInfo\Tests\Extractor;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor;
16+
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor;
17+
use Symfony\Component\PropertyInfo\Type;
18+
19+
/**
20+
* @author Dmitrii Poddubnyi <[email protected]>
21+
*/
22+
class ConstructorExtractorTest extends TestCase
23+
{
24+
/**
25+
* @var ConstructorExtractor
26+
*/
27+
private $extractor;
28+
29+
protected function setUp(): void
30+
{
31+
$this->extractor = new ConstructorExtractor([new DummyExtractor()]);
32+
}
33+
34+
public function testInstanceOf()
35+
{
36+
$this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface', $this->extractor);
37+
}
38+
39+
public function testGetTypes()
40+
{
41+
$this->assertEquals([new Type(Type::BUILTIN_TYPE_STRING)], $this->extractor->getTypes('Foo', 'bar', []));
42+
}
43+
44+
public function testGetTypes_ifNoExtractors()
45+
{
46+
$extractor = new ConstructorExtractor([]);
47+
$this->assertNull($extractor->getTypes('Foo', 'bar', []));
48+
}
49+
}

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,25 @@ protected function isPhpDocumentorV5()
282282
return (new \ReflectionMethod(StandardTagFactory::class, 'create'))
283283
->hasReturnType();
284284
}
285+
286+
/**
287+
* @dataProvider constructorTypesProvider
288+
*/
289+
public function testExtractConstructorTypes($property, array $type = null)
290+
{
291+
$this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property));
292+
}
293+
294+
public function constructorTypesProvider()
295+
{
296+
return [
297+
['date', [new Type(Type::BUILTIN_TYPE_INT)]],
298+
['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]],
299+
['dateObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]],
300+
['dateTime', null],
301+
['ddd', null],
302+
];
303+
}
285304
}
286305

287306
class EmptyDocBlock

0 commit comments

Comments
 (0)