Skip to content

[PropertyInfo] ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor #30335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
59 changes: 58 additions & 1 deletion src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*
* @final
*/
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface
class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface
{
const PROPERTY = 0;
const ACCESSOR = 1;
Expand Down Expand Up @@ -161,6 +161,63 @@ public function getTypes(string $class, string $property, array $context = []):
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
*
* @final
*/
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface
{
/**
* @internal
Expand Down Expand Up @@ -175,6 +175,44 @@ public function getTypes(string $class, string $property, array $context = []):
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}
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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', []));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,25 @@ protected function isPhpDocumentorV5()
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
Expand Down
Loading