Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[Serializer] fix Inherited properties normalization
  • Loading branch information
Link1515 authored and nicolas-grekas committed Nov 12, 2025
commit 45e43139b2dfa9d00701fc775fe4b64aeac7cfbf
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ protected function extractAttributes(object $object, ?string $format = null, arr
'i' => str_starts_with($name, 'is') && isset($name[$i = 2]),
default => false,
} && !ctype_lower($name[$i])) {
if ($reflClass->hasProperty($name)) {
if ($this->hasProperty($reflMethod, $name)) {
$attributeName = $name;
} else {
$attributeName = substr($name, $i);
Expand Down Expand Up @@ -139,6 +139,19 @@ protected function extractAttributes(object $object, ?string $format = null, arr
return array_keys($attributes);
}

private function hasProperty(\ReflectionMethod $method, string $propName): bool
{
$class = $method->getDeclaringClass();

do {
if ($class->hasProperty($propName)) {
return true;
}
} while ($class = $class->getParentClass());

return false;
}

protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
{
$mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,7 @@ public function testNormalizeObjectWithPropertyAndAccessorMethodsWithSameName()
$this->assertSame('getFoo', $denormalized->getFoo());

// On the initial object the value was 'foo', but the normalizer prefers the accessor method 'getFoo'
// Thus on the denoramilzed object the value is 'getFoo'
// Thus on the denormalized object the value is 'getFoo'
$this->assertSame('foo', $object->foo);
$this->assertSame('getFoo', $denormalized->foo);

Expand All @@ -1026,6 +1026,56 @@ public function testNormalizeObjectWithPropertyAndAccessorMethodsWithSameName()
$this->assertSame('isFoo', $denormalized->isFoo());
}

public function testNormalizeChildExtendsObjectWithPropertyAndAccessorSameName()
{
// This test follows the same logic used in testNormalizeObjectWithPropertyAndAccessorMethodsWithSameName()
$normalizer = $this->getNormalizerForAccessors();

$object = new ChildExtendsObjectWithPropertyAndAccessorSameName(
'foo',
'getFoo',
'canFoo',
'hasFoo',
'isFoo'
);
$normalized = $normalizer->normalize($object);

$this->assertSame([
'getFoo' => 'getFoo',
'canFoo' => 'canFoo',
'hasFoo' => 'hasFoo',
'isFoo' => 'isFoo',
// The getFoo accessor method is used for foo, thus it's also 'getFoo' instead of 'foo'
'foo' => 'getFoo',
], $normalized);

$denormalized = $this->normalizer->denormalize($normalized, ChildExtendsObjectWithPropertyAndAccessorSameName::class);

$this->assertSame('getFoo', $denormalized->getFoo());

// On the initial object the value was 'foo', but the normalizer prefers the accessor method 'getFoo'
// Thus on the denormalized object the value is 'getFoo'
$this->assertSame('foo', $object->foo);
$this->assertSame('getFoo', $denormalized->foo);

$this->assertSame('hasFoo', $denormalized->hasFoo());
$this->assertSame('canFoo', $denormalized->canFoo());
$this->assertSame('isFoo', $denormalized->isFoo());
}

public function testNormalizeChildWithPropertySameAsParentMethod()
{
$normalizer = $this->getNormalizerForAccessors();

$object = new ChildWithPropertySameAsParentMethod('foo');
$normalized = $normalizer->normalize($object);

$this->assertSame([
'foo' => 'foo',
],
$normalized);
}

/**
* Priority of accessor methods is defined by the PropertyReadInfoExtractorInterface passed to the PropertyAccessor
* component. By default ReflectionExtractor::$defaultAccessorPrefixes are used.
Expand Down Expand Up @@ -1501,6 +1551,18 @@ public function isFoo()
}
}

class ChildExtendsObjectWithPropertyAndAccessorSameName extends ObjectWithPropertyAndAccessorSameName
{
}

class ChildWithPropertySameAsParentMethod extends ObjectWithPropertyAndAllAccessorMethods
{
private $canFoo;
private $getFoo;
private $hasFoo;
private $isFoo;
}

class ObjectWithPropertyHasserAndIsser
{
public function __construct(
Expand Down