Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 86821ff

Browse files
Merge branch '6.4' into 7.3
* 6.4: [Cache] Fix DSN auth not passed to clusters in RedisTrait do not parse "scalar" as an object [Form] Fix OrderedHashMap auto-increment logic with mixed keys don't skip custom view transformers while normalizing submitted newlines [Serializer] Fix is/has/can accessor naming regression while preserving collision detection
2 parents af7b0d1 + 8e02b21 commit 86821ff

4 files changed

Lines changed: 282 additions & 73 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Mapping\Loader;
13+
14+
use Symfony\Component\Serializer\Attribute\Ignore;
15+
16+
/**
17+
* Provides methods to detect accessor name collisions during serialization.
18+
*
19+
* @internal
20+
*/
21+
trait AccessorCollisionResolverTrait
22+
{
23+
private function getAttributeNameFromAccessor(\ReflectionClass $class, \ReflectionMethod $method, bool $andMutator): ?string
24+
{
25+
$methodName = $method->name;
26+
27+
$i = match ($methodName[0]) {
28+
's' => $andMutator && str_starts_with($methodName, 'set') ? 3 : null,
29+
'g' => str_starts_with($methodName, 'get') ? 3 : null,
30+
'h' => str_starts_with($methodName, 'has') ? 3 : null,
31+
'c' => str_starts_with($methodName, 'can') ? 3 : null,
32+
'i' => str_starts_with($methodName, 'is') ? 2 : null,
33+
default => null,
34+
};
35+
36+
// ctype_lower check to find out if method looks like accessor but actually is not, e.g. hash, cancel
37+
if (null === $i || ctype_lower($methodName[$i] ?? 'a') || $method->isStatic()) {
38+
return null;
39+
}
40+
41+
if ('s' === $methodName[0] ? !$method->getNumberOfParameters() : ($method->getNumberOfRequiredParameters() || \in_array((string) $method->getReturnType(), ['void', 'never'], true))) {
42+
return null;
43+
}
44+
45+
$attributeName = substr($methodName, $i);
46+
47+
if (!$class->hasProperty($attributeName)) {
48+
$attributeName = lcfirst($attributeName);
49+
}
50+
51+
return $attributeName;
52+
}
53+
54+
private function hasPropertyForAccessor(\ReflectionClass $class, string $propName): bool
55+
{
56+
do {
57+
if ($class->hasProperty($propName)) {
58+
return true;
59+
}
60+
} while ($class = $class->getParentClass());
61+
62+
return false;
63+
}
64+
65+
private function hasAttributeNameCollision(\ReflectionClass $class, string $attributeName, string $methodName): bool
66+
{
67+
if ($this->hasPropertyForAccessor($class, $attributeName)) {
68+
return true;
69+
}
70+
71+
if ($class->hasMethod($attributeName)) {
72+
$candidate = $class->getMethod($attributeName);
73+
if ($candidate->getName() !== $methodName && $this->isReadableAccessorMethod($candidate)) {
74+
return true;
75+
}
76+
}
77+
78+
$ucAttributeName = ucfirst($attributeName);
79+
foreach (['get', 'is', 'has', 'can'] as $prefix) {
80+
$candidateName = $prefix.$ucAttributeName;
81+
if ($candidateName === $methodName || !$class->hasMethod($candidateName)) {
82+
continue;
83+
}
84+
85+
if ($this->isReadableAccessorMethod($class->getMethod($candidateName))) {
86+
return true;
87+
}
88+
}
89+
90+
return false;
91+
}
92+
93+
private function isReadableAccessorMethod(\ReflectionMethod $method): bool
94+
{
95+
return $method->isPublic()
96+
&& !$method->isStatic()
97+
&& !$method->getAttributes(Ignore::class)
98+
&& !$method->getNumberOfRequiredParameters()
99+
&& !\in_array((string) $method->getReturnType(), ['void', 'never'], true);
100+
}
101+
}

