Skip to content

Commit d524bc9

Browse files
bug #61028 [Serializer] Fix readonly property initialization from incorrect scope (santysisi)
This PR was merged into the 6.4 branch. Discussion ---------- [Serializer] Fix `readonly` property initialization from incorrect scope | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix #60846 | License | MIT Readonly properties couldn't be initialized during denormalization due to scope restrictions. This change checks if a property is `readonly` and uninitialized, if so, it sets the value using the declaring class's scope. Also added a safety check to throw a `LogicException` if a `readonly` property is already initialized, to avoid accidental mutation. Commits ------- 79c2ea6 [Serializer] Fix readonly property initialization from incorrect scope
2 parents 65eb6cf + 79c2ea6 commit d524bc9

File tree

6 files changed

+134
-1
lines changed

6 files changed

+134
-1
lines changed

src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
1515
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
16+
use Symfony\Component\Serializer\Exception\LogicException;
1617
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
1718
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
1819
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -202,7 +203,22 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v
202203
return;
203204
}
204205

205-
$reflectionProperty->setValue($object, $value);
206+
if (!$reflectionProperty->isReadOnly()) {
207+
$reflectionProperty->setValue($object, $value);
208+
209+
return;
210+
}
211+
212+
if (!$reflectionProperty->isInitialized($object)) {
213+
$declaringClass = $reflectionProperty->getDeclaringClass();
214+
$declaringClass->getProperty($reflectionProperty->getName())->setValue($object, $value);
215+
216+
return;
217+
}
218+
219+
if ($reflectionProperty->getValue($object) !== $value) {
220+
throw new LogicException(\sprintf('Attempting to change readonly property "%s"::$%s.', $object::class, $reflectionProperty->getName()));
221+
}
206222
}
207223

208224
/**
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
class BookDummy
15+
{
16+
public function __construct(
17+
public private(set) string $title,
18+
public protected(set) string $author,
19+
protected private(set) int $pubYear,
20+
) {
21+
}
22+
23+
public function getPubYear(): int
24+
{
25+
return $this->pubYear;
26+
}
27+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
readonly class ChildClassDummy extends ParentClassDummy
15+
{
16+
public string $childProp;
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
readonly class ParentClassDummy
15+
{
16+
private string $parentProp;
17+
18+
public function getParentProp(): string
19+
{
20+
return $this->parentProp;
21+
}
22+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
class SpecialBookDummy extends BookDummy
15+
{
16+
}

src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@
3030
use Symfony\Component\Serializer\SerializerInterface;
3131
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\GroupDummy;
3232
use Symfony\Component\Serializer\Tests\Fixtures\Attributes\GroupDummyChild;
33+
use Symfony\Component\Serializer\Tests\Fixtures\ChildClassDummy;
3334
use Symfony\Component\Serializer\Tests\Fixtures\Dummy;
3435
use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy;
3536
use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy;
3637
use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder;
38+
use Symfony\Component\Serializer\Tests\Fixtures\SpecialBookDummy;
3739
use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait;
3840
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
3941
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
@@ -174,6 +176,39 @@ public function testDenormalize()
174176
$this->assertEquals('bar', $obj->getBar());
175177
}
176178

179+
/**
180+
* @requires PHP 8.2
181+
*/
182+
public function testDenormalizeWithReadOnlyClass()
183+
{
184+
/** @var ChildClassDummy $object */
185+
$object = $this->normalizer->denormalize(
186+
['parentProp' => 'parentProp', 'childProp' => 'childProp'],
187+
ChildClassDummy::class,
188+
'any'
189+
);
190+
191+
$this->assertSame('parentProp', $object->getParentProp());
192+
$this->assertSame('childProp', $object->childProp);
193+
}
194+
195+
/**
196+
* @requires PHP 8.4
197+
*/
198+
public function testDenormalizeWithAsymmetricPropertyVisibility()
199+
{
200+
/** @var SpecialBookDummy $object */
201+
$object = $this->normalizer->denormalize(
202+
['title' => 'life', 'author' => 'Santiago San Martin', 'pubYear' => 2000],
203+
SpecialBookDummy::class,
204+
'any'
205+
);
206+
207+
$this->assertSame('life', $object->title);
208+
$this->assertSame('Santiago San Martin', $object->author);
209+
$this->assertSame(2000, $object->getPubYear());
210+
}
211+
177212
public function testNormalizeWithParentClass()
178213
{
179214
$group = new GroupDummyChild();

0 commit comments

Comments
 (0)