diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 90911cadf6c6c..f6c9cf60b49d9 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -207,6 +207,29 @@ public function getTypesFromConstructor(string $class, string $property): ?array public function getType(string $class, string $property, array $context = []): ?Type { + // BC layer, remove context key in 8.0 + $extractPublicPropertiesFirst = \array_key_exists('extract_public_properties_first', $context); + if (!$extractPublicPropertiesFirst) { + trigger_deprecation('symfony/property-info', '7.3', 'Not setting "extract_public_properties_first" context key is deprecated, it will default to "true" in 8.0.'); + } + + if ($extractPublicPropertiesFirst) { + try { + $reflectionClass = new \ReflectionClass($class); + $reflectionProperty = $reflectionClass->getProperty($property); + } catch (\ReflectionException) { + return null; + } + + if ($reflectionProperty->isPublic()) { + try { + return $this->typeResolver->resolve($reflectionProperty); + } catch (UnsupportedException $e) { + throw $e; + } + } + } + [$mutatorReflection, $prefix] = $this->getMutatorMethod($class, $property); if ($mutatorReflection) { @@ -240,27 +263,30 @@ public function getType(string $class, string $property, array $context = []): ? } } - try { - $reflectionClass = new \ReflectionClass($class); - $reflectionProperty = $reflectionClass->getProperty($property); - } catch (\ReflectionException) { - return null; - } + // BC layer, remove block in 8.0 + if (!$extractPublicPropertiesFirst) { + try { + $reflectionClass = new \ReflectionClass($class); + $reflectionProperty = $reflectionClass->getProperty($property); + } catch (\ReflectionException) { + return null; + } - try { - return $this->typeResolver->resolve($reflectionProperty); - } catch (UnsupportedException) { - } + try { + return $this->typeResolver->resolve($reflectionProperty); + } catch (UnsupportedException) { + } - if (null === $defaultValue = ($reflectionClass->getDefaultProperties()[$property] ?? null)) { - return null; - } + if (null === $defaultValue = ($reflectionClass->getDefaultProperties()[$property] ?? null)) { + return null; + } - $typeIdentifier = TypeIdentifier::from(static::MAP_TYPES[\gettype($defaultValue)] ?? \gettype($defaultValue)); - $type = 'array' === $typeIdentifier->value ? Type::array() : Type::builtin($typeIdentifier); + $typeIdentifier = TypeIdentifier::from(static::MAP_TYPES[\gettype($defaultValue)] ?? \gettype($defaultValue)); + $type = 'array' === $typeIdentifier->value ? Type::array() : Type::builtin($typeIdentifier); - if ($this->isNullableProperty($class, $property)) { - $type = Type::nullable($type); + if ($this->isNullableProperty($class, $property)) { + $type = Type::nullable($type); + } } return $type; @@ -738,7 +764,7 @@ private function isAllowedProperty(string $class, string $property, bool $writeA /** * Gets the accessor method. * - * Returns an array with a the instance of \ReflectionMethod as first key + * Returns an array with the instance of \ReflectionMethod as first key * and the prefix of the method as second or null if not found. */ private function getAccessorMethod(string $class, string $property): ?array @@ -764,7 +790,7 @@ private function getAccessorMethod(string $class, string $property): ?array } /** - * Returns an array with a the instance of \ReflectionMethod as first key + * Returns an array with the instance of \ReflectionMethod as first key * and the prefix of the method as second or null if not found. */ private function getMutatorMethod(string $class, string $property): ?array diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index 972091d3031f0..ddfb4ac3d8bf1 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -1024,4 +1024,24 @@ public static function extractConstructorTypesProvider(): iterable yield ['dateTime', Type::object(\DateTimeImmutable::class)]; yield ['ddd', null]; } + + /** + * @dataProvider extractPropertyOrderProvider + */ + public function testPublicPropertyExtractedFirst(string $property, Type $type) + { + $this->assertEquals( + $type, + $this->extractor->getType('Symfony\Component\PropertyInfo\Tests\Fixtures\PublicPropertyWithHasDummy', $property, ['extract_public_properties_first' => true]) + ); + } + + /** + * @return iterable + */ + public static function extractPropertyOrderProvider(): iterable + { + yield ['publicProperty', Type::string()]; + yield ['privateProperty', Type::bool()]; + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PublicPropertyWithHasDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PublicPropertyWithHasDummy.php new file mode 100644 index 0000000000000..9a8437ea0d4b7 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/PublicPropertyWithHasDummy.php @@ -0,0 +1,27 @@ + + * + * 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; + +class PublicPropertyWithHasDummy +{ + public string $publicProperty; + + private string $privateProperty; + + public function hasPublicProperty(): bool + { + } + + public function hasPrivateProperty(): bool + { + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index fb45a924bee70..02900bfdd7e04 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -373,7 +373,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } } - if (null !== $type = $this->getType($resolvedClass, $attribute)) { + if (null !== $type = $this->getType($resolvedClass, $attribute, $context)) { try { // BC layer for PropertyTypeExtractorInterface::getTypes(). // Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). @@ -926,7 +926,7 @@ private function validateAndDenormalize(Type $type, string $currentClass, string */ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, string $parameterName, mixed $parameterData, array $context, ?string $format = null): mixed { - if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $type = $this->getType($class->getName(), $parameterName)) { + if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $type = $this->getType($class->getName(), $parameterName, $context)) { return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format); } @@ -946,7 +946,7 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara /** * @return Type|list|null */ - private function getType(string $currentClass, string $attribute): Type|array|null + private function getType(string $currentClass, string $attribute, array $context = []): Type|array|null { if (null === $this->propertyTypeExtractor) { return null; @@ -957,7 +957,7 @@ private function getType(string $currentClass, string $attribute): Type|array|nu return false === $this->typeCache[$key] ? null : $this->typeCache[$key]; } - if (null !== $type = $this->getPropertyType($currentClass, $attribute)) { + if (null !== $type = $this->getPropertyType($currentClass, $attribute, $context)) { return $this->typeCache[$key] = $type; } @@ -967,7 +967,7 @@ private function getType(string $currentClass, string $attribute): Type|array|nu } foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) { - if (null !== $type = $this->getPropertyType($mappedClass, $attribute)) { + if (null !== $type = $this->getPropertyType($mappedClass, $attribute, $context)) { return $this->typeCache[$key] = $type; } } @@ -984,10 +984,10 @@ private function getType(string $currentClass, string $attribute): Type|array|nu * * @return Type|list|null */ - private function getPropertyType(string $className, string $property): Type|array|null + private function getPropertyType(string $className, string $property, array $context = []): Type|array|null { if (class_exists(Type::class) && method_exists($this->propertyTypeExtractor, 'getType')) { - return $this->propertyTypeExtractor->getType($className, $property); + return $this->propertyTypeExtractor->getType($className, $property, $context); } return $this->propertyTypeExtractor->getTypes($className, $property); diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index e59e4402059bd..7b0701cfee169 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -1674,6 +1674,30 @@ public function testPartialDenormalizationWithInvalidVariadicParameter() DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, ]); } + + public function testExtractPublicPropertiesFirst() + { + $json = '{ "publicProperty": "foo", "privateProperty": true }'; + + $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $result = $serializer->deserialize( + $json, + DummyPublicPropertyWithHas::class, + 'json', + [ + 'extract_public_properties_first' => true, + ] + ); + + $this->assertEquals( + 'foo', + $result->publicProperty + ); + $this->assertEquals( + '1', + $result->getPrivateProperty() + ); + } } class Model @@ -1828,6 +1852,27 @@ public function getIterator(): \ArrayIterator } } +class DummyPublicPropertyWithHas +{ + public string $publicProperty; + + private string $privateProperty; + + public function hasPublicProperty(): bool + { + } + + public function getPrivateProperty(): bool + { + return $this->privateProperty; + } + + public function setPrivateProperty(bool $value): void + { + $this->privateProperty = (string) $value; + } +} + abstract class DummyNormalizer implements NormalizerInterface, DenormalizerInterface { abstract public function getSupportedTypes(?string $format): array;