[PropertyInfo] Add an extractor to guess if a property is initializable

This commit is contained in:
Kévin Dunglas 2018-04-21 12:04:48 +02:00 committed by Fabien Potencier
parent 9ad492f312
commit 9d2ab9e348
16 changed files with 200 additions and 14 deletions

View File

@ -76,6 +76,7 @@ use Symfony\Component\Messenger\Transport\TransportInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
@ -347,6 +348,8 @@ class FrameworkExtension extends Extension
->addTag('property_info.description_extractor');
$container->registerForAutoconfiguration(PropertyAccessExtractorInterface::class)
->addTag('property_info.access_extractor');
$container->registerForAutoconfiguration(PropertyInitializableExtractorInterface::class)
->addTag('property_info.initializable_extractor');
$container->registerForAutoconfiguration(EncoderInterface::class)
->addTag('serializer.encoder');
$container->registerForAutoconfiguration(DecoderInterface::class)

View File

@ -12,18 +12,21 @@
<argument type="collection" />
<argument type="collection" />
<argument type="collection" />
<argument type="collection" />
</service>
<service id="Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface" alias="property_info" />
<service id="Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface" alias="property_info" />
<service id="Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface" alias="property_info" />
<service id="Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface" alias="property_info" />
<service id="Symfony\Component\PropertyInfo\PropertyListExtractorInterface" alias="property_info" />
<service id="Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface" alias="property_info" />
<!-- Extractor -->
<service id="property_info.reflection_extractor" class="Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor">
<tag name="property_info.list_extractor" priority="-1000" />
<tag name="property_info.type_extractor" priority="-1002" />
<tag name="property_info.access_extractor" priority="-1000" />
<tag name="property_info.initializable_extractor" priority="-1000" />
</service>
</services>
</container>

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
4.2.0
-----
* added `PropertyInitializableExtractorInterface` to test if a property can be initialized through the constructor (implemented by `ReflectionExtractor`)
3.3.0
-----

View File

