From 5049e25b0142030604633f05aba150a7014d2385 Mon Sep 17 00:00:00 2001 From: karser Date: Thu, 21 Feb 2019 18:26:31 +0200 Subject: [PATCH] Added ConstructorExtractor which has higher priority than PhpDocExtractor and ReflectionExtractor --- .../PropertyInfoConstructorPass.php | 50 ++++++++++++++++ ...structorArgumentTypeExtractorInterface.php | 33 +++++++++++ .../Extractor/ConstructorExtractor.php | 48 +++++++++++++++ .../Extractor/PhpDocExtractor.php | 59 ++++++++++++++++++- .../Extractor/ReflectionExtractor.php | 40 ++++++++++++- .../PropertyInfoConstructorPassTest.php | 54 +++++++++++++++++ .../Extractor/ConstructorExtractorTest.php | 49 +++++++++++++++ .../Tests/Extractor/PhpDocExtractorTest.php | 19 ++++++ .../Extractor/ReflectionExtractorTest.php | 19 ++++++ .../Tests/Fixtures/ConstructorDummy.php | 30 ++++++++++ .../Tests/Fixtures/DummyExtractor.php | 11 +++- 11 files changed, 409 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoConstructorPass.php create mode 100644 src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php create mode 100644 src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php create mode 100644 src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoConstructorPassTest.php create mode 100644 src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php create mode 100644 src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummy.php diff --git a/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoConstructorPass.php b/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoConstructorPass.php new file mode 100644 index 0000000000000..2fb4f94d768e7 --- /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 + */ +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)); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php new file mode 100644 index 0000000000000..cbde902e98015 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php @@ -0,0 +1,33 @@ + + * + * 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 + * + * @internal + */ +interface ConstructorArgumentTypeExtractorInterface +{ + /** + * Gets types of an argument from constructor. + * + * @return Type[]|null + * + * @internal + */ + public function getTypesFromConstructor(string $class, string $property): ?array; +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php new file mode 100644 index 0000000000000..702251cde3ab5 --- /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 + */ +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; + } +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index 9a64428c98b89..f77178a6f0502 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -29,7 +29,7 @@ * * @final */ -class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface +class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, ConstructorArgumentTypeExtractorInterface { const PROPERTY = 0; const ACCESSOR = 1; @@ -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); diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 442552724e796..d9fd45b439262 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -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 @@ -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} */ 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..7c3631db27d16 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php @@ -0,0 +1,49 @@ + + * + * 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 + */ +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', [])); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index d352fa12b61f0..cb717ab8f4266 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -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 diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index b9bdafbd33d60..8084555b4216b 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -547,4 +547,23 @@ public function testGetWriteInfoDeprecatedEnableMagicCallExtractionInContext() 'enable_magic_call_extraction' => true, ]); } + + /** + * @dataProvider extractConstructorTypesProvider + */ + public function testExtractConstructorTypes(string $property, array $type = null) + { + $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); + } + + public function extractConstructorTypesProvider(): array + { + 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 04b4b0f8bb24e..d1d1b3ac25cb5 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 = []): ?array return [new Type(Type::BUILTIN_TYPE_INT)]; } + /** + * {@inheritdoc} + */ + public function getTypesFromConstructor(string $class, string $property): ?array + { + return [new Type(Type::BUILTIN_TYPE_STRING)]; + } + /** * {@inheritdoc} */ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy