diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index a81f3124a67c4..432be688a73c5 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.3.0 +----- + +* added `exclude_static_properties` option to `ReflectionExtractor` to exclude public static properties when listing properties + 4.2.0 ----- diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index b74a6115aae31..c4ef26711ef56 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -27,6 +27,8 @@ */ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface { + public const EXCLUDE_STATIC_PROPERTIES = 'exclude_static_properties'; + /** * @internal */ @@ -75,7 +77,7 @@ public function getProperties($class, array $context = array()) $properties = array(); foreach ($reflectionProperties as $reflectionProperty) { - if ($reflectionProperty->isPublic()) { + if ($reflectionProperty->isPublic() && (!($context[self::EXCLUDE_STATIC_PROPERTIES] ?? false) || !$reflectionProperty->isStatic())) { $properties[$reflectionProperty->name] = $reflectionProperty->name; } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 762b0fed481cd..2ee90067c1713 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -34,113 +34,176 @@ protected function setUp() $this->extractor = new ReflectionExtractor(); } - public function testGetProperties() + /** + * @dataProvider getPropertiesProvider + */ + public function testGetProperties($class, $expected, $mutatorPrefixes, $accessorPrefixes, $arrayMutatorPrefixes, $context) { - $this->assertSame( - array( - 'bal', - 'parent', - 'collection', - 'nestedCollection', - 'mixedCollection', - 'B', - 'Guid', - 'g', - 'h', - 'i', - 'j', - 'emptyVar', - 'iteratorCollection', - 'iteratorCollectionWithKey', - 'nestedIterators', - 'foo', - 'foo2', - 'foo3', - 'foo4', - 'foo5', - 'files', - 'a', - 'DOB', - 'Id', - '123', - 'self', - 'realParent', - 'c', - 'd', - 'e', - 'f', - ), - $this->extractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy') - ); + $extractor = new ReflectionExtractor($mutatorPrefixes, $accessorPrefixes, $arrayMutatorPrefixes); - $this->assertNull($this->extractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\NoProperties')); + $this->assertSame($expected, $extractor->getProperties($class, $context)); } - public function testGetPropertiesWithCustomPrefixes() + public function getPropertiesProvider() { - $customExtractor = new ReflectionExtractor(array('add', 'remove'), array('is', 'can')); - - $this->assertSame( + return array( array( - 'bal', - 'parent', - 'collection', - 'nestedCollection', - 'mixedCollection', - 'B', - 'Guid', - 'g', - 'h', - 'i', - 'j', - 'emptyVar', - 'iteratorCollection', - 'iteratorCollectionWithKey', - 'nestedIterators', - 'foo', - 'foo2', - 'foo3', - 'foo4', - 'foo5', - 'files', - 'c', - 'd', - 'e', - 'f', + 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', + array( + 'bal', + 'parent', + 'collection', + 'nestedCollection', + 'mixedCollection', + 'B', + 'Guid', + 'g', + 'h', + 'i', + 'j', + 'k', + 'emptyVar', + 'iteratorCollection', + 'iteratorCollectionWithKey', + 'nestedIterators', + 'foo', + 'foo2', + 'foo3', + 'foo4', + 'foo5', + 'files', + 'a', + 'DOB', + 'Id', + '123', + 'self', + 'realParent', + 'c', + 'd', + 'e', + 'f', + ), + null, + null, + null, + array(), + ), + array( + 'Symfony\Component\PropertyInfo\Tests\Fixtures\NoProperties', + null, + null, + null, + null, + array(), + ), + array( + 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', + array( + 'bal', + 'parent', + 'collection', + 'nestedCollection', + 'mixedCollection', + 'B', + 'Guid', + 'g', + 'h', + 'i', + 'j', + 'k', + 'emptyVar', + 'iteratorCollection', + 'iteratorCollectionWithKey', + 'nestedIterators', + 'foo', + 'foo2', + 'foo3', + 'foo4', + 'foo5', + 'files', + 'c', + 'd', + 'e', + 'f', + ), + array('add', 'remove'), + array('is', 'can'), + null, + array(), + ), + array( + 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', + array( + 'bal', + 'parent', + 'collection', + 'nestedCollection', + 'mixedCollection', + 'B', + 'Guid', + 'g', + 'h', + 'i', + 'j', + 'k', + 'emptyVar', + 'iteratorCollection', + 'iteratorCollectionWithKey', + 'nestedIterators', + 'foo', + 'foo2', + 'foo3', + 'foo4', + 'foo5', + 'files', + ), + array(), + array(), + array(), + array(), ), - $customExtractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy') - ); - } - - public function testGetPropertiesWithNoPrefixes() - { - $noPrefixExtractor = new ReflectionExtractor(array(), array(), array()); - - $this->assertSame( array( - 'bal', - 'parent', - 'collection', - 'nestedCollection', - 'mixedCollection', - 'B', - 'Guid', - 'g', - 'h', - 'i', - 'j', - 'emptyVar', - 'iteratorCollection', - 'iteratorCollectionWithKey', - 'nestedIterators', - 'foo', - 'foo2', - 'foo3', - 'foo4', - 'foo5', - 'files', + 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', + array( + 'bal', + 'parent', + 'collection', + 'nestedCollection', + 'mixedCollection', + 'B', + 'Guid', + 'g', + 'h', + 'i', + 'j', + 'emptyVar', + 'iteratorCollection', + 'iteratorCollectionWithKey', + 'nestedIterators', + 'foo', + 'foo2', + 'foo3', + 'foo4', + 'foo5', + 'files', + 'a', + 'DOB', + 'Id', + '123', + 'self', + 'realParent', + 'c', + 'd', + 'e', + 'f', + ), + null, + null, + null, + array( + ReflectionExtractor::EXCLUDE_STATIC_PROPERTIES => true, + ), ), - $noPrefixExtractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy') ); } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index 9bd856bd47df5..f6ebdc8c3fbe1 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -93,6 +93,11 @@ class Dummy extends ParentDummy */ public $j; + /** + * @var ?string + */ + public static $k; + /** * This should not be removed. * diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 6d8a9ff51e84b..545b9065c6acc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -14,6 +14,8 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; @@ -32,7 +34,9 @@ class ObjectNormalizer extends AbstractObjectNormalizer private $discriminatorCache = array(); - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = array()) + private $propertyListExtractor; + + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = array(), PropertyListExtractorInterface $propertyListExtractor = null) { if (!\class_exists(PropertyAccess::class)) { throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.'); @@ -41,6 +45,7 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext); $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->propertyListExtractor = $propertyListExtractor ?? new ReflectionExtractor(); } /** @@ -56,55 +61,16 @@ public function hasCacheableSupportsMethod(): bool */ protected function extractAttributes($object, $format = null, array $context = array()) { - // If not using groups, detect manually - $attributes = array(); - - // methods - $reflClass = new \ReflectionClass($object); - foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) { - if ( - 0 !== $reflMethod->getNumberOfRequiredParameters() || - $reflMethod->isStatic() || - $reflMethod->isConstructor() || - $reflMethod->isDestructor() - ) { - continue; - } - - $name = $reflMethod->name; - $attributeName = null; - - if (0 === strpos($name, 'get') || 0 === strpos($name, 'has')) { - // getters and hassers - $attributeName = substr($name, 3); + $properties = $this->propertyListExtractor->getProperties(\get_class($object), array(ReflectionExtractor::EXCLUDE_STATIC_PROPERTIES => true)); - if (!$reflClass->hasProperty($attributeName)) { - $attributeName = lcfirst($attributeName); - } - } elseif (0 === strpos($name, 'is')) { - // issers - $attributeName = substr($name, 2); - - if (!$reflClass->hasProperty($attributeName)) { - $attributeName = lcfirst($attributeName); - } + $allowedProperties = array(); + foreach ($properties as $property) { + if ($this->isAllowedAttribute($object, $property, $format, $context)) { + $allowedProperties[] = $property; } - - if (null !== $attributeName && $this->isAllowedAttribute($object, $attributeName, $format, $context)) { - $attributes[$attributeName] = true; - } - } - - // properties - foreach ($reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflProperty) { - if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name, $format, $context)) { - continue; - } - - $attributes[$reflProperty->name] = true; } - return array_keys($attributes); + return $allowedProperties; } /** diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 9eff7914d1c89..98f8d8b450eda 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -867,7 +867,7 @@ public function testExtractAttributesRespectsFormat() $data->setFoo('bar'); $data->bar = 'foo'; - $this->assertSame(array('foo' => 'bar', 'bar' => 'foo'), $normalizer->normalize($data, 'foo_and_bar_included')); + $this->assertSame(array('bar' => 'foo', 'foo' => 'bar'), $normalizer->normalize($data, 'foo_and_bar_included')); } public function testExtractAttributesRespectsContext() @@ -878,7 +878,7 @@ public function testExtractAttributesRespectsContext() $data->setFoo('bar'); $data->bar = 'foo'; - $this->assertSame(array('foo' => 'bar', 'bar' => 'foo'), $normalizer->normalize($data, null, array('include_foo_and_bar' => true))); + $this->assertSame(array('bar' => 'foo', 'foo' => 'bar'), $normalizer->normalize($data, null, array('include_foo_and_bar' => true))); } public function testAttributesContextNormalize() diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 52961bb7c0528..41de6d150e51e 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -17,7 +17,8 @@ ], "require": { "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" + "symfony/polyfill-ctype": "~1.8", + "symfony/property-info": "~4.2" }, "require-dev": { "symfony/yaml": "~3.4|~4.0", @@ -25,7 +26,6 @@ "symfony/property-access": "~3.4|~4.0", "symfony/http-foundation": "~3.4|~4.0", "symfony/cache": "~3.4|~4.0", - "symfony/property-info": "~3.4|~4.0", "symfony/validator": "~3.4|~4.0", "doctrine/annotations": "~1.0", "symfony/dependency-injection": "~3.4|~4.0",