@ -30,14 +30,16 @@ class PropertyInfoPass implements CompilerPassInterface
private $typeExtractorTag;
private $descriptionExtractorTag;
private $accessExtractorTag;
private $initializableExtractorTag;
public function __construct(string $propertyInfoService = 'property_info', string $listExtractorTag = 'property_info.list_extractor', string $typeExtractorTag = 'property_info.type_extractor', string $descriptionExtractorTag = 'property_info.description_extractor', string $accessExtractorTag = 'property_info.access_extractor')
public function __construct(string $propertyInfoService = 'property_info', string $listExtractorTag = 'property_info.list_extractor', string $typeExtractorTag = 'property_info.type_extractor', string $descriptionExtractorTag = 'property_info.description_extractor', string $accessExtractorTag = 'property_info.access_extractor', string $initializableExtractorTag = 'property_info.initializable_extractor')
{
$this->propertyInfoService = $propertyInfoService;
$this->listExtractorTag = $listExtractorTag;
$this->typeExtractorTag = $typeExtractorTag;
$this->descriptionExtractorTag = $descriptionExtractorTag;
$this->accessExtractorTag = $accessExtractorTag;
$this->initializableExtractorTag = $initializableExtractorTag;
}
/**
@ -62,5 +64,8 @@ class PropertyInfoPass implements CompilerPassInterface
$accessExtractors = $this->findAndSortTaggedServices($this->accessExtractorTag, $container);
$definition->replaceArgument(3, new IteratorArgument($accessExtractors));
$initializableExtractors = $this->findAndSortTaggedServices($this->initializableExtractorTag, $container);
$definition->replaceArgument(4, new IteratorArgument($initializableExtractors));
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\PropertyInfo\Extractor;
use Symfony\Component\Inflector\Inflector;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
@ -24,7 +25,7 @@ use Symfony\Component\PropertyInfo\Type;
*
* @final
*/
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
{
/**
* @internal
@ -146,6 +147,34 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
return null !== $reflectionMethod;
}
/**
* {@inheritdoc}
*/
public function isInitializable(string $class, string $property, array $context = array()): ?bool
{
try {
$reflectionClass = new \ReflectionClass($class);
} catch (\ReflectionException $e) {
return null;
}
if (!$reflectionClass->isInstantiable()) {
return false;
}
if ($constructor = $reflectionClass->getConstructor()) {
foreach ($constructor->getParameters() as $parameter) {
if ($property === $parameter->name) {
return true;
}
}
} elseif ($parentClass = $reflectionClass->getParentClass()) {
return $this->isInitializable($parentClass->getName(), $property);
}
return false;
}
/**
* @return Type[]|null
*/

View File

@ -20,7 +20,7 @@ use Psr\Cache\CacheItemPoolInterface;
*
* @final
*/
class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface
class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface
{
private $propertyInfoExtractor;
private $cacheItemPool;
@ -80,6 +80,14 @@ class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface
return $this->extract('getTypes', array($class, $property, $context));
}
/**
* {@inheritdoc}
*/
public function isInitializable(string $class, string $property, array $context = array()): ?bool
{
return $this->extract('isInitializable', array($class, $property, $context));
}
/**
* Retrieves the cached data if applicable or delegates to the decorated extractor.
*

View File

@ -18,25 +18,28 @@ namespace Symfony\Component\PropertyInfo;
*
* @final
*/
class PropertyInfoExtractor implements PropertyInfoExtractorInterface
class PropertyInfoExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface
{
private $listExtractors;
private $typeExtractors;
private $descriptionExtractors;
private $accessExtractors;
private $initializableExtractors;
/**
* @param iterable|PropertyListExtractorInterface[] $listExtractors
* @param iterable|PropertyTypeExtractorInterface[] $typeExtractors
* @param iterable|PropertyDescriptionExtractorInterface[] $descriptionExtractors
* @param iterable|PropertyAccessExtractorInterface[] $accessExtractors
* @param iterable|PropertyListExtractorInterface[] $listExtractors
* @param iterable|PropertyTypeExtractorInterface[] $typeExtractors
* @param iterable|PropertyDescriptionExtractorInterface[] $descriptionExtractors
* @param iterable|PropertyAccessExtractorInterface[] $accessExtractors
* @param iterable|PropertyInitializableExtractorInterface[] $initializableExtractors
*/
public function __construct(iterable $listExtractors = array(), iterable $typeExtractors = array(), iterable $descriptionExtractors = array(), iterable $accessExtractors = array())
public function __construct(iterable $listExtractors = array(), iterable $typeExtractors = array(), iterable $descriptionExtractors = array(), iterable $accessExtractors = array(), iterable $initializableExtractors = array())
{
$this->listExtractors = $listExtractors;
$this->typeExtractors = $typeExtractors;
$this->descriptionExtractors = $descriptionExtractors;
$this->accessExtractors = $accessExtractors;
$this->initializableExtractors = $initializableExtractors;
}
/**
@ -87,6 +90,14 @@ class PropertyInfoExtractor implements PropertyInfoExtractorInterface
return $this->extract($this->accessExtractors, 'isWritable', array($class, $property, $context));
}
/**
* {@inheritdoc}
*/
public function isInitializable(string $class, string $property, array $context = array()): ?bool
{
return $this->extract($this->initializableExtractors, 'isInitializable', array($class, $property, $context));
}
/**
* Iterates over registered extractors and return the first value found.
*

View File

@ -0,0 +1,25 @@
<?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;
/**
* Guesses if the property can be initialized through the constructor.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface PropertyInitializableExtractorInterface
{
/**
* Is the property initializable? Returns true if a constructor's parameter matches the given property name.
*/
public function isInitializable(string $class, string $property, array $context = array()): ?bool;
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\PropertyInfo\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor;
use Symfony\Component\PropertyInfo\Tests\Fixtures\NullExtractor;
use Symfony\Component\PropertyInfo\Type;
@ -30,7 +31,7 @@ class AbstractPropertyInfoExtractorTest extends TestCase
protected function setUp()
{
$extractors = array(new NullExtractor(), new DummyExtractor());
$this->propertyInfo = new PropertyInfoExtractor($extractors, $extractors, $extractors, $extractors);
$this->propertyInfo = new PropertyInfoExtractor($extractors, $extractors, $extractors, $extractors, $extractors);
}
public function testInstanceOf()
@ -39,6 +40,7 @@ class AbstractPropertyInfoExtractorTest extends TestCase
$this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface', $this->propertyInfo);
$this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface', $this->propertyInfo);
$this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface', $this->propertyInfo);
$this->assertInstanceOf(PropertyInitializableExtractorInterface::class, $this->propertyInfo);
}
public function testGetShortDescription()
@ -70,4 +72,9 @@ class AbstractPropertyInfoExtractorTest extends TestCase
{
$this->assertEquals(array('a', 'b'), $this->propertyInfo->getProperties('Foo'));
}
public function testIsInitializable()
{
$this->assertTrue($this->propertyInfo->isInitializable('Foo', 'bar', array()));
}
}

View File

