Skip to content

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

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
Sep 4, 2018
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
Expand Up @@ -76,6 +76,7 @@
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;
Expand Down Expand Up @@ -347,6 +348,8 @@ public function load(array $configs, ContainerBuilder $container)
->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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
5 changes: 5 additions & 0 deletions src/Symfony/Component/PropertyInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
-----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -62,5 +64,8 @@ public function process(ContainerBuilder $container)

$accessExtractors = $this->findAndSortTaggedServices($this->accessExtractorTag, $container);
$definition->replaceArgument(3, new IteratorArgument($accessExtractors));

$initializableExtractors = $this->findAndSortTaggedServices($this->initializableExtractorTag, $container);
$definition->replaceArgument(4, new IteratorArgument($initializableExtractors));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

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;
Expand All @@ -24,7 +25,7 @@
*
* @final
*/
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface
class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
{
/**
* @internal
Expand Down Expand Up @@ -146,6 +147,34 @@ public function isWritable($class, $property, array $context = array())
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that the constructor parameter name must match the property one should be documented somewhere.

return true;
}
}
} elseif ($parentClass = $reflectionClass->getParentClass()) {
return $this->isInitializable($parentClass->getName(), $property);
}

return false;
}

/**
* @return Type[]|null
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*
* @final
*/
class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface
class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface
{
private $propertyInfoExtractor;
private $cacheItemPool;
Expand Down Expand Up @@ -80,6 +80,14 @@ public function getTypes($class, $property, array $context = array())
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.
*
Expand Down
23 changes: 17 additions & 6 deletions src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,28 @@
*
* @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;
}

/**
Expand Down Expand Up @@ -87,6 +90,14 @@ public function isWritable($class, $property, array $context = array())
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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

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;
Expand All @@ -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()
Expand All @@ -39,6 +40,7 @@ public function testInstanceOf()
$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()
Expand Down Expand Up @@ -70,4 +72,9 @@ public function testGetProperties()
{
$this->assertEquals(array('a', 'b'), $this->propertyInfo->getProperties('Foo'));
}

public function testIsInitializable()
{
$this->assertTrue($this->propertyInfo->isInitializable('Foo', 'bar', array()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function testServicesAreOrderedAccordingToPriority($index, $tag)
{
$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);
Expand All @@ -49,14 +49,15 @@ public function provideTags()
array(1, 'property_info.type_extractor'),
array(2, 'property_info.description_extractor'),
array(3, 'property_info.access_extractor'),
array(4, 'property_info.initializable_extractor'),
);
}

public function testReturningEmptyArrayWhenNoService()
{
$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);
Expand All @@ -65,5 +66,6 @@ public function testReturningEmptyArrayWhenNoService()
$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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
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;

/**
Expand Down Expand Up @@ -270,4 +273,24 @@ public function testSingularize()
$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),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@

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;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface
class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface
{
/**
* {@inheritdoc}
Expand Down Expand Up @@ -69,4 +70,12 @@ public function getProperties($class, array $context = array())
{
return array('a', 'b');
}

/**
* {@inheritdoc}
*/
public function isInitializable(string $class, string $property, array $context = array()): ?bool
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -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)
{
}
}
Loading