Added ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor
This commit is contained in:
parent
12330e8ee6
commit
5049e25b01
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\PropertyInfo\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* Adds extractors to the property_info.constructor_extractor service.
|
||||
*
|
||||
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
|
||||
*/
|
||||
final class PropertyInfoConstructorPass implements CompilerPassInterface
|
||||
{
|
||||
use PriorityTaggedServiceTrait;
|
||||
|
||||
private $service;
|
||||
private $tag;
|
||||
|
||||
public function __construct(string $service = 'property_info.constructor_extractor', string $tag = 'property_info.constructor_extractor')
|
||||
{
|
||||
$this->service = $service;
|
||||
$this->tag = $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition($this->service)) {
|
||||
return;
|
||||
}
|
||||
$definition = $container->getDefinition($this->service);
|
||||
|
||||
$listExtractors = $this->findAndSortTaggedServices($this->tag, $container);
|
||||
$definition->replaceArgument(0, new IteratorArgument($listExtractors));
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\PropertyInfo\Extractor;
|
||||
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* Infers the constructor argument type.
|
||||
*
|
||||
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface ConstructorArgumentTypeExtractorInterface
|
||||
{
|
||||
/**
|
||||
* Gets types of an argument from constructor.
|
||||
*
|
||||
* @return Type[]|null
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function getTypesFromConstructor(string $class, string $property): ?array;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\PropertyInfo\Extractor;
|
||||
|
||||
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
|
||||
|
||||
/**
|
||||
* Extracts the constructor argument type using ConstructorArgumentTypeExtractorInterface implementations.
|
||||
*
|
||||
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
|
||||
*/
|
||||
final class ConstructorExtractor implements PropertyTypeExtractorInterface
|
||||
{
|
||||
/** @var iterable|ConstructorArgumentTypeExtractorInterface[] */
|
||||
private $extractors;
|
||||
|
||||
/**
|
||||
* @param iterable|ConstructorArgumentTypeExtractorInterface[] $extractors
|
||||
*/
|
||||
public function __construct(iterable $extractors = [])
|
||||
{
|
||||
$this->extractors = $extractors;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTypes($class, $property, array $context = [])
|
||||
{
|
||||
foreach ($this->extractors as $extractor) {
|
||||
$value = $extractor->getTypesFromConstructor($class, $property);
|
||||
if (null !== $value) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper;
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface
|
||||
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
|
||||
{
|
||||
const PROPERTY = 0;
|
||||
const ACCESSOR = 1;
|
||||
@ -161,6 +161,63 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
|
||||
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTypesFromConstructor(string $class, string $property): ?array
|
||||
{
|
||||
$docBlock = $this->getDocBlockFromConstructor($class, $property);
|
||||
|
||||
if (!$docBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$types = [];
|
||||
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
|
||||
foreach ($docBlock->getTagsByName('param') as $tag) {
|
||||
if ($tag && null !== $tag->getType()) {
|
||||
$types = array_merge($types, $this->phpDocTypeHelper->getTypes($tag->getType()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($types[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
private function getDocBlockFromConstructor(string $class, string $property): ?DocBlock
|
||||
{
|
||||
try {
|
||||
$reflectionClass = new \ReflectionClass($class);
|
||||
} catch (\ReflectionException $e) {
|
||||
return null;
|
||||
}
|
||||
$reflectionConstructor = $reflectionClass->getConstructor();
|
||||
if (!$reflectionConstructor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$docBlock = $this->docBlockFactory->create($reflectionConstructor, $this->contextFactory->createFromReflector($reflectionConstructor));
|
||||
|
||||
return $this->filterDocBlockParams($docBlock, $property);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function filterDocBlockParams(DocBlock $docBlock, string $allowedParam): DocBlock
|
||||
{
|
||||
$tags = array_values(array_filter($docBlock->getTagsByName('param'), function ($tag) use ($allowedParam) {
|
||||
return $tag instanceof DocBlock\Tags\Param && $allowedParam === $tag->getVariableName();
|
||||
}));
|
||||
|
||||
return new DocBlock($docBlock->getSummary(), $docBlock->getDescription(), $tags, $docBlock->getContext(),
|
||||
$docBlock->getLocation(), $docBlock->isTemplateStart(), $docBlock->isTemplateEnd());
|
||||
}
|
||||
|
||||
private function getDocBlock(string $class, string $property): array
|
||||
{
|
||||
$propertyHash = sprintf('%s::%s', $class, $property);
|
||||
|
@ -30,7 +30,7 @@ use Symfony\Component\String\Inflector\InflectorInterface;
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface
|
||||
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
@ -175,6 +175,44 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTypesFromConstructor(string $class, string $property): ?array
|
||||
{
|
||||
try {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
} catch (\ReflectionException $e) {
|
||||
return null;
|
||||
}
|
||||
if (!$reflectionConstructor = $reflection->getConstructor()) {
|
||||
return null;
|
||||
}
|
||||
if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) {
|
||||
return null;
|
||||
}
|
||||
if (!$reflectionType = $reflectionParameter->getType()) {
|
||||
return null;
|
||||
}
|
||||
if (!$type = $this->extractFromReflectionType($reflectionType, $reflectionConstructor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$type];
|
||||
}
|
||||
|
||||
private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter
|
||||
{
|
||||
$reflectionParameter = null;
|
||||
foreach ($reflectionConstructor->getParameters() as $reflectionParameter) {
|
||||
if ($reflectionParameter->getName() === $property) {
|
||||
return $reflectionParameter;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\PropertyInfo\Tests\DependencyInjection;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass;
|
||||
|
||||
class PropertyInfoConstructorPassTest extends TestCase
|
||||
{
|
||||
public function testServicesAreOrderedAccordingToPriority()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$tag = 'property_info.constructor_extractor';
|
||||
$definition = $container->register('property_info.constructor_extractor')->setArguments([null, null]);
|
||||
$container->register('n2')->addTag($tag, ['priority' => 100]);
|
||||
$container->register('n1')->addTag($tag, ['priority' => 200]);
|
||||
$container->register('n3')->addTag($tag);
|
||||
|
||||
$pass = new PropertyInfoConstructorPass();
|
||||
$pass->process($container);
|
||||
|
||||
$expected = new IteratorArgument([
|
||||
new Reference('n1'),
|
||||
new Reference('n2'),
|
||||
new Reference('n3'),
|
||||
]);
|
||||
$this->assertEquals($expected, $definition->getArgument(0));
|
||||
}
|
||||
|
||||
public function testReturningEmptyArrayWhenNoService()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$propertyInfoExtractorDefinition = $container->register('property_info.constructor_extractor')
|
||||
->setArguments([[]]);
|
||||
|
||||
$pass = new PropertyInfoConstructorPass();
|
||||
$pass->process($container);
|
||||
|
||||
$this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(0));
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\PropertyInfo\Tests\Extractor;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor;
|
||||
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
|
||||
*/
|
||||
class ConstructorExtractorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ConstructorExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->extractor = new ConstructorExtractor([new DummyExtractor()]);
|
||||
}
|
||||
|
||||
public function testInstanceOf()
|
||||
{
|
||||
$this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface', $this->extractor);
|
||||
}
|
||||
|
||||
public function testGetTypes()
|
||||
{
|
||||
$this->assertEquals([new Type(Type::BUILTIN_TYPE_STRING)], $this->extractor->getTypes('Foo', 'bar', []));
|
||||
}
|
||||
|
||||
public function testGetTypes_ifNoExtractors()
|
||||
{
|
||||
$extractor = new ConstructorExtractor([]);
|
||||
$this->assertNull($extractor->getTypes('Foo', 'bar', []));
|
||||
}
|
||||
}
|
@ -282,6 +282,25 @@ class PhpDocExtractorTest extends TestCase
|
||||
return (new \ReflectionMethod(StandardTagFactory::class, 'create'))
|
||||
->hasReturnType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider constructorTypesProvider
|
||||
*/
|
||||
public function testExtractConstructorTypes($property, array $type = null)
|
||||
{
|
||||
$this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property));
|
||||
}
|
||||
|
||||
public function constructorTypesProvider()
|
||||
{
|
||||
return [
|
||||
['date', [new Type(Type::BUILTIN_TYPE_INT)]],
|
||||
['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]],
|
||||
['dateObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]],
|
||||
['dateTime', null],
|
||||
['ddd', null],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyDocBlock
|
||||
|
@ -547,4 +547,23 @@ class ReflectionExtractorTest extends TestCase
|
||||
'enable_magic_call_extraction' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider extractConstructorTypesProvider
|
||||
*/
|
||||
public function testExtractConstructorTypes(string $property, array $type = null)
|
||||
{
|
||||
$this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property));
|
||||
}
|
||||
|
||||
public function extractConstructorTypesProvider(): array
|
||||
{
|
||||
return [
|
||||
['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]],
|
||||
['date', null],
|
||||
['dateObject', null],
|
||||
['dateTime', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')]],
|
||||
['ddd', null],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
|
||||
|
||||
/**
|
||||
* @author Dmitrii Poddubnyi <dpoddubny@gmail.com>
|
||||
*/
|
||||
class ConstructorDummy
|
||||
{
|
||||
/** @var string */
|
||||
private $timezone;
|
||||
|
||||
/** @var \DateTimeInterface */
|
||||
private $date;
|
||||
|
||||
/** @var int */
|
||||
private $dateTime;
|
||||
|
||||
/**
|
||||
* @param \DateTimeZone $timezone
|
||||
* @param int $date Timestamp
|
||||
* @param \DateTimeInterface $dateObject
|
||||
*/
|
||||
public function __construct(\DateTimeZone $timezone, $date, $dateObject, \DateTime $dateTime)
|
||||
{
|
||||
$this->timezone = $timezone->getName();
|
||||
$this->date = \DateTime::createFromFormat('U', $date);
|
||||
$this->dateTime = $dateTime->getTimestamp();
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
|
||||
|
||||
use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface;
|
||||
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
|
||||
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
|
||||
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
|
||||
@ -21,7 +22,7 @@ use Symfony\Component\PropertyInfo\Type;
|
||||
/**
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
|
||||
class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, ConstructorArgumentTypeExtractorInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
@ -47,6 +48,14 @@ class DummyExtractor implements PropertyListExtractorInterface, PropertyDescript
|
||||
return [new Type(Type::BUILTIN_TYPE_INT)];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTypesFromConstructor(string $class, string $property): ?array
|
||||
{
|
||||
return [new Type(Type::BUILTIN_TYPE_STRING)];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
Reference in New Issue
Block a user