diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 87ce95e44bc13..cad267ccd8eae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1413,6 +1413,7 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container, $definition->setPrivate(true); $definition->addTag('property_info.description_extractor', ['priority' => -1000]); $definition->addTag('property_info.type_extractor', ['priority' => -1001]); + $definition->addTag('property_info.constructor_extractor', ['priority' => -1001]); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 999f4aed36f96..1306a9bb7ef54 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -44,6 +44,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Messenger\DependencyInjection\MessengerPass; +use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass; use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass; use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; @@ -107,6 +108,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new FragmentRendererPass()); $this->addCompilerPassIfExists($container, SerializerPass::class); $this->addCompilerPassIfExists($container, PropertyInfoPass::class); + $this->addCompilerPassIfExists($container, PropertyInfoConstructorPass::class); $container->addCompilerPass(new DataCollectorTranslatorPass()); $container->addCompilerPass(new ControllerArgumentValueResolverPass()); $container->addCompilerPass(new CachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 32); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml index e8778f70822bf..925cc62f3bea1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml @@ -25,8 +25,14 @@ + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php index 61669e90adbc7..b654008dacb2b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php @@ -21,6 +21,27 @@ public function testPhpDocPriority() $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT))], static::$container->get('property_info')->getTypes('Symfony\Bundle\FrameworkBundle\Tests\Functional\Dummy', 'codes')); } + + /** + * @dataProvider constructorOverridesPropertyTypeProvider + */ + public function testConstructorOverridesPropertyType($property, array $type = null) + { + static::bootKernel(['test_case' => 'Serializer']); + $extractor = static::$container->get('property_info'); + $this->assertEquals($type, $extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); + } + + public function constructorOverridesPropertyTypeProvider() + { + return [ + ['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], + ['date', [new Type(Type::BUILTIN_TYPE_INT)]], + ['dateObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]], + ['dateTime', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')]], + ['ddd', null], + ]; + } } class Dummy diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index e18554972e423..cf129bc336e0d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -54,7 +54,7 @@ "symfony/var-dumper": "~3.4|~4.0", "symfony/workflow": "^4.1", "symfony/yaml": "~3.4|~4.0", - "symfony/property-info": "~3.4|~4.0", + "symfony/property-info": "~3.4.23|^4.2.4", "symfony/lock": "~3.4|~4.0", "symfony/web-link": "~3.4|~4.0", "doctrine/annotations": "~1.0", @@ -70,7 +70,7 @@ "symfony/dotenv": "<4.2", "symfony/form": "<4.2", "symfony/messenger": "<4.2", - "symfony/property-info": "<3.4", + "symfony/property-info": "<3.4.23|>=4.0.0,<4.2.4", "symfony/serializer": "<4.2", "symfony/stopwatch": "<3.4", "symfony/translation": "<4.2", diff --git a/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoConstructorPass.php b/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoConstructorPass.php new file mode 100644 index 0000000000000..ccf84e82faa3c --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoConstructorPass.php @@ -0,0 +1,50 @@ + + * + * 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 + */ +class PropertyInfoConstructorPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + private $service; + private $tag; + + public function __construct($service = 'property_info.constructor_extractor', $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)); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php new file mode 100644 index 0000000000000..3192b79fb0646 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php @@ -0,0 +1,32 @@ + + * + * 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 + */ +interface ConstructorArgumentTypeExtractorInterface +{ + /** + * Gets types of an argument from constructor. + * + * @param string $class + * @param string $property + * + * @return Type[]|null + */ + public function getTypesFromConstructor($class, $property); +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php new file mode 100644 index 0000000000000..9695523bc050a --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php @@ -0,0 +1,48 @@ + + * + * 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 + */ +class ConstructorExtractor implements PropertyTypeExtractorInterface +{ + /** @var iterable|ConstructorArgumentTypeExtractorInterface[] */ + private $extractors; + + /** + * @param iterable|ConstructorArgumentTypeExtractorInterface[] $extractors + */ + public function __construct($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; + } +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index 4837d2200c852..1e0a401c4034b 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -27,7 +27,7 @@ * * @final */ -class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface +class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface { const PROPERTY = 0; const ACCESSOR = 1; @@ -151,6 +151,77 @@ public function getTypes($class, $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($class, $property) + { + $docBlock = $this->getDocBlockFromConstructor($class, $property); + + if (!$docBlock) { + return; + } + + $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; + } + + return $types; + } + + /** + * Gets the DocBlock from a constructor. + * + * @param string $class + * @param string $property + * + * @return DocBlock|null + */ + private function getDocBlockFromConstructor($class, $property) + { + 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; + } + } + + /** + * @param DocBlock $docBlock + * @param string $allowedParam + * + * @return DocBlock + */ + private function filterDocBlockParams(DocBlock $docBlock, $allowedParam) + { + $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); diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 0ed1e4e2afe3d..1543674184b5a 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -25,7 +25,7 @@ * * @final */ -class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface +class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, ConstructorArgumentTypeExtractorInterface { /** * @internal @@ -119,6 +119,50 @@ public function getTypes($class, $property, array $context = []) } } + /** + * {@inheritdoc} + */ + public function getTypesFromConstructor($class, $property) + { + 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]; + } + + /** + * @param string $property + * @param \ReflectionMethod $reflectionConstructor + * + * @return \ReflectionParameter|null + */ + private function getReflectionParameterFromConstructor($property, \ReflectionMethod $reflectionConstructor) + { + $reflectionParameter = null; + foreach ($reflectionConstructor->getParameters() as $reflectionParameter) { + if ($reflectionParameter->getName() === $property) { + return $reflectionParameter; + } + } + + return null; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoConstructorPassTest.php b/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoConstructorPassTest.php new file mode 100644 index 0000000000000..ee3151f2710a9 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoConstructorPassTest.php @@ -0,0 +1,54 @@ + + * + * 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)); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php new file mode 100644 index 0000000000000..11a28c0f7c824 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php @@ -0,0 +1,40 @@ + + */ +class ConstructorExtractorTest extends TestCase +{ + /** + * @var ConstructorExtractor + */ + private $extractor; + + protected function setUp() + { + $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', [])); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 930dc6e24c9b8..33280e79032dd 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -247,6 +247,25 @@ public function testDocBlockFallback($property, $types) { $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback', $property)); } + + /** + * @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 diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index e58c70d41d702..cb5e02ceb0d5b 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -293,4 +293,23 @@ public function getInitializableProperties(): array [NotInstantiable::class, 'foo', false], ]; } + + /** + * @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 [ + ['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], + ['date', null], + ['dateObject', null], + ['dateTime', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')]], + ['ddd', null], + ]; + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummy.php new file mode 100644 index 0000000000000..23ef5cceaef75 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummy.php @@ -0,0 +1,30 @@ + + */ +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(); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php index 9003b2fa38a6d..d75f1bc0d388f 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php @@ -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 @@ /** * @author Kévin Dunglas */ -class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface +class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, ConstructorArgumentTypeExtractorInterface { /** * {@inheritdoc} @@ -47,6 +48,14 @@ public function getTypes($class, $property, array $context = []) return [new Type(Type::BUILTIN_TYPE_INT)]; } + /** + * {@inheritdoc} + */ + public function getTypesFromConstructor($class, $property) + { + return [new Type(Type::BUILTIN_TYPE_STRING)]; + } + /** * {@inheritdoc} */