Mapping/Loader/AttributeLoader.php

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
*/
3434
class AttributeLoader implements LoaderInterface
3535
{
36+
use AccessorCollisionResolverTrait;
37+
3638
private const KNOWN_ATTRIBUTES = [
3739
DiscriminatorMap::class,
3840
Groups::class,
@@ -115,25 +117,13 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
115117
continue; /* matches the BC behavior in `Symfony\Component\Serializer\Normalizer\ObjectNormalizer::extractAttributes` */
116118
}
117119

118-
$accessorOrMutator = match ($name[0]) {
119-
's' => str_starts_with($name, 'set') && isset($name[$i = 3]) && $method->getNumberOfParameters(),
120-
'g' => str_starts_with($name, 'get') && isset($name[$i = 3]),
121-
'h' => str_starts_with($name, 'has') && isset($name[$i = 3]),
122-
'c' => str_starts_with($name, 'can') && isset($name[$i = 3]),
123-
'i' => str_starts_with($name, 'is') && isset($name[$i = 2]),
124-
default => false,
125-
} && ('s' === $name[0] || !$method->getNumberOfRequiredParameters() && !\in_array((string) $method->getReturnType(), ['void', 'never'], true));
126-
127-
$hasProperty = $this->hasProperty($method->getDeclaringClass(), $name);
128-
if ($hasProperty || $accessorOrMutator && !ctype_lower($name[$i])) {
129-
if ($hasProperty) {
130-
$attributeName = $name;
131-
} else {
132-
$attributeName = substr($name, $i);
120+
$attributeName = $this->getAttributeNameFromAccessor($reflectionClass, $method, true);
121+
$accessorOrMutator = null !== $attributeName;
122+
$hasProperty = $this->hasPropertyForAccessor($method->getDeclaringClass(), $name);
133123

134-
if (!$reflectionClass->hasProperty($attributeName)) {
135-
$attributeName = lcfirst($attributeName);
136-
}
124+
if ($hasProperty || $accessorOrMutator) {
125+
if (null === $attributeName || 's' !== $name[0] && $hasProperty && $this->hasAttributeNameCollision($reflectionClass, $attributeName, $name)) {
126+
$attributeName = $name;
137127
}
138128

139129
if (isset($attributesMetadata[$attributeName])) {
@@ -237,15 +227,4 @@ private function isKnownAttribute(string $attributeName): bool
237227

238228
return false;
239229
}
240-
241-
private function hasProperty(\ReflectionClass $class, string $propName): bool
242-
{
243-
do {
244-
if ($class->hasProperty($propName)) {
245-
return true;
246-
}
247-
} while ($class = $class->getParentClass());
248-
249-
return false;
250-
}
251230
}

Normalizer/ObjectNormalizer.php

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
1919
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
2020
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
21-
use Symfony\Component\Serializer\Annotation\Ignore;
21+
use Symfony\Component\Serializer\Attribute\Ignore;
2222
use Symfony\Component\Serializer\Exception\LogicException;
2323
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
2424
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
25+
use Symfony\Component\Serializer\Mapping\Loader\AccessorCollisionResolverTrait;
2526
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2627

2728
/**
@@ -31,6 +32,8 @@
3132
*/
3233
final class ObjectNormalizer extends AbstractObjectNormalizer
3334
{
35+
use AccessorCollisionResolverTrait;
36+
3437
private static $reflectionCache = [];
3538
private static $isReadableCache = [];
3639
private static $isWritableCache = [];
@@ -75,37 +78,10 @@ protected function extractAttributes(object $object, ?string $format = null, arr
7578
$reflClass = new \ReflectionClass($class);
7679

7780
foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) {
78-
if (
79-
$reflMethod->getNumberOfRequiredParameters()
80-
|| $reflMethod->isStatic()
81-
|| $reflMethod->isConstructor()
82-
|| $reflMethod->isDestructor()
83-
|| \in_array((string) $reflMethod->getReturnType(), ['void', 'never'], true)
84-
) {
85-
continue;
86-
}
87-
8881
$name = $reflMethod->name;
89-
$attributeName = null;
90-
91-
// ctype_lower check to find out if method looks like accessor but actually is not, e.g. hash, cancel
92-
if (match ($name[0]) {
93-
'g' => str_starts_with($name, 'get') && isset($name[$i = 3]),
94-
'h' => str_starts_with($name, 'has') && isset($name[$i = 3]),
95-
'c' => str_starts_with($name, 'can') && isset($name[$i = 3]),
96-
'i' => str_starts_with($name, 'is') && isset($name[$i = 2]),
97-
default => false,
98-
} && !ctype_lower($name[$i])) {
99-
if ($this->hasProperty($reflMethod->getDeclaringClass(), $name)) {
100-
$attributeName = $name;
101-
} else {
102-
$attributeName = substr($name, $i);
103-
104-
if (!$reflClass->hasProperty($attributeName)) {
105-
$attributeName = lcfirst($attributeName);
106-
}
107-
}
108-
} elseif ($this->hasProperty($reflMethod->getDeclaringClass(), $name)) {
82+
$attributeName = $this->getAttributeNameFromAccessor($reflClass, $reflMethod, false);
83+
84+
if ($this->hasPropertyForAccessor($reflMethod->getDeclaringClass(), $name) && (null === $attributeName || $this->hasAttributeNameCollision($reflClass, $attributeName, $name))) {
10985
$attributeName = $name;
11086
}
11187

@@ -130,17 +106,6 @@ protected function extractAttributes(object $object, ?string $format = null, arr
130106
return array_keys($attributes);
131107
}
132108

133-
private function hasProperty(\ReflectionClass $class, string $propName): bool
134-
{
135-
do {
136-
if ($class->hasProperty($propName)) {
137-
return true;
138-
}
139-
} while ($class = $class->getParentClass());
140-
141-
return false;
142-
}
143-
144109
protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
145110
{
146111
$mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object);

0 commit comments

Comments
 (0)