Added ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor

This commit is contained in:
karser 2019-02-21 18:26:31 +02:00 committed by Fabien Potencier
parent 12330e8ee6
commit 5049e25b01
11 changed files with 409 additions and 3 deletions

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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}
*/

View File

@ -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));
}
}

View File

@ -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', []));
}
}

View File

@ -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

View File

@ -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],
];
}
}

View File

@ -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();
}
}

View File

@ -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}
*/