diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index ddb46716..1284e91e 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -124,7 +124,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool } $accessorOrMutator = preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches); - if ($accessorOrMutator) { + if ($accessorOrMutator && !ctype_lower($matches[2][0])) { $attributeName = lcfirst($matches[2]); if (isset($attributesMetadata[$attributeName])) { diff --git a/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index 3a398ef3..ec3d7cb1 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -87,8 +87,8 @@ private function isGetMethod(\ReflectionMethod $method): bool return !$method->isStatic() && !($method->getAttributes(Ignore::class) || $method->getAttributes(LegacyIgnore::class)) && !$method->getNumberOfRequiredParameters() - && ((2 < ($methodLength = \strlen($method->name)) && str_starts_with($method->name, 'is')) - || (3 < $methodLength && (str_starts_with($method->name, 'has') || str_starts_with($method->name, 'get'))) + && ((2 < ($methodLength = \strlen($method->name)) && str_starts_with($method->name, 'is') && !ctype_lower($method->name[2])) + || (3 < $methodLength && (str_starts_with($method->name, 'has') || str_starts_with($method->name, 'get')) && !ctype_lower($method->name[3])) ); } @@ -100,7 +100,9 @@ private function isSetMethod(\ReflectionMethod $method): bool return !$method->isStatic() && !$method->getAttributes(Ignore::class) && 0 < $method->getNumberOfParameters() - && str_starts_with($method->name, 'set'); + && str_starts_with($method->name, 'set') + && !ctype_lower($method->name[3]) + ; } protected function extractAttributes(object $object, ?string $format = null, array $context = []): array diff --git a/Normalizer/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index 95d79fa3..4466e742 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -88,7 +88,8 @@ protected function extractAttributes(object $object, ?string $format = null, arr $name = $reflMethod->name; $attributeName = null; - if (3 < \strlen($name) && match ($name[0]) { + // ctype_lower check to find out if method looks like accessor but actually is not, e.g. hash, cancel + if (3 < \strlen($name) && !ctype_lower($name[3]) && match ($name[0]) { 'g' => str_starts_with($name, 'get'), 'h' => str_starts_with($name, 'has'), 'c' => str_starts_with($name, 'can'), @@ -100,7 +101,7 @@ protected function extractAttributes(object $object, ?string $format = null, arr if (!$reflClass->hasProperty($attributeName)) { $attributeName = lcfirst($attributeName); } - } elseif ('is' !== $name && str_starts_with($name, 'is')) { + } elseif ('is' !== $name && str_starts_with($name, 'is') && !ctype_lower($name[2])) { // issers $attributeName = substr($name, 2); diff --git a/Tests/Fixtures/Attributes/AccessorishGetters.php b/Tests/Fixtures/Attributes/AccessorishGetters.php new file mode 100644 index 00000000..f434e84f --- /dev/null +++ b/Tests/Fixtures/Attributes/AccessorishGetters.php @@ -0,0 +1,39 @@ + + * + * 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\Attributes; + +class AccessorishGetters +{ + public function hash(): void + { + } + + public function cancel() + { + } + + public function getField1() + { + } + + public function isField2() + { + } + + public function hasField3() + { + } + + public function setField4() + { + } +} diff --git a/Tests/Mapping/Loader/AttributeLoaderTest.php b/Tests/Mapping/Loader/AttributeLoaderTest.php index a7659550..5a078bf3 100644 --- a/Tests/Mapping/Loader/AttributeLoaderTest.php +++ b/Tests/Mapping/Loader/AttributeLoaderTest.php @@ -23,6 +23,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyThirdChild; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AccessorishGetters; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\BadAttributeDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\BadMethodContextDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\ContextDummyParent; @@ -229,6 +230,22 @@ public function testLoadWithInvalidAttribute() $this->loader->loadClassMetadata($classMetadata); } + public function testIgnoresAccessorishGetters() + { + $classMetadata = new ClassMetadata(AccessorishGetters::class); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + + self::assertCount(4, $classMetadata->getAttributesMetadata()); + + self::assertArrayHasKey('field1', $attributesMetadata); + self::assertArrayHasKey('field2', $attributesMetadata); + self::assertArrayHasKey('field3', $attributesMetadata); + self::assertArrayHasKey('field4', $attributesMetadata); + self::assertArrayNotHasKey('h', $attributesMetadata); + } + protected function getLoaderForContextMapping(): AttributeLoader { return $this->loader; diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index 2c30a57b..80788a0d 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -521,6 +521,14 @@ public function testNormalizeWithDiscriminator() $this->assertSame(['type' => 'one', 'url' => 'URL_ONE'], $normalizer->normalize(new GetSetMethodDiscriminatedDummyOne())); } + public function testNormalizeWithMethodNamesSimilarToAccessors() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory); + + $this->assertSame(['class' => 'class', 123 => 123], $normalizer->normalize(new GetSetWithAccessorishMethod())); + } + public function testDenormalizeWithDiscriminator() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); @@ -908,3 +916,46 @@ public function setBar($bar = null, $other = true) $this->bar = $bar; } } + +class GetSetWithAccessorishMethod +{ + public function cancel() + { + return 'cancel'; + } + + public function hash() + { + return 'hash'; + } + + public function getClass() + { + return 'class'; + } + + public function setClass() + { + } + + public function get123() + { + return 123; + } + + public function set123() + { + } + + public function gettings() + { + } + + public function settings() + { + } + + public function isolate() + { + } +} diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index ed6a9d3b..600fd7cd 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -961,6 +961,24 @@ public function testObjectNormalizerWithAttributeLoaderAndObjectHasStaticPropert $normalizer = new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader())); $this->assertSame([], $normalizer->normalize($class)); } + + public function testNormalizeWithMethodNamesSimilarToAccessors() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $normalizer = new ObjectNormalizer($classMetadataFactory); + + $object = new ObjectWithAccessorishMethods(); + $normalized = $normalizer->normalize($object); + + $this->assertFalse($object->isAccessorishCalled()); + $this->assertSame([ + 'accessorishCalled' => false, + 'tell' => true, + 'class' => true, + 'responsibility' => true, + 123 => 321 + ], $normalized); + } } class ProxyObjectDummy extends ObjectDummy @@ -1243,3 +1261,63 @@ class ObjectDummyWithIgnoreAttributeAndPrivateProperty private $private = 'private'; } + +class ObjectWithAccessorishMethods +{ + private $accessorishCalled = false; + + public function isAccessorishCalled() + { + return $this->accessorishCalled; + } + + public function cancel() + { + $this->accessorishCalled = true; + } + + public function hash() + { + $this->accessorishCalled = true; + } + + public function canTell() + { + return true; + } + + public function getClass() + { + return true; + } + + public function hasResponsibility() + { + return true; + } + + public function get_foo() + { + return 'bar'; + } + + public function get123() + { + return 321; + } + + public function gettings() + { + $this->accessorishCalled = true; + } + + public function settings() + { + $this->accessorishCalled = true; + } + + public function isolate() + { + $this->accessorishCalled = true; + } +}