diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index ae2523e515d0c..f0bb7e285acd4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -73,6 +73,7 @@ class UnusedTagsPass implements CompilerPassInterface 'property_info.initializable_extractor', 'property_info.list_extractor', 'property_info.type_extractor', + 'property_info.attributes_extractor', 'proxy', 'remote_event.consumer', 'routing.condition_service', diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index ade8c70a9ef77..386ab4ea63437 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -130,6 +130,7 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyAttributesExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; @@ -642,6 +643,8 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('property_info.access_extractor'); $container->registerForAutoconfiguration(PropertyInitializableExtractorInterface::class) ->addTag('property_info.initializable_extractor'); + $container->registerForAutoconfiguration(PropertyAttributesExtractorInterface::class) + ->addTag('property_info.attributes_extractor'); $container->registerForAutoconfiguration(EncoderInterface::class) ->addTag('serializer.encoder'); $container->registerForAutoconfiguration(DecoderInterface::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php index 90587839d54c4..b491546bd51c0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php @@ -13,6 +13,7 @@ use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyAttributesExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; @@ -26,7 +27,7 @@ return static function (ContainerConfigurator $container) { $container->services() ->set('property_info', PropertyInfoExtractor::class) - ->args([[], [], [], [], []]) + ->args([[], [], [], [], [], []]) ->alias(PropertyAccessExtractorInterface::class, 'property_info') ->alias(PropertyDescriptionExtractorInterface::class, 'property_info') @@ -34,6 +35,7 @@ ->alias(PropertyTypeExtractorInterface::class, 'property_info') ->alias(PropertyListExtractorInterface::class, 'property_info') ->alias(PropertyInitializableExtractorInterface::class, 'property_info') + ->alias(PropertyAttributesExtractorInterface::class, 'property_info') ->set('property_info.cache', PropertyInfoCacheExtractor::class) ->decorate('property_info') @@ -45,6 +47,7 @@ ->tag('property_info.type_extractor', ['priority' => -1002]) ->tag('property_info.access_extractor', ['priority' => -1000]) ->tag('property_info.initializable_extractor', ['priority' => -1000]) + ->tag('property_info.attributes_extractor', ['priority' => -1000]) ->alias(PropertyReadInfoExtractorInterface::class, 'property_info.reflection_extractor') ->alias(PropertyWriteInfoExtractorInterface::class, 'property_info.reflection_extractor') diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 490dab43b4754..77492aa27d2b4 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add `PropertyAttributesExtractorInterface` to extract property attributes (implemented by `ReflectionExtractor`) + 7.1 --- diff --git a/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoPass.php b/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoPass.php index de2a374ec564d..37cac5d8a32ef 100644 --- a/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoPass.php +++ b/src/Symfony/Component/PropertyInfo/DependencyInjection/PropertyInfoPass.php @@ -47,5 +47,8 @@ public function process(ContainerBuilder $container): void $initializableExtractors = $this->findAndSortTaggedServices('property_info.initializable_extractor', $container); $definition->setArgument(4, new IteratorArgument($initializableExtractors)); + + $attributesExtractor = $this->findAndSortTaggedServices('property_info.attributes_extractor', $container); + $definition->setArgument(5, new IteratorArgument($attributesExtractor)); } } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 953e33f04f27c..a5af834c6dc34 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyInfo\Extractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyAttributesExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyReadInfo; @@ -36,7 +37,7 @@ * * @final */ -class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface +class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface, PropertyAttributesExtractorInterface { /** * @internal @@ -507,6 +508,30 @@ public function getWriteInfo(string $class, string $property, array $context = [ return $noneProperty; } + public function getAttributes(string $class, string $property, array $context = []): ?array + { + try { + $reflClass = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + if (!$reflClass->hasProperty($property)) { + return null; + } + + $attributes = []; + $reflProperty = $reflClass->getProperty($property); + foreach ($reflProperty->getAttributes() as $attribute) { + $attributes[] = [ + 'name' => $attribute->getName(), + 'arguments' => $attribute->getArguments(), + ]; + } + + return $attributes; + } + /** * @return LegacyType[]|null */ diff --git a/src/Symfony/Component/PropertyInfo/PropertyAttributesExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyAttributesExtractorInterface.php new file mode 100644 index 0000000000000..b0d3600c7396e --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyAttributesExtractorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * @author Andrew Alyamovsky + */ +interface PropertyAttributesExtractorInterface +{ + /** + * Gets the attributes of the property. + * + * Returns an array of attributes, each attribute is an associative array with the following keys: + * - name: The fully-qualified class name of the attribute + * - arguments: An associative array of attribute arguments if present + * + * @return array}>|null + */ + public function getAttributes(string $class, string $property, array $context = []): ?array; +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php index 38b9c68a2e29e..85e8a07a8f2ab 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php @@ -21,7 +21,7 @@ * * @final */ -class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface +class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface, PropertyAttributesExtractorInterface { private array $arrayCache = []; @@ -69,6 +69,11 @@ public function getTypes(string $class, string $property, array $context = []): return $this->extract('getTypes', [$class, $property, $context]); } + public function getAttributes(string $class, string $property, array $context = []): ?array + { + return $this->extract('getAttributes', [$class, $property, $context]); + } + public function isInitializable(string $class, string $property, array $context = []): ?bool { return $this->extract('isInitializable', [$class, $property, $context]); diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php index 8e8952c7f4e23..6ab19dba8238a 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php @@ -20,7 +20,7 @@ * * @final */ -class PropertyInfoExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface +class PropertyInfoExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface, PropertyAttributesExtractorInterface { /** * @param iterable $listExtractors @@ -28,6 +28,7 @@ class PropertyInfoExtractor implements PropertyInfoExtractorInterface, PropertyI * @param iterable $descriptionExtractors * @param iterable $accessExtractors * @param iterable $initializableExtractors + * @param iterable $attributesExtractors */ public function __construct( private readonly iterable $listExtractors = [], @@ -35,6 +36,7 @@ public function __construct( private readonly iterable $descriptionExtractors = [], private readonly iterable $accessExtractors = [], private readonly iterable $initializableExtractors = [], + private readonly iterable $attributesExtractors = [], ) { } @@ -66,6 +68,11 @@ public function getTypes(string $class, string $property, array $context = []): return $this->extract($this->typeExtractors, 'getTypes', [$class, $property, $context]); } + public function getAttributes(string $class, string $property, array $context = []): ?array + { + return $this->extract($this->attributesExtractors, 'getAttributes', [$class, $property, $context]); + } + public function isReadable(string $class, string $property, array $context = []): ?bool { return $this->extract($this->accessExtractors, 'isReadable', [$class, $property, $context]); diff --git a/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoPassTest.php b/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoPassTest.php index a1db4822e045c..120f0eefdb255 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoPassTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/DependencyInjection/PropertyInfoPassTest.php @@ -26,7 +26,7 @@ public function testServicesAreOrderedAccordingToPriority($index, $tag) { $container = new ContainerBuilder(); - $definition = $container->register('property_info')->setArguments([null, null, null, null, null]); + $definition = $container->register('property_info')->setArguments([null, null, null, null, null, null]); $container->register('n2')->addTag($tag, ['priority' => 100]); $container->register('n1')->addTag($tag, ['priority' => 200]); $container->register('n3')->addTag($tag); @@ -50,6 +50,7 @@ public static function provideTags() [2, 'property_info.description_extractor'], [3, 'property_info.access_extractor'], [4, 'property_info.initializable_extractor'], + [5, 'property_info.attributes_extractor'], ]; } @@ -57,7 +58,7 @@ public function testReturningEmptyArrayWhenNoService() { $container = new ContainerBuilder(); $propertyInfoExtractorDefinition = $container->register('property_info') - ->setArguments([[], [], [], [], []]); + ->setArguments([[], [], [], [], [], []]); $propertyInfoPass = new PropertyInfoPass(); $propertyInfoPass->process($container); @@ -67,5 +68,6 @@ public function testReturningEmptyArrayWhenNoService() $this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(2)); $this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(3)); $this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(4)); + $this->assertEquals(new IteratorArgument([]), $propertyInfoExtractorDefinition->getArgument(5)); } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index d5e5c676727ed..a222193332038 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -19,6 +19,8 @@ use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyAttribute; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyWithAttributes; use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy; @@ -498,6 +500,108 @@ public static function getInitializableProperties(): array ]; } + /** + * @dataProvider attributesProvider + */ + public function testGetAttributes(string $class, string $property, ?array $expected) + { + $this->assertSame($expected, $this->extractor->getAttributes($class, $property)); + } + + public static function attributesProvider(): array + { + return [ + [ + DummyWithAttributes::class, + 'a', + [ + [ + 'name' => DummyAttribute::class, + 'arguments' => [ + 'type' => 'foo', + 'name' => 'nameA', + 'version' => 1, + ], + ], + ], + ], + [ + DummyWithAttributes::class, + 'b', + [ + [ + 'name' => DummyAttribute::class, + 'arguments' => [ + 'type' => 'bar', + 'name' => 'nameB', + 'version' => 2, + ], + ], + ], + ], + [ + DummyWithAttributes::class, + 'c', + [ + [ + 'name' => DummyAttribute::class, + 'arguments' => [ + 'type' => 'baz', + 'name' => 'nameC', + 'version' => 3, + ], + ], + ], + ], + [ + DummyWithAttributes::class, + 'd', + [ + [ + 'name' => DummyAttribute::class, + 'arguments' => [ + 0 => 'foo', + 1 => 'nameD', + 2 => 4, + ], + ], + ], + ], + [ + DummyWithAttributes::class, + 'e', + [ + [ + 'name' => DummyAttribute::class, + 'arguments' => [ + 'type' => 'foo', + 'name' => 'nameE1', + 'version' => 5, + ], + ], + [ + 'name' => DummyAttribute::class, + 'arguments' => [ + 'type' => 'foo', + 'name' => 'nameE2', + 'version' => 10, + ], + ], + ], + ], + [ + DummyWithAttributes::class, + 'f', + [], + ], + [ + DummyWithAttributes::class, + 'nonExistentProperty', + null, + ], + ]; + } + /** * @group legacy * diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyAttribute.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyAttribute.php new file mode 100644 index 0000000000000..1c6bac8cf1e89 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyAttribute.php @@ -0,0 +1,14 @@ + */ -class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, ConstructorArgumentTypeExtractorInterface +class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, ConstructorArgumentTypeExtractorInterface, PropertyAttributesExtractorInterface { public function getShortDescription($class, $property, array $context = []): ?string { @@ -45,6 +46,19 @@ public function getType($class, $property, array $context = []): ?Type return Type::int(); } + public function getAttributes($class, $property, array $context = []): ?array + { + return [ + [ + 'name' => \stdClass::class, + 'arguments' => [ + 'foo' => 'bar', + 'baz' => 'qux', + ], + ], + ]; + } + public function getTypesFromConstructor(string $class, string $property): ?array { return [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]; diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyWithAttributes.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyWithAttributes.php new file mode 100644 index 0000000000000..02f00832d2dbe --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyWithAttributes.php @@ -0,0 +1,24 @@ + */ -class NullExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface +class NullExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyAttributesExtractorInterface { public function getShortDescription($class, $property, array $context = []): ?string { @@ -57,6 +58,14 @@ public function getType($class, $property, array $context = []): ?Type return null; } + public function getAttributes($class, $property, array $context = []): ?array + { + $this->assertIsString($class); + $this->assertIsString($property); + + return null; + } + public function isReadable($class, $property, array $context = []): ?bool { $this->assertIsString($class);