@ -26,7 +26,7 @@ class PropertyInfoPassTest extends TestCase
{
$container = new ContainerBuilder();
$definition = $container->register('property_info')->setArguments(array(null, null, null, null));
$definition = $container->register('property_info')->setArguments(array(null, null, null, null, null));
$container->register('n2')->addTag($tag, array('priority' => 100));
$container->register('n1')->addTag($tag, array('priority' => 200));
$container->register('n3')->addTag($tag);
@ -49,6 +49,7 @@ class PropertyInfoPassTest extends TestCase
array(1, 'property_info.type_extractor'),
array(2, 'property_info.description_extractor'),
array(3, 'property_info.access_extractor'),
array(4, 'property_info.initializable_extractor'),
);
}
@ -56,7 +57,7 @@ class PropertyInfoPassTest extends TestCase
{
$container = new ContainerBuilder();
$propertyInfoExtractorDefinition = $container->register('property_info')
->setArguments(array(array(), array(), array(), array()));
->setArguments(array(array(), array(), array(), array(), array()));
$propertyInfoPass = new PropertyInfoPass();
$propertyInfoPass->process($container);
@ -65,5 +66,6 @@ class PropertyInfoPassTest extends TestCase
$this->assertEquals(new IteratorArgument(array()), $propertyInfoExtractorDefinition->getArgument(1));
$this->assertEquals(new IteratorArgument(array()), $propertyInfoExtractorDefinition->getArgument(2));
$this->assertEquals(new IteratorArgument(array()), $propertyInfoExtractorDefinition->getArgument(3));
$this->assertEquals(new IteratorArgument(array()), $propertyInfoExtractorDefinition->getArgument(4));
}
}

View File

@ -14,6 +14,9 @@ namespace Symfony\Component\PropertyInfo\Tests\Extractor;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\Tests\Fixtures\AdderRemoverDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended2;
use Symfony\Component\PropertyInfo\Type;
/**
@ -270,4 +273,24 @@ class ReflectionExtractorTest extends TestCase
$this->assertTrue($this->extractor->isWritable(AdderRemoverDummy::class, 'feet'));
$this->assertEquals(array('analyses', 'feet'), $this->extractor->getProperties(AdderRemoverDummy::class));
}
/**
* @dataProvider getInitializableProperties
*/
public function testIsInitializable(string $class, string $property, bool $expected)
{
$this->assertSame($expected, $this->extractor->isInitializable($class, $property));
}
public function getInitializableProperties(): array
{
return array(
array(Php71Dummy::class, 'string', true),
array(Php71Dummy::class, 'intPrivate', true),
array(Php71Dummy::class, 'notExist', false),
array(Php71DummyExtended2::class, 'intWithAccessor', true),
array(Php71DummyExtended2::class, 'intPrivate', false),
array(NotInstantiable::class, 'foo', false),
);
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
@ -20,7 +21,7 @@ use Symfony\Component\PropertyInfo\Type;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface
class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
{
/**
* {@inheritdoc}
@ -69,4 +70,12 @@ class DummyExtractor implements PropertyListExtractorInterface, PropertyDescript
{
return array('a', 'b');
}
/**
* {@inheritdoc}
*/
public function isInitializable(string $class, string $property, array $context = array()): ?bool
{
return true;
}
}

View File

@ -0,0 +1,22 @@
<?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\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class NotInstantiable
{
private function __construct(string $foo)
{
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
@ -21,7 +22,7 @@ use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class NullExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface
class NullExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
{
/**
* {@inheritdoc}
@ -76,6 +77,14 @@ class NullExtractor implements PropertyListExtractorInterface, PropertyDescripti
$this->assertIsString($class);
}
/**
* {@inheritdoc}
*/
public function isInitializable(string $class, string $property, array $context = array()): ?bool
{
return null;
}
private function assertIsString($string)
{
if (!\is_string($string)) {

View File

@ -16,6 +16,10 @@ namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
*/
class Php71Dummy
{
public function __construct(string $string, int $intPrivate)
{
}
public function getFoo(): ?array
{
}
@ -32,3 +36,18 @@ class Php71Dummy
{
}
}
class Php71DummyExtended extends Php71Dummy
{
}
class Php71DummyExtended2 extends Php71Dummy
{
public function __construct(int $intWithAccessor)
{
}
public function getIntWithAccessor()
{
}
}

View File

@ -61,4 +61,10 @@ class PropertyInfoCacheExtractorTest extends AbstractPropertyInfoExtractorTest
parent::testGetProperties();
parent::testGetProperties();
}
public function testIsInitializable()
{
parent::testIsInitializable();
parent::testIsInitializable();
}
}