Skip to content

Commit 95e2d3f

Browse files
committed
Added ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor
1 parent 5a3e894 commit 95e2d3f

11 files changed

+409
-3
lines changed
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 <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\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 <dpoddubny@gmail.com>
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+
}
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 <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\PropertyInfo\Extractor;
13+
14+
use Symfony\Component\PropertyInfo\Type;
15+
16+
/**
17+
* Infers the constructor argument type.
18+
*
19+
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
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+
}
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 <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\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 <dpoddubny@gmail.com>
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

+58-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
*
2828
* @final
2929
*/
30-
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface
30+
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
3131
{
3232
const PROPERTY = 0;
3333
const ACCESSOR = 1;
@@ -151,6 +151,63 @@ public function getTypes($class, $property, array $context = [])
151151
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
152152
}
153153

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

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

+39-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*
2626
* @final
2727
*/
28-
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
28+
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, ConstructorArgumentTypeExtractorInterface
2929
{
3030
/**
3131
* @internal
@@ -129,6 +129,44 @@ public function getTypes($class, $property, array $context = [])
129129
}
130130
}
131131

132+
/**
133+
* {@inheritdoc}
134+
*/
135+
public function getTypesFromConstructor(string $class, string $property): ?array
136+
{
137+
try {
138+
$reflection = new \ReflectionClass($class);
139+
} catch (\ReflectionException $e) {
140+
return null;
141+
}
142+
if (!$reflectionConstructor = $reflection->getConstructor()) {
143+
return null;
144+
}
145+
if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) {
146+
return null;
147+
}
148+
if (!$reflectionType = $reflectionParameter->getType()) {
149+
return null;
150+
}
151+
if (!$type = $this->extractFromReflectionType($reflectionType, $reflectionConstructor)) {
152+
return null;
153+
}
154+
155+
return [$type];
156+
}
157+
158+
private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter
159+
{
160+
$reflectionParameter = null;
161+
foreach ($reflectionConstructor->getParameters() as $reflectionParameter) {
162+
if ($reflectionParameter->getName() === $property) {
163+
return $reflectionParameter;
164+
}
165+
}
166+
167+
return null;
168+
}
169+
132170
/**
133171
* {@inheritdoc}
134172
*/
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 <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\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+
}
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 <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\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 <dpoddubny@gmail.com>
21+
*/
22+
class ConstructorExtractorTest extends TestCase
23+
{
24+
/**
25+
* @var ConstructorExtractor
26+
*/
27+
private $extractor;
28+
29+
protected function setUp()
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

+19
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,25 @@ public function testDocBlockFallback($property, $types)
247247
{
248248
$this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback', $property));
249249
}
250+
251+
/**
252+
* @dataProvider constructorTypesProvider
253+
*/
254+
public function testExtractConstructorTypes($property, array $type = null)
255+
{
256+
$this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property));
257+
}
258+
259+
public function constructorTypesProvider()
260+
{
261+
return [
262+
['date', [new Type(Type::BUILTIN_TYPE_INT)]],
263+
['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]],
264+
['dateObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]],
265+
['dateTime', null],
266+
['ddd', null],
267+
];
268+
}
250269
}
251270

252271
class EmptyDocBlock

0 commit comments

Comments
 (0)