Skip to content

Commit

Permalink
feature #26997 [PropertyInfo] Add an extractor to guess if a property…
Browse files Browse the repository at this point in the history
… is initializable (dunglas)

This PR was squashed before being merged into the 4.2-dev branch (closes #26997).

Discussion
----------

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

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | n/a   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | todo

When dealing with value objects, being able to detect if a property can be initialized using the constructor is a very valuable information. It's mandatory to add a proper value object support in API Platform and in the Serializer component.

See api-platform/core#1749 and api-platform/core#1843 for the related discussions, extended use cases and proof of concepts.

This PR adds a new interface to guess if a property can be initialized through the constructor, and an implementation using the reflection (in `ReflectionExtractor`).

Commits
-------

9d2ab9e [PropertyInfo] Add an extractor to guess if a property is initializable
  • Loading branch information
fabpot committed Sep 4, 2018
2 parents bc419fa + 9d2ab9e commit 4401f2f
Show file tree
Hide file tree
Showing 16 changed files with 200 additions and 14 deletions.
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
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
@@ -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
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));
}
}
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) {
return true;
}
}
} elseif ($parentClass = $reflectionClass->getParentClass()) {
return $this->isInitializable($parentClass->getName(), $property);
}

return false;
}

/**
* @return Type[]|null
*/
Expand Down
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
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
@@ -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;
}
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()));
}
}
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));
}
}
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),
);
}
}
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;
}
}
@@ -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)
{
}
}

0 comments on commit 4401f2f

Please sign in to comment.