From 79c2ea622d7eec2206075f60671932108cbafad4 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Wed, 2 Jul 2025 20:26:03 -0300 Subject: [PATCH] [Serializer] Fix readonly property initialization from incorrect scope --- .../Normalizer/PropertyNormalizer.php | 18 +++++++++- .../Serializer/Tests/Fixtures/BookDummy.php | 27 ++++++++++++++ .../Tests/Fixtures/ChildClassDummy.php | 17 +++++++++ .../Tests/Fixtures/ParentClassDummy.php | 22 ++++++++++++ .../Tests/Fixtures/SpecialBookDummy.php | 16 +++++++++ .../Normalizer/PropertyNormalizerTest.php | 35 +++++++++++++++++++ 6 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/BookDummy.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/ChildClassDummy.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/ParentClassDummy.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/SpecialBookDummy.php diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 6a5d0acd8904d..fdd48b047a654 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -13,6 +13,7 @@ use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -202,7 +203,22 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v return; } - $reflectionProperty->setValue($object, $value); + if (!$reflectionProperty->isReadOnly()) { + $reflectionProperty->setValue($object, $value); + + return; + } + + if (!$reflectionProperty->isInitialized($object)) { + $declaringClass = $reflectionProperty->getDeclaringClass(); + $declaringClass->getProperty($reflectionProperty->getName())->setValue($object, $value); + + return; + } + + if ($reflectionProperty->getValue($object) !== $value) { + throw new LogicException(\sprintf('Attempting to change readonly property "%s"::$%s.', $object::class, $reflectionProperty->getName())); + } } /** diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/BookDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/BookDummy.php new file mode 100644 index 0000000000000..2b62667c0dbc3 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/BookDummy.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\Serializer\Tests\Fixtures; + +class BookDummy +{ + public function __construct( + public private(set) string $title, + public protected(set) string $author, + protected private(set) int $pubYear, + ) { + } + + public function getPubYear(): int + { + return $this->pubYear; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/ChildClassDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/ChildClassDummy.php new file mode 100644 index 0000000000000..f8d4f0743a4c4 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/ChildClassDummy.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +readonly class ChildClassDummy extends ParentClassDummy +{ + public string $childProp; +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/ParentClassDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/ParentClassDummy.php new file mode 100644 index 0000000000000..730d71aecfec9 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/ParentClassDummy.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +readonly class ParentClassDummy +{ + private string $parentProp; + + public function getParentProp(): string + { + return $this->parentProp; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SpecialBookDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SpecialBookDummy.php new file mode 100644 index 0000000000000..44679e2f205f0 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SpecialBookDummy.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +class SpecialBookDummy extends BookDummy +{ +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 0601a2d602084..386f0613d60ed 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -30,10 +30,12 @@ use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\GroupDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\GroupDummyChild; +use Symfony\Component\Serializer\Tests\Fixtures\ChildClassDummy; use Symfony\Component\Serializer\Tests\Fixtures\Dummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy; use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy; use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder; +use Symfony\Component\Serializer\Tests\Fixtures\SpecialBookDummy; use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait; @@ -174,6 +176,39 @@ public function testDenormalize() $this->assertEquals('bar', $obj->getBar()); } + /** + * @requires PHP 8.2 + */ + public function testDenormalizeWithReadOnlyClass() + { + /** @var ChildClassDummy $object */ + $object = $this->normalizer->denormalize( + ['parentProp' => 'parentProp', 'childProp' => 'childProp'], + ChildClassDummy::class, + 'any' + ); + + $this->assertSame('parentProp', $object->getParentProp()); + $this->assertSame('childProp', $object->childProp); + } + + /** + * @requires PHP 8.4 + */ + public function testDenormalizeWithAsymmetricPropertyVisibility() + { + /** @var SpecialBookDummy $object */ + $object = $this->normalizer->denormalize( + ['title' => 'life', 'author' => 'Santiago San Martin', 'pubYear' => 2000], + SpecialBookDummy::class, + 'any' + ); + + $this->assertSame('life', $object->title); + $this->assertSame('Santiago San Martin', $object->author); + $this->assertSame(2000, $object->getPubYear()); + } + public function testNormalizeWithParentClass() { $group = new GroupDummyChild();