Skip to content

Commit 1a23213

Browse files
committed
bug #54148 [Serializer] Fix object normalizer when properties has the same name as their accessor (NeilPeyssard)
This PR was merged into the 5.4 branch. Discussion ---------- [Serializer] Fix object normalizer when properties has the same name as their accessor | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix #54109 | License | MIT Hello, This PR fix a bug in object normalization when properties has the same name as their accessor. The current behavior mess up class metadata between properties and methods so the keys in the normalized format does not match the object properties, and metadata are not correctly applied (`SerializedName` in our exemple). This bug also affects versions 6.4 and 7+, but I'm not sure if we can merge this PR in these branches without refactoring. Let me know if another PR should be open for version 6.4 and 7. Commits ------- 8575199f30 [Serializer] Fix object normalizer when properties has the same name as their accessor
2 parents 3cff915 + 096cddc commit 1a23213

File tree

6 files changed

+238
-5
lines changed

6 files changed

+238
-5
lines changed

Mapping/Loader/AnnotationLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
106106

107107
$accessorOrMutator = preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches);
108108
if ($accessorOrMutator) {
109-
$attributeName = lcfirst($matches[2]);
109+
$attributeName = $reflectionClass->hasProperty($method->name) ? $method->name : lcfirst($matches[2]);
110110

111111
if (isset($attributesMetadata[$attributeName])) {
112112
$attributeMetadata = $attributesMetadata[$attributeName];

Normalizer/ObjectNormalizer.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,25 @@ protected function extractAttributes(object $object, ?string $format = null, arr
8686

8787
if (str_starts_with($name, 'get') || str_starts_with($name, 'has')) {
8888
// getters and hassers
89-
$attributeName = substr($name, 3);
89+
$attributeName = $name;
9090

9191
if (!$reflClass->hasProperty($attributeName)) {
92-
$attributeName = lcfirst($attributeName);
92+
$attributeName = substr($attributeName, 3);
93+
94+
if (!$reflClass->hasProperty($attributeName)) {
95+
$attributeName = lcfirst($attributeName);
96+
}
9397
}
9498
} elseif (str_starts_with($name, 'is')) {
9599
// issers
96-
$attributeName = substr($name, 2);
100+
$attributeName = $name;
97101

98102
if (!$reflClass->hasProperty($attributeName)) {
99-
$attributeName = lcfirst($attributeName);
103+
$attributeName = substr($attributeName, 2);
104+
105+
if (!$reflClass->hasProperty($attributeName)) {
106+
$attributeName = lcfirst($attributeName);
107+
}
100108
}
101109
}
102110

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 SamePropertyAsMethodDummy
15+
{
16+
private $freeTrial;
17+
private $hasSubscribe;
18+
private $getReady;
19+
private $isActive;
20+
21+
public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive)
22+
{
23+
$this->freeTrial = $freeTrial;
24+
$this->hasSubscribe = $hasSubscribe;
25+
$this->getReady = $getReady;
26+
$this->isActive = $isActive;
27+
}
28+
29+
public function getFreeTrial()
30+
{
31+
return $this->freeTrial;
32+
}
33+
34+
public function hasSubscribe()
35+
{
36+
return $this->hasSubscribe;
37+
}
38+
39+
public function getReady()
40+
{
41+
return $this->getReady;
42+
}
43+
44+
public function isActive()
45+
{
46+
return $this->isActive;
47+
}
48+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
use Symfony\Component\Serializer\Annotation\SerializedName;
15+
16+
class SamePropertyAsMethodWithMethodSerializedNameDummy
17+
{
18+
private $freeTrial;
19+
private $hasSubscribe;
20+
private $getReady;
21+
private $isActive;
22+
23+
public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive)
24+
{
25+
$this->freeTrial = $freeTrial;
26+
$this->hasSubscribe = $hasSubscribe;
27+
$this->getReady = $getReady;
28+
$this->isActive = $isActive;
29+
}
30+
31+
/**
32+
* @SerializedName("free_trial_method")
33+
*/
34+
public function getFreeTrial()
35+
{
36+
return $this->freeTrial;
37+
}
38+
39+
/**
40+
* @SerializedName("has_subscribe_method")
41+
*/
42+
public function hasSubscribe()
43+
{
44+
return $this->hasSubscribe;
45+
}
46+
47+
/**
48+
* @SerializedName("get_ready_method")
49+
*/
50+
public function getReady()
51+
{
52+
return $this->getReady;
53+
}
54+
55+
/**
56+
* @SerializedName("is_active_method")
57+
*/
58+
public function isActive()
59+
{
60+
return $this->isActive;
61+
}
62+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
use Symfony\Component\Serializer\Annotation\SerializedName;
15+
16+
class SamePropertyAsMethodWithPropertySerializedNameDummy
17+
{
18+
/**
19+
* @SerializedName("free_trial_property")
20+
*/
21+
private $freeTrial;
22+
23+
/**
24+
* @SerializedName("has_subscribe_property")
25+
*/
26+
private $hasSubscribe;
27+
28+
/**
29+
* @SerializedName("get_ready_property")
30+
*/
31+
private $getReady;
32+
33+
/**
34+
* @SerializedName("is_active_property")
35+
*/
36+
private $isActive;
37+
38+
public function __construct($freeTrial, $hasSubscribe, $getReady, $isActive)
39+
{
40+
$this->freeTrial = $freeTrial;
41+
$this->hasSubscribe = $hasSubscribe;
42+
$this->getReady = $getReady;
43+
$this->isActive = $isActive;
44+
}
45+
46+
public function getFreeTrial()
47+
{
48+
return $this->freeTrial;
49+
}
50+
51+
public function hasSubscribe()
52+
{
53+
return $this->hasSubscribe;
54+
}
55+
56+
public function getReady()
57+
{
58+
return $this->getReady;
59+
}
60+
61+
public function isActive()
62+
{
63+
return $this->isActive;
64+
}
65+
}

Tests/Normalizer/ObjectNormalizerTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy;
4141
use Symfony\Component\Serializer\Tests\Fixtures\Php74DummyPrivate;
4242
use Symfony\Component\Serializer\Tests\Fixtures\Php80Dummy;
43+
use Symfony\Component\Serializer\Tests\Fixtures\SamePropertyAsMethodDummy;
44+
use Symfony\Component\Serializer\Tests\Fixtures\SamePropertyAsMethodWithMethodSerializedNameDummy;
45+
use Symfony\Component\Serializer\Tests\Fixtures\SamePropertyAsMethodWithPropertySerializedNameDummy;
4346
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
4447
use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait;
4548
use Symfony\Component\Serializer\Tests\Normalizer\Features\CacheableObjectAttributesTestTrait;
@@ -869,6 +872,53 @@ public function testNormalizeStdClass()
869872

870873
$this->assertSame(['baz' => 'baz'], $this->normalizer->normalize($o2));
871874
}
875+
876+
public function testSamePropertyAsMethod()
877+
{
878+
$object = new SamePropertyAsMethodDummy('free_trial', 'has_subscribe', 'get_ready', 'is_active');
879+
$expected = [
880+
'freeTrial' => 'free_trial',
881+
'hasSubscribe' => 'has_subscribe',
882+
'getReady' => 'get_ready',
883+
'isActive' => 'is_active',
884+
];
885+
886+
$this->assertSame($expected, $this->normalizer->normalize($object));
887+
}
888+
889+
public function testSamePropertyAsMethodWithPropertySerializedName()
890+
{
891+
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
892+
$this->normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
893+
$this->normalizer->setSerializer($this->serializer);
894+
895+
$object = new SamePropertyAsMethodWithPropertySerializedNameDummy('free_trial', 'has_subscribe', 'get_ready', 'is_active');
896+
$expected = [
897+
'free_trial_property' => 'free_trial',
898+
'has_subscribe_property' => 'has_subscribe',
899+
'get_ready_property' => 'get_ready',
900+
'is_active_property' => 'is_active',
901+
];
902+
903+
$this->assertSame($expected, $this->normalizer->normalize($object));
904+
}
905+
906+
public function testSamePropertyAsMethodWithMethodSerializedName()
907+
{
908+
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
909+
$this->normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
910+
$this->normalizer->setSerializer($this->serializer);
911+
912+
$object = new SamePropertyAsMethodWithMethodSerializedNameDummy('free_trial', 'has_subscribe', 'get_ready', 'is_active');
913+
$expected = [
914+
'free_trial_method' => 'free_trial',
915+
'has_subscribe_method' => 'has_subscribe',
916+
'get_ready_method' => 'get_ready',
917+
'is_active_method' => 'is_active',
918+
];
919+
920+
$this->assertSame($expected, $this->normalizer->normalize($object));
921+
}
872922
}
873923

874924
class ProxyObjectDummy extends ObjectDummy

0 commit comments

Comments
 (0)