From 714bbe96c195d07757a6a33bbe558d3fefb6fd0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4dlich?= Date: Sat, 25 Nov 2023 10:37:56 +0100 Subject: [PATCH 001/104] [Serializer] Consider SerializedPath in debug command output --- Command/DebugCommand.php | 1 + Tests/Command/DebugCommandTest.php | 2 ++ Tests/Dummy/DummyClassOne.php | 2 ++ 3 files changed, 5 insertions(+) diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index 13873dd1f..c85ee213e 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -102,6 +102,7 @@ private function getAttributesData(ClassMetadataInterface $classMetadata): array 'groups' => $attributeMetadata->getGroups(), 'maxDepth' => $attributeMetadata->getMaxDepth(), 'serializedName' => $attributeMetadata->getSerializedName(), + 'serializedPath' => $attributeMetadata->getSerializedPath() ? (string) $attributeMetadata->getSerializedPath() : null, 'ignore' => $attributeMetadata->isIgnored(), 'normalizationContexts' => $attributeMetadata->getNormalizationContexts(), 'denormalizationContexts' => $attributeMetadata->getDenormalizationContexts(), diff --git a/Tests/Command/DebugCommandTest.php b/Tests/Command/DebugCommandTest.php index 879231160..5b4f73c17 100644 --- a/Tests/Command/DebugCommandTest.php +++ b/Tests/Command/DebugCommandTest.php @@ -46,6 +46,7 @@ public function testOutputWithClassArgument() | | ], | | | "maxDepth" => 1, | | | "serializedName" => "identifier", | + | | "serializedPath" => null, | | | "ignore" => true, | | | "normalizationContexts" => [ | | | "*" => [ | @@ -66,6 +67,7 @@ public function testOutputWithClassArgument() | | "groups" => [], | | | "maxDepth" => null, | | | "serializedName" => null, | + | | "serializedPath" => [data][name], | | | "ignore" => false, | | | "normalizationContexts" => [], | | | "denormalizationContexts" => [] | diff --git a/Tests/Dummy/DummyClassOne.php b/Tests/Dummy/DummyClassOne.php index 2b3c94cb8..fc78db51c 100644 --- a/Tests/Dummy/DummyClassOne.php +++ b/Tests/Dummy/DummyClassOne.php @@ -16,6 +16,7 @@ use Symfony\Component\Serializer\Attribute\Ignore; use Symfony\Component\Serializer\Attribute\MaxDepth; use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Component\Serializer\Attribute\SerializedPath; class DummyClassOne { @@ -29,5 +30,6 @@ class DummyClassOne )] public string $code; + #[SerializedPath('[data][name]')] public string $name; } From 1a0e5c16759bd2917f6d4a8e74059e49bce1bbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4dlich?= Date: Sun, 26 Nov 2023 20:01:11 +0100 Subject: [PATCH 002/104] Fix DebugCommandTest --- Tests/Command/DebugCommandTest.php | 74 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/Tests/Command/DebugCommandTest.php b/Tests/Command/DebugCommandTest.php index 5b4f73c17..7bfdf93dd 100644 --- a/Tests/Command/DebugCommandTest.php +++ b/Tests/Command/DebugCommandTest.php @@ -36,43 +36,43 @@ public function testOutputWithClassArgument() Symfony\Component\Serializer\Tests\Dummy\DummyClassOne ------------------------------------------------------ - +----------+-------------------------------------+ - | Property | Options | - +----------+-------------------------------------+ - | code | [ | - | | "groups" => [ | - | | "book:read", | - | | "book:write" | - | | ], | - | | "maxDepth" => 1, | - | | "serializedName" => "identifier", | - | | "serializedPath" => null, | - | | "ignore" => true, | - | | "normalizationContexts" => [ | - | | "*" => [ | - | | "groups" => [ | - | | "book:read" | - | | ] | - | | ] | - | | ], | - | | "denormalizationContexts" => [ | - | | "*" => [ | - | | "groups" => [ | - | | "book:write" | - | | ] | - | | ] | - | | ] | - | | ] | - | name | [ | - | | "groups" => [], | - | | "maxDepth" => null, | - | | "serializedName" => null, | - | | "serializedPath" => [data][name], | - | | "ignore" => false, | - | | "normalizationContexts" => [], | - | | "denormalizationContexts" => [] | - | | ] | - +----------+-------------------------------------+ + +----------+---------------------------------------+ + | Property | Options | + +----------+---------------------------------------+ + | code | [ | + | | "groups" => [ | + | | "book:read", | + | | "book:write" | + | | ], | + | | "maxDepth" => 1, | + | | "serializedName" => "identifier", | + | | "serializedPath" => null, | + | | "ignore" => true, | + | | "normalizationContexts" => [ | + | | "*" => [ | + | | "groups" => [ | + | | "book:read" | + | | ] | + | | ] | + | | ], | + | | "denormalizationContexts" => [ | + | | "*" => [ | + | | "groups" => [ | + | | "book:write" | + | | ] | + | | ] | + | | ] | + | | ] | + | name | [ | + | | "groups" => [], | + | | "maxDepth" => null, | + | | "serializedName" => null, | + | | "serializedPath" => "[data][name]", | + | | "ignore" => false, | + | | "normalizationContexts" => [], | + | | "denormalizationContexts" => [] | + | | ] | + +----------+---------------------------------------+ TXT, $tester->getDisplay(true), From 467ed3ffdfc6c6fa6ad857a0bb61b705eccf1d96 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Mon, 20 Nov 2023 19:32:45 +0100 Subject: [PATCH 003/104] [CssSelector][Serializer][Translation] [Command] Clean unused code --- Encoder/XmlEncoder.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Encoder/XmlEncoder.php b/Encoder/XmlEncoder.php index 24d786e38..a3809bc84 100644 --- a/Encoder/XmlEncoder.php +++ b/Encoder/XmlEncoder.php @@ -200,13 +200,9 @@ final protected function appendCData(\DOMNode $node, string $val): bool final protected function appendDocumentFragment(\DOMNode $node, \DOMDocumentFragment $fragment): bool { - if ($fragment instanceof \DOMDocumentFragment) { - $node->appendChild($fragment); + $node->appendChild($fragment); - return true; - } - - return false; + return true; } final protected function appendComment(\DOMNode $node, string $data): bool From d61282e18457f007b79d9aad202253478992714b Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Mon, 4 Dec 2023 02:41:30 +0300 Subject: [PATCH 004/104] [PropertyAccess][Serializer] Fix "type unknown" on denormalize --- Normalizer/AbstractObjectNormalizer.php | 3 ++- Tests/Normalizer/ObjectNormalizerTest.php | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 487cd4bda..1361115e2 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException as PropertyAccessInvalidArgumentException; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -374,7 +375,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $exception = NotNormalizableValueException::createForUnexpectedDataType( sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $data, - ['unknown'], + $e instanceof InvalidTypeException ? [$e->expectedType] : ['unknown'], $context['deserialization_path'] ?? null, false, $e->getCode(), diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index fa9f5c396..39ab6c4cf 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -14,11 +14,13 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -835,6 +837,24 @@ public function testNormalizeStdClass() $this->assertSame(['baz' => 'baz'], $this->normalizer->normalize($o2)); } + + public function testNotNormalizableValueInvalidType() + { + if (!class_exists(InvalidTypeException::class)) { + $this->markTestSkipped('Skipping test as the improvements on PropertyAccess are required.'); + } + + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Expected argument of type "string", "array" given at property path "initialized"'); + + try { + $this->normalizer->denormalize(['initialized' => ['not a string']], TypedPropertiesObject::class, 'array'); + } catch (NotNormalizableValueException $e) { + $this->assertSame(['string'], $e->getExpectedTypes()); + + throw $e; + } + } } class ProxyObjectDummy extends ObjectDummy From d42b6d015a741ecb5bbcb9c2ca6f6002f7e24452 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 14 Dec 2023 11:03:37 +0100 Subject: [PATCH 005/104] Set `strict` parameter of `in_array` to true where possible --- Mapping/AttributeMetadata.php | 2 +- NameConverter/CamelCaseToSnakeCaseNameConverter.php | 4 ++-- Normalizer/AbstractNormalizer.php | 4 ++-- Normalizer/AbstractObjectNormalizer.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Mapping/AttributeMetadata.php b/Mapping/AttributeMetadata.php index 9b04bb7e3..647d59309 100644 --- a/Mapping/AttributeMetadata.php +++ b/Mapping/AttributeMetadata.php @@ -90,7 +90,7 @@ public function getName(): string public function addGroup(string $group): void { - if (!\in_array($group, $this->groups)) { + if (!\in_array($group, $this->groups, true)) { $this->groups[] = $group; } } diff --git a/NameConverter/CamelCaseToSnakeCaseNameConverter.php b/NameConverter/CamelCaseToSnakeCaseNameConverter.php index ab6f99e13..a7b450fd2 100644 --- a/NameConverter/CamelCaseToSnakeCaseNameConverter.php +++ b/NameConverter/CamelCaseToSnakeCaseNameConverter.php @@ -30,7 +30,7 @@ public function __construct( public function normalize(string $propertyName): string { - if (null === $this->attributes || \in_array($propertyName, $this->attributes)) { + if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) { return strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); } @@ -45,7 +45,7 @@ public function denormalize(string $propertyName): string $camelCasedName = lcfirst($camelCasedName); } - if (null === $this->attributes || \in_array($camelCasedName, $this->attributes)) { + if (null === $this->attributes || \in_array($camelCasedName, $this->attributes, true)) { return $camelCasedName; } diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index f53d4b139..40945fd2d 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -253,7 +253,7 @@ protected function getGroups(array $context): array protected function isAllowedAttribute(object|string $classOrObject, string $attribute, string $format = null, array $context = []): bool { $ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES]; - if (\in_array($attribute, $ignoredAttributes)) { + if (\in_array($attribute, $ignoredAttributes, true)) { return false; } @@ -326,7 +326,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex $attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context); $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName; - $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes); + $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes, true); $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context); if ($constructorParameter->isVariadic()) { if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 7868ec10d..f27161cf1 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -334,7 +334,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context); - if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) { + if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes, true)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) { if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) { $extraAttributes[] = $attribute; } From d2d2cc7f2e56c928864a843c1dae3159511e6ad4 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Mon, 18 Dec 2023 08:46:12 +0100 Subject: [PATCH 006/104] Code updates --- Normalizer/DataUriNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Normalizer/DataUriNormalizer.php b/Normalizer/DataUriNormalizer.php index c1aa9695b..3e0a61241 100644 --- a/Normalizer/DataUriNormalizer.php +++ b/Normalizer/DataUriNormalizer.php @@ -89,7 +89,7 @@ public function supportsNormalization(mixed $data, string $format = null, array */ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): \SplFileInfo { - if (null === $data || !preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) { + if (null === $data || !preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) { throw NotNormalizableValueException::createForUnexpectedDataType('The provided "data:" URI is not valid.', $data, ['string'], $context['deserialization_path'] ?? null, true); } From 54f1106c09d0a9c3e2bba6b0349225f004b688b0 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 1 Nov 2023 09:14:07 +0100 Subject: [PATCH 007/104] [Tests] Streamline --- Tests/Annotation/SerializedNameTest.php | 4 +- .../SerializerPassTest.php | 12 ++-- Tests/Encoder/JsonDecodeTest.php | 4 +- Tests/Encoder/JsonEncoderTest.php | 3 +- .../Factory/CacheMetadataFactoryTest.php | 3 +- .../CompiledClassMetadataFactoryTest.php | 6 +- Tests/Mapping/Loader/YamlFileLoaderTest.php | 4 +- .../AbstractObjectNormalizerTest.php | 41 +++++++++----- Tests/Normalizer/DataUriNormalizerTest.php | 4 +- .../Normalizer/GetSetMethodNormalizerTest.php | 9 ++- .../JsonSerializableNormalizerTest.php | 3 +- Tests/Normalizer/ObjectNormalizerTest.php | 10 ++-- Tests/Normalizer/PropertyNormalizerTest.php | 5 +- Tests/SerializerTest.php | 56 ++++++++++++++----- 14 files changed, 109 insertions(+), 55 deletions(-) diff --git a/Tests/Annotation/SerializedNameTest.php b/Tests/Annotation/SerializedNameTest.php index c2b5e5f2a..3a829aecf 100644 --- a/Tests/Annotation/SerializedNameTest.php +++ b/Tests/Annotation/SerializedNameTest.php @@ -30,7 +30,7 @@ public function testNotAStringSerializedNameParameter() public function testSerializedNameParameters() { - $maxDepth = new SerializedName('foo'); - $this->assertEquals('foo', $maxDepth->getSerializedName()); + $foo = new SerializedName('foo'); + $this->assertEquals('foo', $foo->getSerializedName()); } } diff --git a/Tests/DependencyInjection/SerializerPassTest.php b/Tests/DependencyInjection/SerializerPassTest.php index eb77263f4..037eafdb6 100644 --- a/Tests/DependencyInjection/SerializerPassTest.php +++ b/Tests/DependencyInjection/SerializerPassTest.php @@ -28,20 +28,20 @@ class SerializerPassTest extends TestCase { public function testThrowExceptionWhenNoNormalizers() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('You must tag at least one service as "serializer.normalizer" to use the "serializer" service'); $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); $container->register('serializer'); $serializerPass = new SerializerPass(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('You must tag at least one service as "serializer.normalizer" to use the "serializer" service'); + $serializerPass->process($container); } public function testThrowExceptionWhenNoEncoders() { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('You must tag at least one service as "serializer.encoder" to use the "serializer" service'); $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); $container->register('serializer') @@ -50,6 +50,10 @@ public function testThrowExceptionWhenNoEncoders() $container->register('normalizer')->addTag('serializer.normalizer'); $serializerPass = new SerializerPass(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('You must tag at least one service as "serializer.encoder" to use the "serializer" service'); + $serializerPass->process($container); } diff --git a/Tests/Encoder/JsonDecodeTest.php b/Tests/Encoder/JsonDecodeTest.php index 66cd10114..f336bcd42 100644 --- a/Tests/Encoder/JsonDecodeTest.php +++ b/Tests/Encoder/JsonDecodeTest.php @@ -47,11 +47,9 @@ public static function decodeProvider() $stdClass = new \stdClass(); $stdClass->foo = 'bar'; - $assoc = ['foo' => 'bar']; - return [ ['{"foo": "bar"}', $stdClass, []], - ['{"foo": "bar"}', $assoc, ['json_decode_associative' => true]], + ['{"foo": "bar"}', ['foo' => 'bar'], ['json_decode_associative' => true]], ]; } diff --git a/Tests/Encoder/JsonEncoderTest.php b/Tests/Encoder/JsonEncoderTest.php index 1b47684ae..a34e82c6b 100644 --- a/Tests/Encoder/JsonEncoderTest.php +++ b/Tests/Encoder/JsonEncoderTest.php @@ -84,12 +84,13 @@ public function testWithDefaultContext() public function testEncodeNotUtf8WithoutPartialOnError() { - $this->expectException(UnexpectedValueException::class); $arr = [ 'utf8' => 'Hello World!', 'notUtf8' => "\xb0\xd0\xb5\xd0", ]; + $this->expectException(UnexpectedValueException::class); + $this->encoder->encode($arr, 'json'); } diff --git a/Tests/Mapping/Factory/CacheMetadataFactoryTest.php b/Tests/Mapping/Factory/CacheMetadataFactoryTest.php index 9525ca605..6db0b95ae 100644 --- a/Tests/Mapping/Factory/CacheMetadataFactoryTest.php +++ b/Tests/Mapping/Factory/CacheMetadataFactoryTest.php @@ -58,10 +58,11 @@ public function testHasMetadataFor() public function testInvalidClassThrowsException() { - $this->expectException(InvalidArgumentException::class); $decorated = $this->createMock(ClassMetadataFactoryInterface::class); $factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter()); + $this->expectException(InvalidArgumentException::class); + $factory->getMetadataFor('Not\Exist'); } diff --git a/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php b/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php index 683f445df..ff54fb96b 100644 --- a/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php +++ b/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php @@ -34,19 +34,21 @@ public function testItImplementsClassMetadataFactoryInterface() public function testItThrowAnExceptionWhenCacheFileIsNotFound() { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches('#File ".*/Fixtures/not-found-serializer.class.metadata.php" could not be found.#'); - $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/not-found-serializer.class.metadata.php', $classMetadataFactory); } public function testItThrowAnExceptionWhenMetadataIsNotOfTypeArray() { + $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); + $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Compiled metadata must be of the type array, object given.'); - $classMetadataFactory = $this->createMock(ClassMetadataFactoryInterface::class); new CompiledClassMetadataFactory(__DIR__.'/../../Fixtures/object-metadata.php', $classMetadataFactory); } diff --git a/Tests/Mapping/Loader/YamlFileLoaderTest.php b/Tests/Mapping/Loader/YamlFileLoaderTest.php index ea81a9d8a..48e95aecd 100644 --- a/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -65,8 +65,10 @@ public function testLoadClassMetadataReturnsFalseWhenEmpty() public function testLoadClassMetadataReturnsThrowsInvalidMapping() { - $this->expectException(MappingException::class); $loader = new YamlFileLoader(__DIR__.'/../../Fixtures/invalid-mapping.yml'); + + $this->expectException(MappingException::class); + $loader->loadClassMetadata($this->metadata); } diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 0b91fc0db..ca3c7579b 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -82,10 +82,12 @@ public function testInstantiateObjectDenormalizer() public function testDenormalizeWithExtraAttribute() { - $this->expectException(ExtraAttributesException::class); - $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo" is unknown).'); $factory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new AbstractObjectNormalizerDummy($factory); + + $this->expectException(ExtraAttributesException::class); + $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo" is unknown).'); + $normalizer->denormalize( ['fooFoo' => 'foo'], Dummy::class, @@ -96,10 +98,12 @@ public function testDenormalizeWithExtraAttribute() public function testDenormalizeWithExtraAttributes() { - $this->expectException(ExtraAttributesException::class); - $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo", "fooBar" are unknown).'); $factory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new AbstractObjectNormalizerDummy($factory); + + $this->expectException(ExtraAttributesException::class); + $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo", "fooBar" are unknown).'); + $normalizer->denormalize( ['fooFoo' => 'foo', 'fooBar' => 'bar'], Dummy::class, @@ -110,9 +114,11 @@ public function testDenormalizeWithExtraAttributes() public function testDenormalizeWithExtraAttributesAndNoGroupsWithMetadataFactory() { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $this->expectException(ExtraAttributesException::class); $this->expectExceptionMessage('Extra attributes are not allowed ("fooFoo", "fooBar" are unknown).'); - $normalizer = new AbstractObjectNormalizerWithMetadata(); + $normalizer->denormalize( ['fooFoo' => 'foo', 'fooBar' => 'bar', 'bar' => 'bar'], Dummy::class, @@ -134,9 +140,11 @@ public function testDenormalizePlainObject() public function testDenormalizeWithDuplicateNestedAttributes() { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $this->expectException(LogicException::class); $this->expectExceptionMessage('Duplicate serialized path: "one,two,three" used for properties "foo" and "bar".'); - $normalizer = new AbstractObjectNormalizerWithMetadata(); + $normalizer->denormalize([], DuplicateValueNestedDummy::class, 'any'); } @@ -204,8 +212,6 @@ public function testDenormalizeWithNestedAttributes() public function testDenormalizeWithNestedAttributesDuplicateKeys() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Duplicate values for key "quux" found. One value is set via the SerializedPath attribute: "one->four", the other one is set via the SerializedName attribute: "notquux".'); $normalizer = new AbstractObjectNormalizerWithMetadata(); $data = [ 'one' => [ @@ -213,6 +219,10 @@ public function testDenormalizeWithNestedAttributesDuplicateKeys() ], 'quux' => 'notquux', ]; + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Duplicate values for key "quux" found. One value is set via the SerializedPath attribute: "one->four", the other one is set via the SerializedName attribute: "notquux".'); + $normalizer->denormalize($data, DuplicateKeyNestedDummy::class, 'any'); } @@ -265,25 +275,29 @@ public function testDenormalizeWithNestedAttributesInConstructorAndDiscriminator public function testNormalizeWithNestedAttributesMixingArrayTypes() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two]"'); $foobar = new AlreadyPopulatedNestedDummy(); $foobar->foo = 'foo'; $foobar->bar = 'bar'; $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two]"'); + $normalizer->normalize($foobar, 'any'); } public function testNormalizeWithNestedAttributesElementAlreadySet() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two][three]"'); $foobar = new DuplicateValueNestedDummy(); $foobar->foo = 'foo'; $foobar->bar = 'bar'; $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two][three]"'); + $normalizer->normalize($foobar, 'any'); } @@ -691,9 +705,10 @@ private function getDenormalizerForObjectWithBasicProperties() */ public function testExtraAttributesException() { + $normalizer = new ObjectNormalizer(); + $this->expectException(LogicException::class); $this->expectExceptionMessage('A class metadata factory must be provided in the constructor when setting "allow_extra_attributes" to false.'); - $normalizer = new ObjectNormalizer(); $normalizer->denormalize([], \stdClass::class, 'xml', [ 'allow_extra_attributes' => false, diff --git a/Tests/Normalizer/DataUriNormalizerTest.php b/Tests/Normalizer/DataUriNormalizerTest.php index 92e173fe0..7e9af4360 100644 --- a/Tests/Normalizer/DataUriNormalizerTest.php +++ b/Tests/Normalizer/DataUriNormalizerTest.php @@ -121,7 +121,7 @@ public function testGiveNotAccessToLocalFiles() /** * @dataProvider invalidUriProvider */ - public function testInvalidData($uri) + public function testInvalidData(?string $uri) { $this->expectException(UnexpectedValueException::class); $this->normalizer->denormalize($uri, 'SplFileObject'); @@ -148,7 +148,7 @@ public static function invalidUriProvider() /** * @dataProvider validUriProvider */ - public function testValidData($uri) + public function testValidData(string $uri) { $this->assertInstanceOf(\SplFileObject::class, $this->normalizer->denormalize($uri, 'SplFileObject')); } diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index 1d471981e..eb2b65306 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -374,8 +374,6 @@ protected function getDenormalizerForIgnoredAttributes(): GetSetMethodNormalizer public function testUnableToNormalizeObjectAttribute() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer'); $serializer = $this->createMock(SerializerInterface::class); $this->normalizer->setSerializer($serializer); @@ -383,6 +381,9 @@ public function testUnableToNormalizeObjectAttribute() $object = new \stdClass(); $obj->setObject($object); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer'); + $this->normalizer->normalize($obj, 'any'); } @@ -391,14 +392,12 @@ public function testSiblingReference() $serializer = new Serializer([$this->normalizer]); $this->normalizer->setSerializer($serializer); - $siblingHolder = new SiblingHolder(); - $expected = [ 'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'], 'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'], 'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'], ]; - $this->assertEquals($expected, $this->normalizer->normalize($siblingHolder)); + $this->assertEquals($expected, $this->normalizer->normalize(new SiblingHolder())); } public function testDenormalizeNonExistingAttribute() diff --git a/Tests/Normalizer/JsonSerializableNormalizerTest.php b/Tests/Normalizer/JsonSerializableNormalizerTest.php index 54a977f55..f8f8546d7 100644 --- a/Tests/Normalizer/JsonSerializableNormalizerTest.php +++ b/Tests/Normalizer/JsonSerializableNormalizerTest.php @@ -68,9 +68,10 @@ public function testNormalize() public function testCircularNormalize() { - $this->expectException(CircularReferenceException::class); $this->createNormalizer([JsonSerializableNormalizer::CIRCULAR_REFERENCE_LIMIT => 1]); + $this->expectException(CircularReferenceException::class); + $this->serializer ->expects($this->once()) ->method('normalize') diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index f9f2e8ad0..05b1891f5 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -307,8 +307,6 @@ public function testConstructorWithUnconstructableNullableObjectTypeHintDenormal public function testConstructorWithUnknownObjectTypeHintDenormalize() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Could not determine the class of the parameter "unknown".'); $data = [ 'id' => 10, 'unknown' => [ @@ -321,6 +319,9 @@ public function testConstructorWithUnknownObjectTypeHintDenormalize() $serializer = new Serializer([$normalizer]); $normalizer->setSerializer($serializer); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Could not determine the class of the parameter "unknown".'); + $normalizer->denormalize($data, DummyWithConstructorInexistingObject::class); } @@ -623,8 +624,6 @@ protected function getDenormalizerForTypeEnforcement(): ObjectNormalizer public function testUnableToNormalizeObjectAttribute() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer'); $serializer = $this->createMock(SerializerInterface::class); $this->normalizer->setSerializer($serializer); @@ -632,6 +631,9 @@ public function testUnableToNormalizeObjectAttribute() $object = new \stdClass(); $obj->setObject($object); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer'); + $this->normalizer->normalize($obj, 'any'); } diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index 631111d2a..585c2068e 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -400,8 +400,6 @@ public function testDenormalizeShouldIgnoreStaticProperty() public function testUnableToNormalizeObjectAttribute() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Cannot normalize attribute "bar" because the injected serializer is not a normalizer'); $serializer = $this->createMock(SerializerInterface::class); $this->normalizer->setSerializer($serializer); @@ -409,6 +407,9 @@ public function testUnableToNormalizeObjectAttribute() $object = new \stdClass(); $obj->setBar($object); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot normalize attribute "bar" because the injected serializer is not a normalizer'); + $this->normalizer->normalize($obj, 'any'); } diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 624853107..45d467064 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -97,8 +97,10 @@ public function testItThrowsExceptionOnInvalidEncoder() public function testNormalizeNoMatch() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([$this->createMock(NormalizerInterface::class)]); + + $this->expectException(UnexpectedValueException::class); + $serializer->normalize(new \stdClass(), 'xml'); } @@ -118,15 +120,19 @@ public function testNormalizeGivesPriorityToInterfaceOverTraversable() public function testNormalizeOnDenormalizer() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([new TestDenormalizer()], []); + + $this->expectException(UnexpectedValueException::class); + $this->assertTrue($serializer->normalize(new \stdClass(), 'json')); } public function testDenormalizeNoMatch() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([$this->createMock(NormalizerInterface::class)]); + + $this->expectException(UnexpectedValueException::class); + $serializer->denormalize('foo', 'stdClass'); } @@ -140,9 +146,11 @@ public function testDenormalizeOnObjectThatOnlySupportsDenormalization() public function testDenormalizeOnNormalizer() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([new TestNormalizer()], []); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(UnexpectedValueException::class); + $this->assertTrue($serializer->denormalize(json_encode($data), 'stdClass', 'json')); } @@ -237,17 +245,21 @@ public function testSerializeEmpty() public function testSerializeNoEncoder() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([], []); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(UnexpectedValueException::class); + $serializer->serialize($data, 'json'); } public function testSerializeNoNormalizer() { - $this->expectException(LogicException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(LogicException::class); + $serializer->serialize(Model::fromArray($data), 'json'); } @@ -271,25 +283,31 @@ public function testDeserializeUseCache() public function testDeserializeNoNormalizer() { - $this->expectException(LogicException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(LogicException::class); + $serializer->deserialize(json_encode($data), Model::class, 'json'); } public function testDeserializeWrongNormalizer() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([new CustomNormalizer()], ['json' => new JsonEncoder()]); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(UnexpectedValueException::class); + $serializer->deserialize(json_encode($data), Model::class, 'json'); } public function testDeserializeNoEncoder() { - $this->expectException(UnexpectedValueException::class); $serializer = new Serializer([], []); $data = ['title' => 'foo', 'numbers' => [5, 3]]; + + $this->expectException(UnexpectedValueException::class); + $serializer->deserialize(json_encode($data), Model::class, 'json'); } @@ -689,29 +707,37 @@ public function testDeserializeScalar() public function testDeserializeLegacyScalarType() { - $this->expectException(LogicException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); + + $this->expectException(LogicException::class); + $serializer->deserialize('42', 'integer', 'json'); } public function testDeserializeScalarTypeToCustomType() { - $this->expectException(LogicException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); + + $this->expectException(LogicException::class); + $serializer->deserialize('"something"', Foo::class, 'json'); } public function testDeserializeNonscalarTypeToScalar() { - $this->expectException(NotNormalizableValueException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); + + $this->expectException(NotNormalizableValueException::class); + $serializer->deserialize('{"foo":true}', 'string', 'json'); } public function testDeserializeInconsistentScalarType() { - $this->expectException(NotNormalizableValueException::class); $serializer = new Serializer([], ['json' => new JsonEncoder()]); + + $this->expectException(NotNormalizableValueException::class); + $serializer->deserialize('"42"', 'int', 'json'); } @@ -727,8 +753,10 @@ public function testDeserializeScalarArray() public function testDeserializeInconsistentScalarArray() { - $this->expectException(NotNormalizableValueException::class); $serializer = new Serializer([new ArrayDenormalizer()], ['json' => new JsonEncoder()]); + + $this->expectException(NotNormalizableValueException::class); + $serializer->deserialize('["42"]', 'int[]', 'json'); } From baa60530672793e078f8e61ff6822856a8f31a6a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 10 Oct 2023 16:04:32 +0200 Subject: [PATCH 008/104] [Console][EventDispatcher][Security][Serializer][Workflow] Add PHPDoc to attribute classes and properties --- Attribute/Context.php | 5 ++++- Attribute/DiscriminatorMap.php | 6 ++++++ Attribute/Groups.php | 2 +- Attribute/MaxDepth.php | 3 +++ Attribute/SerializedName.php | 3 +++ Attribute/SerializedPath.php | 3 +++ 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Attribute/Context.php b/Attribute/Context.php index 61ff1e79d..5ea2d2eb5 100644 --- a/Attribute/Context.php +++ b/Attribute/Context.php @@ -22,7 +22,10 @@ class Context private array $groups; /** - * @param string|string[] $groups + * @param array $context The common context to use when serializing or deserializing + * @param array $normalizationContext The context to use when serializing + * @param array $denormalizationContext The context to use when deserializing + * @param string|string[] $groups The groups to use when serializing or deserializing * * @throws InvalidArgumentException */ diff --git a/Attribute/DiscriminatorMap.php b/Attribute/DiscriminatorMap.php index 31b9eee7e..33a27db85 100644 --- a/Attribute/DiscriminatorMap.php +++ b/Attribute/DiscriminatorMap.php @@ -19,6 +19,12 @@ #[\Attribute(\Attribute::TARGET_CLASS)] class DiscriminatorMap { + /** + * @param string $typeProperty The property holding the type discriminator + * @param array $mapping The mapping between types and classes (i.e. ['admin_user' => AdminUser::class]) + * + * @throws InvalidArgumentException + */ public function __construct( private readonly string $typeProperty, private readonly array $mapping, diff --git a/Attribute/Groups.php b/Attribute/Groups.php index 1c9c9d025..39914f971 100644 --- a/Attribute/Groups.php +++ b/Attribute/Groups.php @@ -25,7 +25,7 @@ class Groups private readonly array $groups; /** - * @param string|string[] $groups + * @param string|string[] $groups The groups to define on the attribute target */ public function __construct(string|array $groups) { diff --git a/Attribute/MaxDepth.php b/Attribute/MaxDepth.php index f132c87a7..17562b6c2 100644 --- a/Attribute/MaxDepth.php +++ b/Attribute/MaxDepth.php @@ -19,6 +19,9 @@ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] class MaxDepth { + /** + * @param int $maxDepth The maximum serialization depth + */ public function __construct(private readonly int $maxDepth) { if ($maxDepth <= 0) { diff --git a/Attribute/SerializedName.php b/Attribute/SerializedName.php index 265aea967..6acbebd03 100644 --- a/Attribute/SerializedName.php +++ b/Attribute/SerializedName.php @@ -19,6 +19,9 @@ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] class SerializedName { + /** + * @param string $serializedName The name of the property as it will be serialized + */ public function __construct(private readonly string $serializedName) { if ('' === $serializedName) { diff --git a/Attribute/SerializedPath.php b/Attribute/SerializedPath.php index 58a35b6db..118e3adbe 100644 --- a/Attribute/SerializedPath.php +++ b/Attribute/SerializedPath.php @@ -23,6 +23,9 @@ class SerializedPath { private PropertyPath $serializedPath; + /** + * @param string $serializedPath A path using a valid PropertyAccess syntax where the value is stored in a normalized representation + */ public function __construct(string $serializedPath) { try { From ad53681596831f465f8985982fd2ee7e37893eb0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 3 Jan 2024 09:08:31 +0100 Subject: [PATCH 009/104] fix merge --- Normalizer/AbstractObjectNormalizer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index bd75e990b..a2503e290 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -375,7 +375,6 @@ public function denormalize(mixed $data, string $type, string $format = null, ar sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $data, $e instanceof InvalidTypeException ? [$e->expectedType] : ['unknown'], - ['unknown'], $attributeContext['deserialization_path'] ?? null, false, $e->getCode(), From fdc4508632129b4d378d8a0743c078d856682535 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 3 Jan 2024 09:14:50 +0100 Subject: [PATCH 010/104] fix expected types --- Tests/SerializerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 60be7de93..85aad3f0b 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -1253,7 +1253,7 @@ public function testCollectDenormalizationErrorsWithoutTypeExtractor() [ 'currentType' => 'array', 'expectedTypes' => [ - 'unknown', + 'string', ], 'path' => 'string', 'useMessageForUser' => false, @@ -1262,7 +1262,7 @@ public function testCollectDenormalizationErrorsWithoutTypeExtractor() [ 'currentType' => 'array', 'expectedTypes' => [ - 'unknown', + 'int', ], 'path' => 'int', 'useMessageForUser' => false, @@ -1271,7 +1271,7 @@ public function testCollectDenormalizationErrorsWithoutTypeExtractor() [ 'currentType' => 'array', 'expectedTypes' => [ - 'unknown', + 'float', ], 'path' => 'float', 'useMessageForUser' => false, From 549f4f499bbca3819949458c71392acc8d2f2970 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 3 Jan 2024 10:29:17 +0100 Subject: [PATCH 011/104] fix tests --- Tests/SerializerTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 85aad3f0b..80f0de439 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; @@ -1253,7 +1254,7 @@ public function testCollectDenormalizationErrorsWithoutTypeExtractor() [ 'currentType' => 'array', 'expectedTypes' => [ - 'string', + class_exists(InvalidTypeException::class) ? 'string' : 'unknown', ], 'path' => 'string', 'useMessageForUser' => false, @@ -1262,7 +1263,7 @@ public function testCollectDenormalizationErrorsWithoutTypeExtractor() [ 'currentType' => 'array', 'expectedTypes' => [ - 'int', + class_exists(InvalidTypeException::class) ? 'int' : 'unknown', ], 'path' => 'int', 'useMessageForUser' => false, @@ -1271,7 +1272,7 @@ public function testCollectDenormalizationErrorsWithoutTypeExtractor() [ 'currentType' => 'array', 'expectedTypes' => [ - 'float', + class_exists(InvalidTypeException::class) ? 'float' : 'unknown', ], 'path' => 'float', 'useMessageForUser' => false, From ca1db1ae38394d9b05d062cb5bdf9aa52e439afa Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Tue, 2 Jan 2024 15:49:33 +0100 Subject: [PATCH 012/104] CS: trailing commas --- Encoder/ChainDecoder.php | 2 +- Encoder/ChainEncoder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Encoder/ChainDecoder.php b/Encoder/ChainDecoder.php index f1b0cd2e2..ad182a9a4 100644 --- a/Encoder/ChainDecoder.php +++ b/Encoder/ChainDecoder.php @@ -33,7 +33,7 @@ class ChainDecoder implements ContextAwareDecoderInterface * @param array $decoders */ public function __construct( - private readonly array $decoders = [] + private readonly array $decoders = [], ) { } diff --git a/Encoder/ChainEncoder.php b/Encoder/ChainEncoder.php index 731cfc601..5445761f3 100644 --- a/Encoder/ChainEncoder.php +++ b/Encoder/ChainEncoder.php @@ -34,7 +34,7 @@ class ChainEncoder implements ContextAwareEncoderInterface * @param array $encoders */ public function __construct( - private readonly array $encoders = [] + private readonly array $encoders = [], ) { } From 51359502748e605e7ab755c8eb800c08d4ab75cc Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 13 Dec 2023 12:51:12 +0200 Subject: [PATCH 013/104] [Serializer] Add `DateTimeNormalizer::CAST_KEY` context option --- CHANGELOG.md | 5 ++ .../DateTimeNormalizerContextBuilder.php | 8 +++ Normalizer/DateTimeNormalizer.php | 10 ++- .../DateTimeNormalizerContextBuilderTest.php | 3 + Tests/Normalizer/DateTimeNormalizerTest.php | 66 +++++++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b329cf154..13de5123f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `DateTimeNormalizer::CAST_KEY` context option + 7.0 --- diff --git a/Context/Normalizer/DateTimeNormalizerContextBuilder.php b/Context/Normalizer/DateTimeNormalizerContextBuilder.php index 99517afb1..e2d289e60 100644 --- a/Context/Normalizer/DateTimeNormalizerContextBuilder.php +++ b/Context/Normalizer/DateTimeNormalizerContextBuilder.php @@ -61,4 +61,12 @@ public function withTimezone(\DateTimeZone|string|null $timezone): static return $this->with(DateTimeNormalizer::TIMEZONE_KEY, $timezone); } + + /** + * @param 'int'|'float'|null $cast + */ + public function withCast(?string $cast): static + { + return $this->with(DateTimeNormalizer::CAST_KEY, $cast); + } } diff --git a/Normalizer/DateTimeNormalizer.php b/Normalizer/DateTimeNormalizer.php index 4e36aa63b..527a104e7 100644 --- a/Normalizer/DateTimeNormalizer.php +++ b/Normalizer/DateTimeNormalizer.php @@ -25,10 +25,12 @@ final class DateTimeNormalizer implements NormalizerInterface, DenormalizerInter { public const FORMAT_KEY = 'datetime_format'; public const TIMEZONE_KEY = 'datetime_timezone'; + public const CAST_KEY = 'datetime_cast'; private array $defaultContext = [ self::FORMAT_KEY => \DateTimeInterface::RFC3339, self::TIMEZONE_KEY => null, + self::CAST_KEY => null, ]; private const SUPPORTED_TYPES = [ @@ -59,7 +61,7 @@ public function getSupportedTypes(?string $format): array /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $object, ?string $format = null, array $context = []): int|float|string { if (!$object instanceof \DateTimeInterface) { throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".'); @@ -73,7 +75,11 @@ public function normalize(mixed $object, ?string $format = null, array $context $object = $object->setTimezone($timezone); } - return $object->format($dateTimeFormat); + return match ($context[self::CAST_KEY] ?? $this->defaultContext[self::CAST_KEY] ?? false) { + 'int' => (int) $object->format($dateTimeFormat), + 'float' => (float) $object->format($dateTimeFormat), + default => $object->format($dateTimeFormat), + }; } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php b/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php index 8ab41f949..ac4badc19 100644 --- a/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php +++ b/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php @@ -38,6 +38,7 @@ public function testWithers(array $values) $context = $this->contextBuilder ->withFormat($values[DateTimeNormalizer::FORMAT_KEY]) ->withTimezone($values[DateTimeNormalizer::TIMEZONE_KEY]) + ->withCast($values[DateTimeNormalizer::CAST_KEY]) ->toArray(); $this->assertEquals($values, $context); @@ -51,11 +52,13 @@ public static function withersDataProvider(): iterable yield 'With values' => [[ DateTimeNormalizer::FORMAT_KEY => 'format', DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('GMT'), + DateTimeNormalizer::CAST_KEY => 'int', ]]; yield 'With null values' => [[ DateTimeNormalizer::FORMAT_KEY => null, DateTimeNormalizer::TIMEZONE_KEY => null, + DateTimeNormalizer::CAST_KEY => null, ]]; } diff --git a/Tests/Normalizer/DateTimeNormalizerTest.php b/Tests/Normalizer/DateTimeNormalizerTest.php index e65b6f67d..5dbf36fbe 100644 --- a/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/Tests/Normalizer/DateTimeNormalizerTest.php @@ -154,6 +154,72 @@ public static function normalizeUsingTimeZonePassedInContextAndExpectedFormatWit ]; } + /** + * @dataProvider provideNormalizeUsingCastCases + */ + public function testNormalizeUsingCastPassedInConstructor(\DateTimeInterface $value, string $format, ?string $cast, string|int|float $expectedResult) + { + $normalizer = new DateTimeNormalizer([DateTimeNormalizer::CAST_KEY => $cast]); + + $this->assertSame($normalizer->normalize($value, null, [DateTimeNormalizer::FORMAT_KEY => $format]), $expectedResult); + } + + /** + * @dataProvider provideNormalizeUsingCastCases + */ + public function testNormalizeUsingCastPassedInContext(\DateTimeInterface $value, string $format, ?string $cast, string|int|float $expectedResult) + { + $this->assertSame($this->normalizer->normalize($value, null, [DateTimeNormalizer::FORMAT_KEY => $format, DateTimeNormalizer::CAST_KEY => $cast]), $expectedResult); + } + + /** + * @return iterable + */ + public static function provideNormalizeUsingCastCases(): iterable + { + yield [ + \DateTimeImmutable::createFromFormat('U', '1703071202'), + 'Y', + null, + '2023', + ]; + + yield [ + \DateTimeImmutable::createFromFormat('U', '1703071202'), + 'Y', + 'int', + 2023, + ]; + + yield [ + \DateTimeImmutable::createFromFormat('U', '1703071202'), + 'Ymd', + 'int', + 20231220, + ]; + + yield [ + \DateTimeImmutable::createFromFormat('U', '1703071202'), + 'Y', + 'int', + 2023, + ]; + + yield [ + \DateTimeImmutable::createFromFormat('U.v', '1703071202.388'), + 'U.v', + 'float', + 1703071202.388, + ]; + + yield [ + \DateTimeImmutable::createFromFormat('U.u', '1703071202.388811'), + 'U.u', + 'float', + 1703071202.388811, + ]; + } + public function testNormalizeInvalidObjectThrowsException() { $this->expectException(InvalidArgumentException::class); From 38b8e259a542e7c56aeea59ef7af38c483f3fadb Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Tue, 29 Aug 2023 16:10:08 +0200 Subject: [PATCH 014/104] [Serializer] Add default groups --- CHANGELOG.md | 1 + NameConverter/MetadataAwareNameConverter.php | 7 +- Normalizer/AbstractNormalizer.php | 12 +++- Tests/Fixtures/Attributes/GroupDummy.php | 24 +++++++ Tests/Mapping/TestClassMetadataFactory.php | 8 +++ Tests/Normalizer/Features/GroupsTestTrait.php | 65 +++++++++++++++++-- .../Normalizer/GetSetMethodNormalizerTest.php | 2 + Tests/Normalizer/ObjectNormalizerTest.php | 2 + Tests/Normalizer/PropertyNormalizerTest.php | 15 ++++- 9 files changed, 123 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13de5123f..a5cb2e777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `DateTimeNormalizer::CAST_KEY` context option + * Add `Default` and "class name" default groups 7.0 --- diff --git a/NameConverter/MetadataAwareNameConverter.php b/NameConverter/MetadataAwareNameConverter.php index 445ad7422..327d92dc1 100644 --- a/NameConverter/MetadataAwareNameConverter.php +++ b/NameConverter/MetadataAwareNameConverter.php @@ -128,13 +128,16 @@ private function getCacheValueForAttributesMetadata(string $class, array $contex } $metadataGroups = $metadata->getGroups(); + $contextGroups = (array) ($context[AbstractNormalizer::GROUPS] ?? []); + $contextGroupsHasBeenDefined = [] !== $contextGroups; + $contextGroups = array_merge($contextGroups, ['Default', (false !== $nsSep = strrpos($class, '\\')) ? substr($class, $nsSep + 1) : $class]); - if ($contextGroups && !$metadataGroups) { + if ($contextGroupsHasBeenDefined && !$metadataGroups) { continue; } - if ($metadataGroups && !array_intersect($metadataGroups, $contextGroups) && !\in_array('*', $contextGroups, true)) { + if ($metadataGroups && !array_intersect(array_merge($metadataGroups, ['*']), $contextGroups)) { continue; } diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index 776b00fc0..8fbc85042 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -213,11 +213,17 @@ protected function getAllowedAttributes(string|object $classOrObject, array $con return false; } + $classMetadata = $this->classMetadataFactory->getMetadataFor($classOrObject); + $class = $classMetadata->getName(); + $groups = $this->getGroups($context); + $groupsHasBeenDefined = [] !== $groups; + $groups = array_merge($groups, ['Default', (false !== $nsSep = strrpos($class, '\\')) ? substr($class, $nsSep + 1) : $class]); $allowedAttributes = []; $ignoreUsed = false; - foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) { + + foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) { if ($ignore = $attributeMetadata->isIgnored()) { $ignoreUsed = true; } @@ -225,14 +231,14 @@ protected function getAllowedAttributes(string|object $classOrObject, array $con // If you update this check, update accordingly the one in Symfony\Component\PropertyInfo\Extractor\SerializerExtractor::getProperties() if ( !$ignore - && ([] === $groups || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups)) + && (!$groupsHasBeenDefined || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups)) && $this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context) ) { $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata; } } - if (!$ignoreUsed && [] === $groups && $allowExtraAttributes) { + if (!$ignoreUsed && !$groupsHasBeenDefined && $allowExtraAttributes) { // Backward Compatibility with the code using this method written before the introduction of @Ignore return false; } diff --git a/Tests/Fixtures/Attributes/GroupDummy.php b/Tests/Fixtures/Attributes/GroupDummy.php index 749e841a5..5c34c95a4 100644 --- a/Tests/Fixtures/Attributes/GroupDummy.php +++ b/Tests/Fixtures/Attributes/GroupDummy.php @@ -27,6 +27,10 @@ class GroupDummy extends GroupDummyParent implements GroupDummyInterface protected $quux; private $fooBar; private $symfony; + #[Groups(['Default'])] + private $default; + #[Groups(['GroupDummy'])] + private $className; #[Groups(['b'])] public function setBar($bar) @@ -80,4 +84,24 @@ public function setQuux($quux): void { $this->quux = $quux; } + + public function setDefault($default) + { + $this->default = $default; + } + + public function getDefault() + { + return $this->default; + } + + public function setClassName($className) + { + $this->className = $className; + } + + public function getClassName() + { + return $this->className; + } } diff --git a/Tests/Mapping/TestClassMetadataFactory.php b/Tests/Mapping/TestClassMetadataFactory.php index 61147316a..d617ffaeb 100644 --- a/Tests/Mapping/TestClassMetadataFactory.php +++ b/Tests/Mapping/TestClassMetadataFactory.php @@ -63,6 +63,14 @@ public static function createClassMetadata(string $namespace, bool $withParent = $symfony->addGroup('name_converter'); } + $default = new AttributeMetadata('default'); + $default->addGroup('Default'); + $expected->addAttributeMetadata($default); + + $className = new AttributeMetadata('className'); + $className->addGroup('GroupDummy'); + $expected->addAttributeMetadata($className); + // load reflection class so that the comparison passes $expected->getReflectionClass(); diff --git a/Tests/Normalizer/Features/GroupsTestTrait.php b/Tests/Normalizer/Features/GroupsTestTrait.php index 08d5e065a..ba4d76323 100644 --- a/Tests/Normalizer/Features/GroupsTestTrait.php +++ b/Tests/Normalizer/Features/GroupsTestTrait.php @@ -31,13 +31,18 @@ public function testGroupsNormalize() $obj = new GroupDummy(); $obj->setFoo('foo'); $obj->setBar('bar'); + $obj->setQuux('quux'); $obj->setFooBar('fooBar'); $obj->setSymfony('symfony'); $obj->setKevin('kevin'); $obj->setCoopTilleuls('coopTilleuls'); + $obj->setDefault('default'); + $obj->setClassName('className'); $this->assertEquals([ 'bar' => 'bar', + 'default' => 'default', + 'className' => 'className', ], $normalizer->normalize($obj, null, ['groups' => ['c']])); $this->assertEquals([ @@ -47,7 +52,26 @@ public function testGroupsNormalize() 'bar' => 'bar', 'kevin' => 'kevin', 'coopTilleuls' => 'coopTilleuls', + 'default' => 'default', + 'className' => 'className', ], $normalizer->normalize($obj, null, ['groups' => ['a', 'c']])); + + $this->assertEquals([ + 'default' => 'default', + 'className' => 'className', + ], $normalizer->normalize($obj, null, ['groups' => ['unknown']])); + + $this->assertEquals([ + 'quux' => 'quux', + 'symfony' => 'symfony', + 'foo' => 'foo', + 'fooBar' => 'fooBar', + 'bar' => 'bar', + 'kevin' => 'kevin', + 'coopTilleuls' => 'coopTilleuls', + 'default' => 'default', + 'className' => 'className', + ], $normalizer->normalize($obj)); } public function testGroupsDenormalize() @@ -55,27 +79,49 @@ public function testGroupsDenormalize() $normalizer = $this->getDenormalizerForGroups(); $obj = new GroupDummy(); - $obj->setFoo('foo'); + $obj->setDefault('default'); + $obj->setClassName('className'); - $data = ['foo' => 'foo', 'bar' => 'bar']; + $data = [ + 'foo' => 'foo', + 'bar' => 'bar', + 'quux' => 'quux', + 'default' => 'default', + 'className' => 'className', + ]; - $normalized = $normalizer->denormalize( + $denormalized = $normalizer->denormalize( + $data, + GroupDummy::class, + null, + ['groups' => ['unknown']] + ); + $this->assertEquals($obj, $denormalized); + + $obj->setFoo('foo'); + + $denormalized = $normalizer->denormalize( $data, GroupDummy::class, null, ['groups' => ['a']] ); - $this->assertEquals($obj, $normalized); + $this->assertEquals($obj, $denormalized); $obj->setBar('bar'); - $normalized = $normalizer->denormalize( + $denormalized = $normalizer->denormalize( $data, GroupDummy::class, null, ['groups' => ['a', 'b']] ); - $this->assertEquals($obj, $normalized); + $this->assertEquals($obj, $denormalized); + + $obj->setQuux('quux'); + + $denormalized = $normalizer->denormalize($data, GroupDummy::class); + $this->assertEquals($obj, $denormalized); } public function testNormalizeNoPropertyInGroup() @@ -84,7 +130,12 @@ public function testNormalizeNoPropertyInGroup() $obj = new GroupDummy(); $obj->setFoo('foo'); + $obj->setDefault('default'); + $obj->setClassName('className'); - $this->assertEquals([], $normalizer->normalize($obj, null, ['groups' => ['notExist']])); + $this->assertEquals([ + 'default' => 'default', + 'className' => 'className', + ], $normalizer->normalize($obj, null, ['groups' => ['notExist']])); } } diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index 8f28999fe..8000dea19 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -295,6 +295,8 @@ public function testGroupsNormalizeWithNameConverter() 'bar' => null, 'foo_bar' => '@dunglas', 'symfony' => '@coopTilleuls', + 'default' => null, + 'class_name' => null, ], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['name_converter']]) ); diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 7350e71bf..fa47995da 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -482,6 +482,8 @@ public function testGroupsNormalizeWithNameConverter() 'bar' => null, 'foo_bar' => '@dunglas', 'symfony' => '@coopTilleuls', + 'default' => null, + 'class_name' => null, ], $this->normalizer->normalize($obj, null, [ObjectNormalizer::GROUPS => ['name_converter']]) ); diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index 7ba3d95eb..04a9afaf8 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -184,7 +184,18 @@ public function testNormalizeWithParentClass() $group->setKevin('Kevin'); $group->setCoopTilleuls('coop'); $this->assertEquals( - ['foo' => 'foo', 'bar' => 'bar', 'quux' => 'quux', 'kevin' => 'Kevin', 'coopTilleuls' => 'coop', 'fooBar' => null, 'symfony' => null, 'baz' => 'baz'], + [ + 'foo' => 'foo', + 'bar' => 'bar', + 'quux' => 'quux', + 'kevin' => 'Kevin', + 'coopTilleuls' => 'coop', + 'fooBar' => null, + 'symfony' => null, + 'baz' => 'baz', + 'default' => null, + 'className' => null, + ], $this->normalizer->normalize($group, 'any') ); } @@ -303,6 +314,8 @@ public function testGroupsNormalizeWithNameConverter() 'bar' => null, 'foo_bar' => '@dunglas', 'symfony' => '@coopTilleuls', + 'default' => null, + 'class_name' => null, ], $this->normalizer->normalize($obj, null, [PropertyNormalizer::GROUPS => ['name_converter']]) ); From a9853ac96f9716fb70c62957f877c09ca3f1b362 Mon Sep 17 00:00:00 2001 From: "hubert.lenoir" Date: Mon, 4 Mar 2024 15:55:09 +0100 Subject: [PATCH 015/104] [HttpKernel] allow boolean argument support for MapQueryString --- CHANGELOG.md | 1 + Normalizer/AbstractNormalizer.php | 34 +++++++++++--- Normalizer/AbstractObjectNormalizer.php | 4 +- .../Normalizer/Features/FilterBoolObject.php | 19 ++++++++ .../Features/FilterBoolTestTrait.php | 47 +++++++++++++++++++ .../Normalizer/GetSetMethodNormalizerTest.php | 7 +++ Tests/Normalizer/ObjectNormalizerTest.php | 7 +++ Tests/Normalizer/PropertyNormalizerTest.php | 7 +++ 8 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 Tests/Normalizer/Features/FilterBoolObject.php create mode 100644 Tests/Normalizer/Features/FilterBoolTestTrait.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a5cb2e777..88a42d4e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add `DateTimeNormalizer::CAST_KEY` context option * Add `Default` and "class name" default groups + * Add `AbstractNormalizer::FILTER_BOOL` context option 7.0 --- diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index bd3171b17..93c367b7d 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -118,6 +118,16 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn */ public const REQUIRE_ALL_PROPERTIES = 'require_all_properties'; + /** + * Flag to control whether a non-boolean value should be filtered using the + * filter_var function with the {@see https://www.php.net/manual/fr/filter.filters.validate.php} + * \FILTER_VALIDATE_BOOL filter before casting it to a boolean. + * + * "0", "false", "off", "no" and "" will be cast to false. + * "1", "true", "on" and "yes" will be cast to true. + */ + public const FILTER_BOOL = 'filter_bool'; + /** * @internal */ @@ -436,12 +446,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex unset($context['has_constructor']); if (!$reflectionClass->isInstantiable()) { - throw NotNormalizableValueException::createForUnexpectedDataType( - sprintf('Failed to create object because the class "%s" is not instantiable.', $class), - $data, - ['unknown'], - $context['deserialization_path'] ?? null - ); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Failed to create object because the class "%s" is not instantiable.', $class), $data, ['unknown'], $context['deserialization_path'] ?? null); } return new $class(); @@ -473,7 +478,9 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara return null; } - return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context); + $parameterData = $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context); + + return $this->applyFilterBool($parameter, $parameterData, $context); } /** @@ -524,6 +531,19 @@ final protected function applyCallbacks(mixed $value, object|string $object, str return $callback ? $callback($value, $object, $attribute, $format, $context) : $value; } + final protected function applyFilterBool(\ReflectionParameter $parameter, mixed $value, array $context): mixed + { + if (!($context[self::FILTER_BOOL] ?? false)) { + return $value; + } + + if (!($parameterType = $parameter->getType()) instanceof \ReflectionNamedType || 'bool' !== $parameterType->getName()) { + return $value; + } + + return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) ?? $value; + } + /** * Computes the normalization context merged with current one. Metadata always wins over global context, as more specific. * diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 4ca328d59..1c2f52fcd 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -601,7 +601,9 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara $parameterData = $this->validateAndDenormalize($types, $class->getName(), $parameterName, $parameterData, $format, $context); - return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context); + $parameterData = $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context); + + return $this->applyFilterBool($parameter, $parameterData, $context); } /** diff --git a/Tests/Normalizer/Features/FilterBoolObject.php b/Tests/Normalizer/Features/FilterBoolObject.php new file mode 100644 index 000000000..2d9828b91 --- /dev/null +++ b/Tests/Normalizer/Features/FilterBoolObject.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Normalizer\Features; + +class FilterBoolObject +{ + public function __construct(public ?bool $value) + { + } +} diff --git a/Tests/Normalizer/Features/FilterBoolTestTrait.php b/Tests/Normalizer/Features/FilterBoolTestTrait.php new file mode 100644 index 000000000..ceb80dc3b --- /dev/null +++ b/Tests/Normalizer/Features/FilterBoolTestTrait.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Normalizer\Features; + +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +/** + * Test AbstractNormalizer::FILTER_BOOL. + */ +trait FilterBoolTestTrait +{ + abstract protected function getNormalizerForFilterBool(): DenormalizerInterface; + + /** + * @dataProvider provideObjectWithBoolArguments + */ + public function testObjectWithBoolArguments(?bool $expectedValue, ?string $parameterValue) + { + $normalizer = $this->getNormalizerForFilterBool(); + + $dummy = $normalizer->denormalize(['value' => $parameterValue], FilterBoolObject::class, context: ['filter_bool' => true]); + + $this->assertSame($expectedValue, $dummy->value); + } + + public static function provideObjectWithBoolArguments() + { + yield 'default value' => [null, null]; + yield '0' => [false, '0']; + yield 'false' => [false, 'false']; + yield 'no' => [false, 'no']; + yield 'off' => [false, 'off']; + yield '1' => [true, '1']; + yield 'true' => [true, 'true']; + yield 'yes' => [true, 'yes']; + yield 'on' => [true, 'on']; + } +} diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index 8000dea19..c5fb73100 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -39,6 +39,7 @@ use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait; +use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait; @@ -53,6 +54,7 @@ class GetSetMethodNormalizerTest extends TestCase use CallbacksTestTrait; use CircularReferenceTestTrait; use ConstructorArgumentsTestTrait; + use FilterBoolTestTrait; use GroupsTestTrait; use IgnoredAttributesTestTrait; use MaxDepthTestTrait; @@ -279,6 +281,11 @@ protected function getDenormalizerForGroups(): GetSetMethodNormalizer return new GetSetMethodNormalizer($classMetadataFactory); } + protected function getNormalizerForFilterBool(): GetSetMethodNormalizer + { + return new GetSetMethodNormalizer(); + } + public function testGroupsNormalizeWithNameConverter() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index fa47995da..9994a27e5 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -50,6 +50,7 @@ use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\ContextMetadataTestTrait; +use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait; @@ -72,6 +73,7 @@ class ObjectNormalizerTest extends TestCase use CircularReferenceTestTrait; use ConstructorArgumentsTestTrait; use ContextMetadataTestTrait; + use FilterBoolTestTrait; use GroupsTestTrait; use IgnoredAttributesTestTrait; use MaxDepthTestTrait; @@ -345,6 +347,11 @@ protected function getDenormalizerForAttributes(): ObjectNormalizer return $normalizer; } + protected function getNormalizerForFilterBool(): ObjectNormalizer + { + return new ObjectNormalizer(); + } + public function testAttributesContextDenormalizeConstructor() { $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index 04a9afaf8..b93a7bb9f 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -38,6 +38,7 @@ use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait; +use Symfony\Component\Serializer\Tests\Normalizer\Features\FilterBoolTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait; @@ -52,6 +53,7 @@ class PropertyNormalizerTest extends TestCase use CallbacksTestTrait; use CircularReferenceTestTrait; use ConstructorArgumentsTestTrait; + use FilterBoolTestTrait; use GroupsTestTrait; use IgnoredAttributesTestTrait; use MaxDepthTestTrait; @@ -259,6 +261,11 @@ protected function getSelfReferencingModel() return new PropertyCircularReferenceDummy(); } + protected function getNormalizerForFilterBool(): PropertyNormalizer + { + return new PropertyNormalizer(); + } + public function testSiblingReference() { $serializer = new Serializer([$this->normalizer]); From ad2dc5abfc3abd2c9b3087cc9c4d93aa271173bb Mon Sep 17 00:00:00 2001 From: Aurelien Pillevesse Date: Sat, 10 Feb 2024 20:13:52 +0100 Subject: [PATCH 016/104] add context to force snake_case --- Exception/UnexpectedPropertyException.php | 29 +++++++++++++++++++ .../CamelCaseToSnakeCaseNameConverter.php | 18 ++++++++++-- .../CamelCaseToSnakeCaseNameConverterTest.php | 18 ++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 Exception/UnexpectedPropertyException.php diff --git a/Exception/UnexpectedPropertyException.php b/Exception/UnexpectedPropertyException.php new file mode 100644 index 000000000..4f9ead9a6 --- /dev/null +++ b/Exception/UnexpectedPropertyException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Exception; + +/** + * UnexpectedPropertyException. + * + * @author Aurélien Pillevesse + */ +class UnexpectedPropertyException extends \UnexpectedValueException implements ExceptionInterface +{ + public function __construct( + public readonly string $property, + ?\Throwable $previous = null, + ) { + $msg = sprintf('Property is not allowed ("%s" is unknown).', $this->property); + + parent::__construct($msg, 0, $previous); + } +} diff --git a/NameConverter/CamelCaseToSnakeCaseNameConverter.php b/NameConverter/CamelCaseToSnakeCaseNameConverter.php index a7b450fd2..8c0b53157 100644 --- a/NameConverter/CamelCaseToSnakeCaseNameConverter.php +++ b/NameConverter/CamelCaseToSnakeCaseNameConverter.php @@ -11,13 +11,21 @@ namespace Symfony\Component\Serializer\NameConverter; +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; + /** * CamelCase to Underscore name converter. * * @author Kévin Dunglas + * @author Aurélien Pillevesse */ -class CamelCaseToSnakeCaseNameConverter implements NameConverterInterface +class CamelCaseToSnakeCaseNameConverter implements AdvancedNameConverterInterface { + /** + * Require all properties to be written in snake_case. + */ + public const REQUIRE_SNAKE_CASE_PROPERTIES = 'require_snake_case_properties'; + /** * @param array|null $attributes The list of attributes to rename or null for all attributes * @param bool $lowerCamelCase Use lowerCamelCase style @@ -28,7 +36,7 @@ public function __construct( ) { } - public function normalize(string $propertyName): string + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) { return strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); @@ -37,8 +45,12 @@ public function normalize(string $propertyName): string return $propertyName; } - public function denormalize(string $propertyName): string + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { + if (($context[self::REQUIRE_SNAKE_CASE_PROPERTIES] ?? false) && $propertyName !== $this->normalize($propertyName, $class, $format, $context)) { + throw new UnexpectedPropertyException($propertyName); + } + $camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]), $propertyName); if ($this->lowerCamelCase) { diff --git a/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php b/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php index e4d419e45..f9d941890 100644 --- a/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php +++ b/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php @@ -12,11 +12,13 @@ namespace Symfony\Component\Serializer\Tests\NameConverter; use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * @author Kévin Dunglas + * @author Aurélien Pillevesse */ class CamelCaseToSnakeCaseNameConverterTest extends TestCase { @@ -55,4 +57,20 @@ public static function attributeProvider() ['this_is_a_test', 'ThisIsATest', false], ]; } + + public function testDenormalizeWithContext() + { + $nameConverter = new CamelCaseToSnakeCaseNameConverter(null, true); + $denormalizedValue = $nameConverter->denormalize('last_name', context: [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES]); + + $this->assertSame($denormalizedValue, 'lastName'); + } + + public function testErrorDenormalizeWithContext() + { + $nameConverter = new CamelCaseToSnakeCaseNameConverter(null, true); + + $this->expectException(UnexpectedPropertyException::class); + $nameConverter->denormalize('lastName', context: [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES => true]); + } } From d594563498f583b928d5047d9f5ad721623e5aff Mon Sep 17 00:00:00 2001 From: Aurelien Pillevesse Date: Sun, 17 Mar 2024 18:46:53 +0100 Subject: [PATCH 017/104] add missing changelog for PR 53898 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a42d4e8..9e495db1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add `DateTimeNormalizer::CAST_KEY` context option * Add `Default` and "class name" default groups * Add `AbstractNormalizer::FILTER_BOOL` context option + * Add `CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES` context option 7.0 --- From 9e3063cd774a7518a5558f8bdab99a37369062b1 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Mon, 18 Mar 2024 20:27:13 +0100 Subject: [PATCH 018/104] chore: CS fixes --- Normalizer/DateTimeNormalizer.php | 8 ++++++-- Tests/Normalizer/AbstractObjectNormalizerTest.php | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Normalizer/DateTimeNormalizer.php b/Normalizer/DateTimeNormalizer.php index 527a104e7..46dff93f7 100644 --- a/Normalizer/DateTimeNormalizer.php +++ b/Normalizer/DateTimeNormalizer.php @@ -94,8 +94,12 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a { if (\is_int($data) || \is_float($data)) { switch ($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY] ?? null) { - case 'U': $data = sprintf('%d', $data); break; - case 'U.u': $data = sprintf('%.6F', $data); break; + case 'U': + $data = sprintf('%d', $data); + break; + case 'U.u': + $data = sprintf('%.6F', $data); + break; } } diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 4ebe11d91..a8e0b6eb1 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -946,12 +946,12 @@ public function testProvidingContextCacheKeyGeneratesSameChildContextCacheKey() $normalizer = new class() extends AbstractObjectNormalizerDummy { public $childContextCacheKey; - protected function extractAttributes(object $object, string $format = null, array $context = []): array + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array { return array_keys((array) $object); } - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { return $object->{$attribute}; } @@ -986,12 +986,12 @@ public function testChildContextKeepsOriginalContextCacheKey() $normalizer = new class() extends AbstractObjectNormalizerDummy { public $childContextCacheKey; - protected function extractAttributes(object $object, string $format = null, array $context = []): array + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array { return array_keys((array) $object); } - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { return $object->{$attribute}; } @@ -1021,12 +1021,12 @@ public function testChildContextCacheKeyStaysFalseWhenOriginalCacheKeyIsFalse() $normalizer = new class() extends AbstractObjectNormalizerDummy { public $childContextCacheKey; - protected function extractAttributes(object $object, string $format = null, array $context = []): array + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array { return array_keys((array) $object); } - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { return $object->{$attribute}; } From bdcae583f759131672372222d9a468c4b810c1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ostroluck=C3=BD?= Date: Sun, 31 Mar 2024 15:15:18 +0200 Subject: [PATCH 019/104] Remove unnecessary empty usages --- Attribute/DiscriminatorMap.php | 4 ++-- Encoder/CsvEncoder.php | 4 ++-- Mapping/Loader/YamlFileLoader.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Attribute/DiscriminatorMap.php b/Attribute/DiscriminatorMap.php index 33a27db85..a77fc2984 100644 --- a/Attribute/DiscriminatorMap.php +++ b/Attribute/DiscriminatorMap.php @@ -29,11 +29,11 @@ public function __construct( private readonly string $typeProperty, private readonly array $mapping, ) { - if (empty($typeProperty)) { + if (!$typeProperty) { throw new InvalidArgumentException(sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class)); } - if (empty($mapping)) { + if (!$mapping) { throw new InvalidArgumentException(sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class)); } } diff --git a/Encoder/CsvEncoder.php b/Encoder/CsvEncoder.php index dc952b2e0..613fad509 100644 --- a/Encoder/CsvEncoder.php +++ b/Encoder/CsvEncoder.php @@ -62,7 +62,7 @@ public function encode(mixed $data, string $format, array $context = []): string if (!is_iterable($data)) { $data = [[$data]]; - } elseif (empty($data)) { + } elseif (!$data) { $data = [[]]; } else { // Sequential arrays of arrays are considered as collections @@ -191,7 +191,7 @@ public function decode(string $data, string $format, array $context = []): mixed return $result; } - if (empty($result) || isset($result[1])) { + if (!$result || isset($result[1])) { return $result; } diff --git a/Mapping/Loader/YamlFileLoader.php b/Mapping/Loader/YamlFileLoader.php index d47ad7430..b0398355a 100644 --- a/Mapping/Loader/YamlFileLoader.php +++ b/Mapping/Loader/YamlFileLoader.php @@ -160,7 +160,7 @@ private function getClassesFromYaml(): array $classes = $this->yamlParser->parseFile($this->file, Yaml::PARSE_CONSTANT); - if (empty($classes)) { + if (!$classes) { return []; } From b8d7dfe6f48ae0cabe4859e9048468bc7e17660e Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Thu, 26 Oct 2023 10:56:04 +0200 Subject: [PATCH 020/104] [PropertyInfo] Deprecate PropertyInfo Type Co-authored-by: Baptiste Leduc --- Exception/NotNormalizableValueException.php | 10 +- Normalizer/AbstractObjectNormalizer.php | 316 +++++++++++++++--- Normalizer/ArrayDenormalizer.php | 32 +- Normalizer/BackedEnumNormalizer.php | 3 +- Normalizer/DateTimeNormalizer.php | 7 +- Normalizer/DateTimeZoneNormalizer.php | 5 +- Normalizer/UidNormalizer.php | 3 +- .../AbstractObjectNormalizerTest.php | 90 +++-- composer.json | 1 + 9 files changed, 373 insertions(+), 94 deletions(-) diff --git a/Exception/NotNormalizableValueException.php b/Exception/NotNormalizableValueException.php index 279be4127..4b956f1b0 100644 --- a/Exception/NotNormalizableValueException.php +++ b/Exception/NotNormalizableValueException.php @@ -22,17 +22,17 @@ class NotNormalizableValueException extends UnexpectedValueException private bool $useMessageForUser = false; /** - * @param string[] $expectedTypes - * @param bool $useMessageForUser If the message passed to this exception is something that can be shown - * safely to your user. In other words, avoid catching other exceptions and - * passing their message directly to this class. + * @param list $expectedTypes + * @param bool $useMessageForUser If the message passed to this exception is something that can be shown + * safely to your user. In other words, avoid catching other exceptions and + * passing their message directly to this class. */ public static function createForUnexpectedDataType(string $message, mixed $data, array $expectedTypes, ?string $path = null, bool $useMessageForUser = false, int $code = 0, ?\Throwable $previous = null): self { $self = new self($message, $code, $previous); $self->currentType = get_debug_type($data); - $self->expectedTypes = $expectedTypes; + $self->expectedTypes = array_map(strval(...), $expectedTypes); $self->path = $path; $self->useMessageForUser = $useMessageForUser; diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 1c2f52fcd..c89b20047 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -17,7 +17,7 @@ use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; @@ -32,6 +32,12 @@ use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Base class for a normalizer dealing with objects. @@ -51,7 +57,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer public const DEPTH_KEY_PATTERN = 'depth_%s::%s'; /** - * While denormalizing, we can verify that types match. + * While denormalizing, we can verify that type matches. * * You can disable this by setting this flag to true. */ @@ -109,7 +115,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer protected ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver; - private array $typesCache = []; + /** + * @var array|false> + */ + private array $typeCache = []; private array $attributesCache = []; private readonly \Closure $objectClassResolver; @@ -290,7 +299,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $this->validateCallbackContext($context); - if (null === $data && isset($context['value_type']) && $context['value_type'] instanceof Type && $context['value_type']->isNullable()) { + if (null === $data && isset($context['value_type']) && ($context['value_type'] instanceof Type || $context['value_type'] instanceof LegacyType) && $context['value_type']->isNullable()) { return null; } @@ -352,11 +361,15 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } } - $types = $this->getTypes($resolvedClass, $attribute); - - if (null !== $types) { + if (null !== $type = $this->getType($resolvedClass, $attribute)) { try { - $value = $this->validateAndDenormalize($types, $resolvedClass, $attribute, $value, $format, $attributeContext); + // BC layer for PropertyTypeExtractorInterface::getTypes(). + // Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + if (\is_array($type)) { + $value = $this->validateAndDenormalizeLegacy($type, $resolvedClass, $attribute, $value, $format, $attributeContext); + } else { + $value = $this->validateAndDenormalize($type, $resolvedClass, $attribute, $value, $format, $attributeContext); + } } catch (NotNormalizableValueException $exception) { if (isset($context['not_normalizable_value_exceptions'])) { $context['not_normalizable_value_exceptions'][] = $exception; @@ -372,7 +385,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $this->setAttributeValue($object, $attribute, $value, $format, $attributeContext); } catch (PropertyAccessInvalidArgumentException $e) { $exception = NotNormalizableValueException::createForUnexpectedDataType( - sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), + sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $resolvedClass), $data, $e instanceof InvalidTypeException ? [$e->expectedType] : ['unknown'], $attributeContext['deserialization_path'] ?? null, @@ -400,14 +413,17 @@ abstract protected function setAttributeValue(object $object, string $attribute, /** * Validates the submitted data and denormalizes it. * - * @param Type[] $types + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + * + * @param LegacyType[] $types * * @throws NotNormalizableValueException * @throws ExtraAttributesException * @throws MissingConstructorArgumentsException * @throws LogicException */ - private function validateAndDenormalize(array $types, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed + private function validateAndDenormalizeLegacy(array $types, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; $isUnionType = \count($types) > 1; @@ -440,11 +456,11 @@ private function validateAndDenormalize(array $types, string $currentClass, stri $builtinType = $type->getBuiltinType(); if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { if ('' === $data) { - if (Type::BUILTIN_TYPE_ARRAY === $builtinType) { + if (LegacyType::BUILTIN_TYPE_ARRAY === $builtinType) { return []; } - if (Type::BUILTIN_TYPE_STRING === $builtinType) { + if (LegacyType::BUILTIN_TYPE_STRING === $builtinType) { return ''; } @@ -453,24 +469,24 @@ private function validateAndDenormalize(array $types, string $currentClass, stri } switch ($builtinType) { - case Type::BUILTIN_TYPE_BOOL: + case LegacyType::BUILTIN_TYPE_BOOL: // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" if ('false' === $data || '0' === $data) { $data = false; } elseif ('true' === $data || '1' === $data) { $data = true; } else { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null); } break; - case Type::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_INT: if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) { $data = (int) $data; } else { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null); } break; - case Type::BUILTIN_TYPE_FLOAT: + case LegacyType::BUILTIN_TYPE_FLOAT: if (is_numeric($data)) { return (float) $data; } @@ -479,13 +495,13 @@ private function validateAndDenormalize(array $types, string $currentClass, stri 'NaN' => \NAN, 'INF' => \INF, '-INF' => -\INF, - default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null), + default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null), }; } } - if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { - $builtinType = Type::BUILTIN_TYPE_OBJECT; + if (null !== $collectionValueType && LegacyType::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { + $builtinType = LegacyType::BUILTIN_TYPE_OBJECT; $class = $collectionValueType->getClassName().'[]'; if (\count($collectionKeyType = $type->getCollectionKeyTypes()) > 0) { @@ -493,13 +509,13 @@ private function validateAndDenormalize(array $types, string $currentClass, stri } $context['value_type'] = $collectionValueType; - } elseif ($type->isCollection() && \count($collectionValueType = $type->getCollectionValueTypes()) > 0 && Type::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) { + } elseif ($type->isCollection() && \count($collectionValueType = $type->getCollectionValueTypes()) > 0 && LegacyType::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) { // get inner type for any nested array [$innerType] = $collectionValueType; // note that it will break for any other builtinType $dimensions = '[]'; - while (\count($innerType->getCollectionValueTypes()) > 0 && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) { + while (\count($innerType->getCollectionValueTypes()) > 0 && LegacyType::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) { $dimensions .= '[]'; [$innerType] = $innerType->getCollectionValueTypes(); } @@ -518,9 +534,9 @@ private function validateAndDenormalize(array $types, string $currentClass, stri $class = $type->getClassName(); } - $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; + $expectedTypes[LegacyType::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; - if (Type::BUILTIN_TYPE_OBJECT === $builtinType && null !== $class) { + if (LegacyType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $class) { if (!$this->serializer instanceof DenormalizerInterface) { throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); } @@ -537,11 +553,11 @@ private function validateAndDenormalize(array $types, string $currentClass, stri // PHP's json_decode automatically converts Numbers without a decimal part to integers. // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when // a float is expected. - if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) { + if (LegacyType::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) { return (float) $data; } - if ((Type::BUILTIN_TYPE_FALSE === $builtinType && false === $data) || (Type::BUILTIN_TYPE_TRUE === $builtinType && true === $data)) { + if ((LegacyType::BUILTIN_TYPE_FALSE === $builtinType && false === $data) || (LegacyType::BUILTIN_TYPE_TRUE === $builtinType && true === $data)) { return $data; } @@ -590,16 +606,221 @@ private function validateAndDenormalize(array $types, string $currentClass, stri throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute); } + /** + * Validates the submitted data and denormalizes it. + * + * @throws NotNormalizableValueException + * @throws ExtraAttributesException + * @throws MissingConstructorArgumentsException + * @throws LogicException + */ + private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed + { + $expectedTypes = []; + $extraAttributesException = null; + $missingConstructorArgumentsException = null; + + $types = match (true) { + $type instanceof IntersectionType => throw new LogicException('Unable to handle intersection type.'), + $type instanceof UnionType => $type->getTypes(), + default => [$type], + }; + + foreach ($types as $t) { + if (null === $data && $type->isNullable()) { + return null; + } + + $collectionKeyType = $collectionValueType = null; + if ($t instanceof CollectionType) { + $collectionKeyType = $t->getCollectionKeyType(); + $collectionValueType = $t->getCollectionValueType(); + } + + $t = $t->getBaseType(); + + // Fix a collection that contains the only one element + // This is special to xml format only + if ('xml' === $format && $collectionValueType && !$collectionValueType->isA(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) { + $data = [$data]; + } + + // This try-catch should cover all NotNormalizableValueException (and all return branches after the first + // exception) so we could try denormalizing all types of an union type. If the target type is not an union + // type, we will just re-throw the catched exception. + // In the case of no denormalization succeeds with an union type, it will fall back to the default exception + // with the acceptable types list. + try { + // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine, + // if a value is meant to be a string, float, int or a boolean value from the serialized representation. + // That's why we have to transform the values, if one of these non-string basic datatypes is expected. + $typeIdentifier = $t->getTypeIdentifier(); + if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { + switch ($typeIdentifier) { + case TypeIdentifier::ARRAY: + if ('' === $data) { + return []; + } + break; + case TypeIdentifier::BOOL: + // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" + if ('false' === $data || '0' === $data) { + $data = false; + } elseif ('true' === $data || '1' === $data) { + $data = true; + } else { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::bool()], $context['deserialization_path'] ?? null); + } + break; + case TypeIdentifier::INT: + if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) { + $data = (int) $data; + } else { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::int()], $context['deserialization_path'] ?? null); + } + break; + case TypeIdentifier::FLOAT: + if (is_numeric($data)) { + return (float) $data; + } + + return match ($data) { + 'NaN' => \NAN, + 'INF' => \INF, + '-INF' => -\INF, + default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::float()], $context['deserialization_path'] ?? null), + }; + } + } + + if ($collectionValueType) { + $collectionValueBaseType = $collectionValueType instanceof UnionType ? $collectionValueType->asNonNullable()->getBaseType() : $collectionValueType->getBaseType(); + + if ($collectionValueBaseType instanceof ObjectType) { + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $collectionValueBaseType->getClassName().'[]'; + $context['key_type'] = $collectionKeyType; + $context['value_type'] = $collectionValueType; + } elseif (TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) { + // get inner type for any nested array + $innerType = $collectionValueType; + + // note that it will break for any other builtinType + $dimensions = '[]'; + while ($innerType instanceof CollectionType) { + $dimensions .= '[]'; + $innerType = $innerType->getCollectionValueType(); + } + + if ($innerType instanceof ObjectType) { + // the builtinType is the inner one and the class is the class followed by []...[] + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $innerType->getClassName().$dimensions; + } else { + // default fallback (keep it as array) + if ($t instanceof ObjectType) { + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $t->getClassName(); + } else { + $typeIdentifier = $t->getTypeIdentifier()->value; + $class = null; + } + } + } + } else { + if ($t instanceof ObjectType) { + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $t->getClassName(); + } else { + $typeIdentifier = $t->getTypeIdentifier(); + $class = null; + } + } + + $expectedTypes[TypeIdentifier::OBJECT === $typeIdentifier && $class ? $class : $typeIdentifier->value] = true; + + if (TypeIdentifier::OBJECT === $typeIdentifier && null !== $class) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); + } + + $childContext = $this->createChildContext($context, $attribute, $format); + if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) { + return $this->serializer->denormalize($data, $class, $format, $childContext); + } + } + + // JSON only has a Number type corresponding to both int and float PHP types. + // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert + // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible). + // PHP's json_decode automatically converts Numbers without a decimal part to integers. + // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when + // a float is expected. + if (TypeIdentifier::FLOAT === $typeIdentifier && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) { + return (float) $data; + } + + if ((TypeIdentifier::FALSE === $typeIdentifier && false === $data) || (TypeIdentifier::TRUE === $typeIdentifier && true === $data)) { + return $data; + } + + if (('is_'.$typeIdentifier->value)($data)) { + return $data; + } + } catch (NotNormalizableValueException|InvalidArgumentException $e) { + if (!$type instanceof UnionType) { + throw $e; + } + } catch (ExtraAttributesException $e) { + if (!$type instanceof UnionType) { + throw $e; + } + + $extraAttributesException ??= $e; + } catch (MissingConstructorArgumentsException $e) { + if (!$type instanceof UnionType) { + throw $e; + } + + $missingConstructorArgumentsException ??= $e; + } + } + + if ('' === $data && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format) && $type->isNullable()) { + return null; + } + + if ($extraAttributesException) { + throw $extraAttributesException; + } + + if ($missingConstructorArgumentsException) { + throw $missingConstructorArgumentsException; + } + + if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { + return $data; + } + + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute); + } + /** * @internal */ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, string $parameterName, mixed $parameterData, array $context, ?string $format = null): mixed { - if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $types = $this->getTypes($class->getName(), $parameterName)) { + if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $type = $this->getType($class->getName(), $parameterName)) { return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format); } - $parameterData = $this->validateAndDenormalize($types, $class->getName(), $parameterName, $parameterData, $format, $context); + // BC layer for PropertyTypeExtractorInterface::getTypes(). + // Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + if (\is_array($type)) { + $parameterData = $this->validateAndDenormalizeLegacy($type, $class->getName(), $parameterName, $parameterData, $format, $context); + } else { + $parameterData = $this->validateAndDenormalize($type, $class->getName(), $parameterName, $parameterData, $format, $context); + } $parameterData = $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context); @@ -607,42 +828,55 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara } /** - * @return Type[]|null + * @return Type|list|null */ - private function getTypes(string $currentClass, string $attribute): ?array + private function getType(string $currentClass, string $attribute): Type|array|null { if (null === $this->propertyTypeExtractor) { return null; } $key = $currentClass.'::'.$attribute; - if (isset($this->typesCache[$key])) { - return false === $this->typesCache[$key] ? null : $this->typesCache[$key]; + if (isset($this->typeCache[$key])) { + return false === $this->typeCache[$key] ? null : $this->typeCache[$key]; } - if (null !== $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) { - return $this->typesCache[$key] = $types; + if (null !== $type = $this->getPropertyType($currentClass, $attribute)) { + return $this->typeCache[$key] = $type; } if ($discriminatorMapping = $this->classDiscriminatorResolver?->getMappingForClass($currentClass)) { if ($discriminatorMapping->getTypeProperty() === $attribute) { - return $this->typesCache[$key] = [ - new Type(Type::BUILTIN_TYPE_STRING), - ]; + return $this->typeCache[$key] = Type::string(); } foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) { - if (null !== $types = $this->propertyTypeExtractor->getTypes($mappedClass, $attribute)) { - return $this->typesCache[$key] = $types; + if (null !== $type = $this->getPropertyType($mappedClass, $attribute)) { + return $this->typeCache[$key] = $type; } } } - $this->typesCache[$key] = false; + $this->typeCache[$key] = false; return null; } + /** + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + * + * @return Type|list|null + */ + private function getPropertyType(string $className, string $property): Type|array|null + { + if (method_exists($this->propertyTypeExtractor, 'getType')) { + return $this->propertyTypeExtractor->getType($className, $property); + } + + return $this->propertyTypeExtractor->getTypes($className, $property); + } + /** * Sets an attribute and apply the name converter if necessary. */ diff --git a/Normalizer/ArrayDenormalizer.php b/Normalizer/ArrayDenormalizer.php index a9c64a1e7..1bd6c54b3 100644 --- a/Normalizer/ArrayDenormalizer.php +++ b/Normalizer/ArrayDenormalizer.php @@ -11,10 +11,12 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Exception\BadMethodCallException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\UnionType; /** * Denormalizes arrays of objects. @@ -46,7 +48,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!'); } if (!\is_array($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, [Type::BUILTIN_TYPE_ARRAY], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, ['array'], $context['deserialization_path'] ?? null); } if (!str_ends_with($type, '[]')) { throw new InvalidArgumentException('Unsupported class: '.$type); @@ -54,15 +56,20 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $type = substr($type, 0, -2); - $builtinTypes = array_map(static function (Type $keyType) { - return $keyType->getBuiltinType(); - }, \is_array($keyType = $context['key_type'] ?? []) ? $keyType : [$keyType]); + $typeIdentifiers = []; + if (null !== $keyType = ($context['key_type'] ?? null)) { + if ($keyType instanceof Type) { + $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + } else { + $typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]); + } + } foreach ($data as $key => $value) { $subContext = $context; $subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]"; - $this->validateKeyType($builtinTypes, $key, $subContext['deserialization_path']); + $this->validateKeyType($typeIdentifiers, $key, $subContext['deserialization_path']); $data[$key] = $this->denormalizer->denormalize($value, $type, $format, $subContext); } @@ -80,18 +87,21 @@ public function supportsDenormalization(mixed $data, string $type, ?string $form && $this->denormalizer->supportsDenormalization($data, substr($type, 0, -2), $format, $context); } - private function validateKeyType(array $builtinTypes, mixed $key, string $path): void + /** + * @param list $typeIdentifiers + */ + private function validateKeyType(array $typeIdentifiers, mixed $key, string $path): void { - if (!$builtinTypes) { + if (!$typeIdentifiers) { return; } - foreach ($builtinTypes as $builtinType) { - if (('is_'.$builtinType)($key)) { + foreach ($typeIdentifiers as $typeIdentifier) { + if (('is_'.$typeIdentifier)($key)) { return; } } - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $builtinTypes), get_debug_type($key)), $key, $builtinTypes, $path, true); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $typeIdentifiers), get_debug_type($key)), $key, $typeIdentifiers, $path, true); } } diff --git a/Normalizer/BackedEnumNormalizer.php b/Normalizer/BackedEnumNormalizer.php index f6cefed87..504047bc1 100644 --- a/Normalizer/BackedEnumNormalizer.php +++ b/Normalizer/BackedEnumNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -70,7 +69,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } if (!\is_int($data) && !\is_string($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.', $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.', $data, ['int', 'string'], $context['deserialization_path'] ?? null, true); } try { diff --git a/Normalizer/DateTimeNormalizer.php b/Normalizer/DateTimeNormalizer.php index 46dff93f7..71ce26496 100644 --- a/Normalizer/DateTimeNormalizer.php +++ b/Normalizer/DateTimeNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -104,7 +103,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } if (!\is_string($data) || '' === trim($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType('The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType('The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.', $data, ['string'], $context['deserialization_path'] ?? null, true); } try { @@ -122,7 +121,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $dateTimeErrors = $type::getLastErrors(); - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, ['string'], $context['deserialization_path'] ?? null, true); } $defaultDateTimeFormat = $this->defaultContext[self::FORMAT_KEY] ?? null; @@ -137,7 +136,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } catch (NotNormalizableValueException $e) { throw $e; } catch (\Exception $e) { - throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, false, $e->getCode(), $e); + throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, false, $e->getCode(), $e); } } diff --git a/Normalizer/DateTimeZoneNormalizer.php b/Normalizer/DateTimeZoneNormalizer.php index 437e40bfc..f4528a03d 100644 --- a/Normalizer/DateTimeZoneNormalizer.php +++ b/Normalizer/DateTimeZoneNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -52,13 +51,13 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): \DateTimeZone { if ('' === $data || null === $data) { - throw NotNormalizableValueException::createForUnexpectedDataType('The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType('The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.', $data, ['string'], $context['deserialization_path'] ?? null, true); } try { return new \DateTimeZone($data); } catch (\Exception $e) { - throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); + throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); } } diff --git a/Normalizer/UidNormalizer.php b/Normalizer/UidNormalizer.php index 732f802be..a6cc190a9 100644 --- a/Normalizer/UidNormalizer.php +++ b/Normalizer/UidNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Uid\AbstractUid; @@ -71,7 +70,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a try { return $type::fromString($data); } catch (\InvalidArgumentException|\TypeError) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The data is not a valid "%s" string representation.', $type), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The data is not a valid "%s" string representation.', $type), $data, ['string'], $context['deserialization_path'] ?? null, true); } } diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index a8e0b6eb1..1f5a556ce 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -15,7 +15,7 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Attribute\Context; use Symfony\Component\Serializer\Attribute\DiscriminatorMap; use Symfony\Component\Serializer\Attribute\SerializedName; @@ -56,6 +56,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithStringObject; use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectDummyWithContextAttribute; +use Symfony\Component\TypeInfo\Type; class AbstractObjectNormalizerTest extends TestCase { @@ -433,11 +434,20 @@ public function testDenormalizeCollectionDecodedFromXmlWithTwoChildren() private function getDenormalizerForDummyCollection() { $extractor = $this->createMock(PhpDocExtractor::class); - $extractor->method('getTypes') - ->will($this->onConsecutiveCalls( - [new Type('array', false, null, true, new Type('int'), new Type('object', false, DummyChild::class))], - null - )); + + if (method_exists(PhpDocExtractor::class, 'getType')) { + $extractor->method('getType') + ->will($this->onConsecutiveCalls( + Type::list(Type::object(DummyChild::class)), + null, + )); + } else { + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new LegacyType('array', false, null, true, new LegacyType('int'), new LegacyType('object', false, DummyChild::class))], + null + )); + } $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); $arrayDenormalizer = new ArrayDenormalizerDummy(); @@ -488,11 +498,20 @@ public function testDenormalizeNotSerializableObjectToPopulate() private function getDenormalizerForStringCollection() { $extractor = $this->createMock(PhpDocExtractor::class); - $extractor->method('getTypes') - ->will($this->onConsecutiveCalls( - [new Type('array', false, null, true, new Type('int'), new Type('string'))], - null - )); + + if (method_exists(PhpDocExtractor::class, 'getType')) { + $extractor->method('getType') + ->will($this->onConsecutiveCalls( + Type::list(Type::string()), + null, + )); + } else { + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new LegacyType('array', false, null, true, new LegacyType('int'), new LegacyType('string'))], + null + )); + } $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); $arrayDenormalizer = new ArrayDenormalizerDummy(); @@ -675,21 +694,40 @@ public function testDenormalizeBasicTypePropertiesFromXml() private function getDenormalizerForObjectWithBasicProperties() { $extractor = $this->createMock(PhpDocExtractor::class); - $extractor->method('getTypes') - ->will($this->onConsecutiveCalls( - [new Type('bool')], - [new Type('bool')], - [new Type('bool')], - [new Type('bool')], - [new Type('int')], - [new Type('int')], - [new Type('float')], - [new Type('float')], - [new Type('float')], - [new Type('float')], - [new Type('float')], - [new Type('float')] - )); + + if (method_exists(PhpDocExtractor::class, 'getType')) { + $extractor->method('getType') + ->will($this->onConsecutiveCalls( + Type::bool(), + Type::bool(), + Type::bool(), + Type::bool(), + Type::int(), + Type::int(), + Type::float(), + Type::float(), + Type::float(), + Type::float(), + Type::float(), + Type::float(), + )); + } else { + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new LegacyType('bool')], + [new LegacyType('bool')], + [new LegacyType('bool')], + [new LegacyType('bool')], + [new LegacyType('int')], + [new LegacyType('int')], + [new LegacyType('float')], + [new LegacyType('float')], + [new LegacyType('float')], + [new LegacyType('float')], + [new LegacyType('float')], + [new LegacyType('float')] + )); + } $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); $arrayDenormalizer = new ArrayDenormalizerDummy(); diff --git a/composer.json b/composer.json index 627bfccaf..452dcc0a8 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.1", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", From 76696bcaee6c09f2f9fcd5c7135537fd1b43aaa2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 20 Mar 2024 21:34:40 +0100 Subject: [PATCH 021/104] [Serializer] Fix: Report Xml warning/error instead of silently returning a wrong xml --- Encoder/XmlEncoder.php | 23 +++++++++++++++++++++-- Tests/Encoder/XmlEncoderTest.php | 6 ++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Encoder/XmlEncoder.php b/Encoder/XmlEncoder.php index c33a35650..ad4d87c20 100644 --- a/Encoder/XmlEncoder.php +++ b/Encoder/XmlEncoder.php @@ -82,7 +82,7 @@ public function encode(mixed $data, string $format, array $context = []): string $encoderIgnoredNodeTypes = $context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES]; $ignorePiNode = \in_array(\XML_PI_NODE, $encoderIgnoredNodeTypes, true); if ($data instanceof \DOMDocument) { - return $data->saveXML($ignorePiNode ? $data->documentElement : null); + return $this->saveXml($data, $ignorePiNode ? $data->documentElement : null); } $xmlRootNodeName = $context[self::ROOT_NODE_NAME] ?? $this->defaultContext[self::ROOT_NODE_NAME]; @@ -97,7 +97,7 @@ public function encode(mixed $data, string $format, array $context = []): string $this->appendNode($dom, $data, $format, $context, $xmlRootNodeName); } - return $dom->saveXML($ignorePiNode ? $dom->documentElement : null, $context[self::SAVE_OPTIONS] ?? $this->defaultContext[self::SAVE_OPTIONS]); + return $this->saveXml($dom, $ignorePiNode ? $dom->documentElement : null, $context[self::SAVE_OPTIONS] ?? $this->defaultContext[self::SAVE_OPTIONS]); } public function decode(string $data, string $format, array $context = []): mixed @@ -498,4 +498,23 @@ private function createDomDocument(array $context): \DOMDocument return $document; } + + /** + * @throws NotEncodableValueException + */ + private function saveXml(\DOMDocument $document, ?\DOMNode $node = null, ?int $options = null): string + { + $prevErrorHandler = set_error_handler(static function ($type, $message, $file, $line, $context = []) use (&$prevErrorHandler) { + if (\E_ERROR === $type || \E_WARNING === $type) { + throw new NotEncodableValueException($message); + } + + return $prevErrorHandler ? $prevErrorHandler($type, $message, $file, $line, $context) : false; + }); + try { + return $document->saveXML($node, $options); + } finally { + restore_error_handler(); + } + } } diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index 97de86abf..f0ff1c5b9 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -433,6 +433,12 @@ public function testEncodeTraversableWhenNormalizable() $this->assertEquals($expected, $serializer->serialize(new NormalizableTraversableDummy(), 'xml')); } + public function testEncodeException() + { + $this->expectException(NotEncodableValueException::class); + $this->encoder->encode('Invalid character: '.\chr(7), 'xml'); + } + public function testDecode() { $source = $this->getXmlSource(); From 260e68552160e6dd283276144a93f51de107c5a6 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 9 Apr 2024 10:33:09 +0200 Subject: [PATCH 022/104] add $class, $format and $context arguments to NameConverterInterface methods --- CHANGELOG.md | 1 + .../CamelCaseToSnakeCaseNameConverter.php | 10 +++++++--- NameConverter/MetadataAwareNameConverter.php | 14 +++++++++++--- NameConverter/NameConverterInterface.php | 12 ++++++++++-- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e495db1c..4a84fa2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.1 --- + * Add arguments `$class`, `$format` and `$context` to `NameConverterInterface::normalize()` and `NameConverterInterface::denormalize()` * Add `DateTimeNormalizer::CAST_KEY` context option * Add `Default` and "class name" default groups * Add `AbstractNormalizer::FILTER_BOOL` context option diff --git a/NameConverter/CamelCaseToSnakeCaseNameConverter.php b/NameConverter/CamelCaseToSnakeCaseNameConverter.php index 8c0b53157..ad1e3cc9d 100644 --- a/NameConverter/CamelCaseToSnakeCaseNameConverter.php +++ b/NameConverter/CamelCaseToSnakeCaseNameConverter.php @@ -19,7 +19,7 @@ * @author Kévin Dunglas * @author Aurélien Pillevesse */ -class CamelCaseToSnakeCaseNameConverter implements AdvancedNameConverterInterface +class CamelCaseToSnakeCaseNameConverter implements NameConverterInterface { /** * Require all properties to be written in snake_case. @@ -36,7 +36,7 @@ public function __construct( ) { } - public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + public function normalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string { if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) { return strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); @@ -45,8 +45,12 @@ public function normalize(string $propertyName, ?string $class = null, ?string $ return $propertyName; } - public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + public function denormalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string { + $class = 1 < \func_num_args() ? func_get_arg(1) : null; + $format = 2 < \func_num_args() ? func_get_arg(2) : null; + $context = 3 < \func_num_args() ? func_get_arg(3) : []; + if (($context[self::REQUIRE_SNAKE_CASE_PROPERTIES] ?? false) && $propertyName !== $this->normalize($propertyName, $class, $format, $context)) { throw new UnexpectedPropertyException($propertyName); } diff --git a/NameConverter/MetadataAwareNameConverter.php b/NameConverter/MetadataAwareNameConverter.php index 327d92dc1..5366b57b9 100644 --- a/NameConverter/MetadataAwareNameConverter.php +++ b/NameConverter/MetadataAwareNameConverter.php @@ -18,7 +18,7 @@ /** * @author Fabien Bourigault */ -final class MetadataAwareNameConverter implements AdvancedNameConverterInterface +final class MetadataAwareNameConverter implements NameConverterInterface { /** * @var array> @@ -41,8 +41,12 @@ public function __construct( ) { } - public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + public function normalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string { + $class = 1 < \func_num_args() ? func_get_arg(1) : null; + $format = 2 < \func_num_args() ? func_get_arg(2) : null; + $context = 3 < \func_num_args() ? func_get_arg(3) : []; + if (null === $class) { return $this->normalizeFallback($propertyName, $class, $format, $context); } @@ -54,8 +58,12 @@ public function normalize(string $propertyName, ?string $class = null, ?string $ return self::$normalizeCache[$class][$propertyName] ?? $this->normalizeFallback($propertyName, $class, $format, $context); } - public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + public function denormalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string { + $class = 1 < \func_num_args() ? func_get_arg(1) : null; + $format = 2 < \func_num_args() ? func_get_arg(2) : null; + $context = 3 < \func_num_args() ? func_get_arg(3) : []; + if (null === $class) { return $this->denormalizeFallback($propertyName, $class, $format, $context); } diff --git a/NameConverter/NameConverterInterface.php b/NameConverter/NameConverterInterface.php index aba69a49e..d6bfeceb4 100644 --- a/NameConverter/NameConverterInterface.php +++ b/NameConverter/NameConverterInterface.php @@ -20,11 +20,19 @@ interface NameConverterInterface { /** * Converts a property name to its normalized value. + * + * @param class-string|null $class + * @param string|null $format + * @param array $context */ - public function normalize(string $propertyName): string; + public function normalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string; /** * Converts a property name to its denormalized value. + * + * @param class-string|null $class + * @param string|null $format + * @param array $context */ - public function denormalize(string $propertyName): string; + public function denormalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string; } From 7714934f959ca63e33419016e045b84f04e117ae Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 18 Apr 2024 08:05:33 +0200 Subject: [PATCH 023/104] add method parameters in final class --- NameConverter/CamelCaseToSnakeCaseNameConverter.php | 10 ++++++++++ NameConverter/MetadataAwareNameConverter.php | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/NameConverter/CamelCaseToSnakeCaseNameConverter.php b/NameConverter/CamelCaseToSnakeCaseNameConverter.php index ad1e3cc9d..47d69d3ab 100644 --- a/NameConverter/CamelCaseToSnakeCaseNameConverter.php +++ b/NameConverter/CamelCaseToSnakeCaseNameConverter.php @@ -36,6 +36,11 @@ public function __construct( ) { } + /** + * @param class-string|null $class + * @param string|null $format + * @param array $context + */ public function normalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string { if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) { @@ -45,6 +50,11 @@ public function normalize(string $propertyName/* , ?string $class = null, ?strin return $propertyName; } + /** + * @param class-string|null $class + * @param string|null $format + * @param array $context + */ public function denormalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string { $class = 1 < \func_num_args() ? func_get_arg(1) : null; diff --git a/NameConverter/MetadataAwareNameConverter.php b/NameConverter/MetadataAwareNameConverter.php index 5366b57b9..bc693bd90 100644 --- a/NameConverter/MetadataAwareNameConverter.php +++ b/NameConverter/MetadataAwareNameConverter.php @@ -41,7 +41,7 @@ public function __construct( ) { } - public function normalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { $class = 1 < \func_num_args() ? func_get_arg(1) : null; $format = 2 < \func_num_args() ? func_get_arg(2) : null; @@ -58,7 +58,7 @@ public function normalize(string $propertyName/* , ?string $class = null, ?strin return self::$normalizeCache[$class][$propertyName] ?? $this->normalizeFallback($propertyName, $class, $format, $context); } - public function denormalize(string $propertyName/* , ?string $class = null, ?string $format = null, array $context = [] */): string + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { $class = 1 < \func_num_args() ? func_get_arg(1) : null; $format = 2 < \func_num_args() ? func_get_arg(2) : null; From df7e418c75bb8baaee5357bb097b744fde273bf1 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 18 Apr 2024 08:27:37 +0200 Subject: [PATCH 024/104] fix tests --- Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php | 4 ++-- Tests/Normalizer/AbstractNormalizerTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php b/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php index f9d941890..fc9967f0c 100644 --- a/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php +++ b/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php @@ -61,7 +61,7 @@ public static function attributeProvider() public function testDenormalizeWithContext() { $nameConverter = new CamelCaseToSnakeCaseNameConverter(null, true); - $denormalizedValue = $nameConverter->denormalize('last_name', context: [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES]); + $denormalizedValue = $nameConverter->denormalize('last_name', null, null, [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES]); $this->assertSame($denormalizedValue, 'lastName'); } @@ -71,6 +71,6 @@ public function testErrorDenormalizeWithContext() $nameConverter = new CamelCaseToSnakeCaseNameConverter(null, true); $this->expectException(UnexpectedPropertyException::class); - $nameConverter->denormalize('lastName', context: [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES => true]); + $nameConverter->denormalize('lastName', null, null, [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES => true]); } } diff --git a/Tests/Normalizer/AbstractNormalizerTest.php b/Tests/Normalizer/AbstractNormalizerTest.php index a32574878..3108fe3c6 100644 --- a/Tests/Normalizer/AbstractNormalizerTest.php +++ b/Tests/Normalizer/AbstractNormalizerTest.php @@ -272,12 +272,12 @@ public static function getNormalizerWithCustomNameConverter() { $extractor = new PhpDocExtractor(); $nameConverter = new class() implements NameConverterInterface { - public function normalize(string $propertyName): string + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { return ucfirst($propertyName); } - public function denormalize(string $propertyName): string + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { return lcfirst($propertyName); } From 72d87ea1624fee8b9f1aa927a0619c367296defd Mon Sep 17 00:00:00 2001 From: javaDeveloperKid Date: Wed, 17 Apr 2024 23:53:56 +0200 Subject: [PATCH 025/104] fix typo in AbstractNormalizerContextBuilder::withDefaultContructorArguments() --- CHANGELOG.md | 1 + .../AbstractNormalizerContextBuilder.php | 20 +++++++++++++++---- .../AbstractNormalizerContextBuilderTest.php | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a84fa2c7..4f456f3f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add `Default` and "class name" default groups * Add `AbstractNormalizer::FILTER_BOOL` context option * Add `CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES` context option + * Deprecate `AbstractNormalizerContextBuilder::withDefaultContructorArguments(?array $defaultContructorArguments)`, use `withDefaultConstructorArguments(?array $defaultConstructorArguments)` instead (note the missing `s` character in Contructor word in deprecated method) 7.0 --- diff --git a/Context/Normalizer/AbstractNormalizerContextBuilder.php b/Context/Normalizer/AbstractNormalizerContextBuilder.php index ecb328dd6..cb5b0544d 100644 --- a/Context/Normalizer/AbstractNormalizerContextBuilder.php +++ b/Context/Normalizer/AbstractNormalizerContextBuilder.php @@ -104,17 +104,29 @@ public function withAllowExtraAttributes(?bool $allowExtraAttributes): static } /** - * Configures an hashmap of classes containing hashmaps of constructor argument => default value. + * @deprecated since Symfony 7.1, use withDefaultConstructorArguments(?array $defaultConstructorArguments)" instead + * + * @param array>|null $defaultContructorArguments + */ + public function withDefaultContructorArguments(?array $defaultContructorArguments): static + { + trigger_deprecation('symfony/serializer', '7.1', 'The "%s()" method is deprecated, use "withDefaultConstructorArguments(?array $defaultConstructorArguments)" instead.', __METHOD__); + + return self::withDefaultConstructorArguments($defaultContructorArguments); + } + + /** + * Configures a hashmap of classes containing hashmaps of constructor argument => default value. * * The names need to match the parameter names in the constructor arguments. * * Eg: [Foo::class => ['foo' => true, 'bar' => 0]] * - * @param array>|null $defaultContructorArguments + * @param array>|null $defaultConstructorArguments */ - public function withDefaultContructorArguments(?array $defaultContructorArguments): static + public function withDefaultConstructorArguments(?array $defaultConstructorArguments): static { - return $this->with(AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS, $defaultContructorArguments); + return $this->with(AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS, $defaultConstructorArguments); } /** diff --git a/Tests/Context/Normalizer/AbstractNormalizerContextBuilderTest.php b/Tests/Context/Normalizer/AbstractNormalizerContextBuilderTest.php index 158fa8fea..4b8f0cc3f 100644 --- a/Tests/Context/Normalizer/AbstractNormalizerContextBuilderTest.php +++ b/Tests/Context/Normalizer/AbstractNormalizerContextBuilderTest.php @@ -41,7 +41,7 @@ public function testWithers(array $values) ->withGroups($values[AbstractNormalizer::GROUPS]) ->withAttributes($values[AbstractNormalizer::ATTRIBUTES]) ->withAllowExtraAttributes($values[AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES]) - ->withDefaultContructorArguments($values[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS]) + ->withDefaultConstructorArguments($values[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS]) ->withCallbacks($values[AbstractNormalizer::CALLBACKS]) ->withCircularReferenceHandler($values[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER]) ->withIgnoredAttributes($values[AbstractNormalizer::IGNORED_ATTRIBUTES]) From 529d35861cb1b96a8b1bf68b07e37477260c85db Mon Sep 17 00:00:00 2001 From: alexpozzi Date: Thu, 18 Apr 2024 16:39:43 +0200 Subject: [PATCH 026/104] [Serializer] Add XmlEncoder::CDATA_WRAPPING_PATTERN context option --- CHANGELOG.md | 1 + Context/Encoder/XmlEncoderContextBuilder.php | 8 +++ Encoder/XmlEncoder.php | 4 +- .../Encoder/XmlEncoderContextBuilderTest.php | 3 ++ Tests/Encoder/XmlEncoderTest.php | 52 ++++++++++++++++--- 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f456f3f8..3118834d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Add `AbstractNormalizer::FILTER_BOOL` context option * Add `CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES` context option * Deprecate `AbstractNormalizerContextBuilder::withDefaultContructorArguments(?array $defaultContructorArguments)`, use `withDefaultConstructorArguments(?array $defaultConstructorArguments)` instead (note the missing `s` character in Contructor word in deprecated method) + * Add `XmlEncoder::CDATA_WRAPPING_PATTERN` context option 7.0 --- diff --git a/Context/Encoder/XmlEncoderContextBuilder.php b/Context/Encoder/XmlEncoderContextBuilder.php index 34cf78198..0fd1f2f44 100644 --- a/Context/Encoder/XmlEncoderContextBuilder.php +++ b/Context/Encoder/XmlEncoderContextBuilder.php @@ -152,4 +152,12 @@ public function withCdataWrapping(?bool $cdataWrapping): static { return $this->with(XmlEncoder::CDATA_WRAPPING, $cdataWrapping); } + + /** + * Configures the pattern used to evaluate if a CDATA section should be added. + */ + public function withCdataWrappingPattern(?string $cdataWrappingPattern): static + { + return $this->with(XmlEncoder::CDATA_WRAPPING_PATTERN, $cdataWrappingPattern); + } } diff --git a/Encoder/XmlEncoder.php b/Encoder/XmlEncoder.php index ad4d87c20..5dcb2ba7e 100644 --- a/Encoder/XmlEncoder.php +++ b/Encoder/XmlEncoder.php @@ -59,6 +59,7 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa public const TYPE_CAST_ATTRIBUTES = 'xml_type_cast_attributes'; public const VERSION = 'xml_version'; public const CDATA_WRAPPING = 'cdata_wrapping'; + public const CDATA_WRAPPING_PATTERN = 'cdata_wrapping_pattern'; private array $defaultContext = [ self::AS_COLLECTION => false, @@ -70,6 +71,7 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa self::ROOT_NODE_NAME => 'response', self::TYPE_CAST_ATTRIBUTES => true, self::CDATA_WRAPPING => true, + self::CDATA_WRAPPING_PATTERN => '/[<>&]/', ]; public function __construct(array $defaultContext = []) @@ -433,7 +435,7 @@ private function appendNode(\DOMNode $parentNode, mixed $data, string $format, a */ private function needsCdataWrapping(string $val, array $context): bool { - return ($context[self::CDATA_WRAPPING] ?? $this->defaultContext[self::CDATA_WRAPPING]) && preg_match('/[<>&]/', $val); + return ($context[self::CDATA_WRAPPING] ?? $this->defaultContext[self::CDATA_WRAPPING]) && preg_match($context[self::CDATA_WRAPPING_PATTERN] ?? $this->defaultContext[self::CDATA_WRAPPING_PATTERN], $val); } /** diff --git a/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php b/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php index d1ea307a9..2f71c6012 100644 --- a/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php +++ b/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php @@ -46,6 +46,7 @@ public function testWithers(array $values) ->withTypeCastAttributes($values[XmlEncoder::TYPE_CAST_ATTRIBUTES]) ->withVersion($values[XmlEncoder::VERSION]) ->withCdataWrapping($values[XmlEncoder::CDATA_WRAPPING]) + ->withCdataWrappingPattern($values[XmlEncoder::CDATA_WRAPPING_PATTERN]) ->toArray(); $this->assertSame($values, $context); @@ -67,6 +68,7 @@ public static function withersDataProvider(): iterable XmlEncoder::TYPE_CAST_ATTRIBUTES => true, XmlEncoder::VERSION => '1.0', XmlEncoder::CDATA_WRAPPING => false, + XmlEncoder::CDATA_WRAPPING_PATTERN => '/[<>&"\']/', ]]; yield 'With null values' => [[ @@ -83,6 +85,7 @@ public static function withersDataProvider(): iterable XmlEncoder::TYPE_CAST_ATTRIBUTES => null, XmlEncoder::VERSION => null, XmlEncoder::CDATA_WRAPPING => null, + XmlEncoder::CDATA_WRAPPING_PATTERN => null, ]]; } } diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index f0ff1c5b9..5be6be232 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -231,16 +231,56 @@ public function testEncodeRootAttributes() $this->assertEquals($expected, $this->encoder->encode($array, 'xml')); } - public function testEncodeCdataWrapping() + /** + * @dataProvider encodeCdataWrappingWithDefaultPattern + */ + public function testEncodeCdataWrappingWithDefaultPattern($input, $expected) { - $array = [ - 'firstname' => 'Paul & Martha ', + $this->assertEquals($expected, $this->encoder->encode($input, 'xml')); + } + + public static function encodeCdataWrappingWithDefaultPattern() + { + return [ + [ + ['firstname' => 'Paul and Martha'], + ''."\n".'Paul and Martha'."\n", + ], + [ + ['lastname' => 'O\'Donnel'], + ''."\n".'O\'Donnel'."\n", + ], + [ + ['firstname' => 'Paul & Martha '], + ''."\n".']]>'."\n", + ], ]; + } - $expected = ''."\n". - ']]>'."\n"; + /** + * @dataProvider encodeCdataWrappingWithCustomPattern + */ + public function testEncodeCdataWrappingWithCustomPattern($input, $expected) + { + $this->assertEquals($expected, $this->encoder->encode($input, 'xml', ['cdata_wrapping_pattern' => '/[<>&"\']/'])); + } - $this->assertEquals($expected, $this->encoder->encode($array, 'xml')); + public static function encodeCdataWrappingWithCustomPattern() + { + return [ + [ + ['firstname' => 'Paul and Martha'], + ''."\n".'Paul and Martha'."\n", + ], + [ + ['lastname' => 'O\'Donnel'], + ''."\n".''."\n", + ], + [ + ['firstname' => 'Paul & Martha '], + ''."\n".']]>'."\n", + ], + ]; } public function testEnableCdataWrapping() From b8aa793e695b2ae3d8a35a9ed2d43b1ec423eaac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jusi=C4=99ga?= Date: Mon, 6 May 2024 19:21:10 +0200 Subject: [PATCH 027/104] Fixed "Warning: Attempt to read property "value" on string" --- Normalizer/AbstractObjectNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index d1f565cea..e766e3245 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -730,7 +730,7 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $typeIdentifier = TypeIdentifier::OBJECT; $class = $t->getClassName(); } else { - $typeIdentifier = $t->getTypeIdentifier()->value; + $typeIdentifier = $t->getTypeIdentifier(); $class = null; } } From d62457270ee38c12a80d612ed9915885dc82e987 Mon Sep 17 00:00:00 2001 From: Maximilian Beckers Date: Fri, 17 May 2024 07:31:11 +0200 Subject: [PATCH 028/104] [Serializer] rename the first parameter of ``NormalizerInterface::normalize`` from object to data because of type mixed --- Normalizer/NormalizerInterface.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Normalizer/NormalizerInterface.php b/Normalizer/NormalizerInterface.php index 562f87c28..bbc8a94e7 100644 --- a/Normalizer/NormalizerInterface.php +++ b/Normalizer/NormalizerInterface.php @@ -22,9 +22,9 @@ interface NormalizerInterface { /** - * Normalizes an object into a set of arrays/scalars. + * Normalizes data into a set of arrays/scalars. * - * @param mixed $object Object to normalize + * @param mixed $data Data to normalize * @param string|null $format Format the normalization result will be encoded as * @param array $context Context options for the normalizer * @@ -36,7 +36,7 @@ interface NormalizerInterface * @throws LogicException Occurs when the normalizer is not called in an expected context * @throws ExceptionInterface Occurs for all the other cases of errors */ - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null; + public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null; /** * Checks whether the given class is supported for normalization by this normalizer. From af03803b2d5e001948ae792d85f3feb9a2f85178 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 17 May 2024 09:05:25 +0200 Subject: [PATCH 029/104] add missing deprecation contracts dependency --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 452dcc0a8..0092a9643 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8" }, "require-dev": { From 8030de88a41354aeb7659539211d1e9be8ce70e9 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 17 May 2024 08:37:35 +0200 Subject: [PATCH 030/104] add test --- .../AbstractObjectNormalizerTest.php | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index b878ca5c0..fea4f6066 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1129,6 +1129,24 @@ public function testNormalizationWithMaxDepthOnStdclassObjectDoesNotThrowWarning $this->assertSame(['string' => 'yes'], $normalized); } + + public function testDenormalizeCollectionOfScalarTypesPropertyWithPhpDocExtractor() + { + $normalizer = new AbstractObjectNormalizerWithMetadataAndPhpDocExtractor(); + $data = [ + 'type' => 'foo', + 'values' => [ + ['1'], + ['2'], + ['3'], + ['4'], + ['5'], + ], + ]; + $expected = new ScalarCollectionDocBlockDummy([[1], [2], [3], [4], [5]]); + + $this->assertEquals($expected, $normalizer->denormalize($data, ScalarCollectionDocBlockDummy::class)); + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer @@ -1540,3 +1558,50 @@ public function __construct( ) { } } + +#[DiscriminatorMap('type', ['foo' => ScalarCollectionDocBlockDummy::class])] +class ScalarCollectionDocBlockDummy +{ + /** + * @param array>|null $values + */ + public function __construct( + private readonly ?array $values = null, + ) { + } + + /** @return array>|null */ + public function getValues(): ?array + { + return $this->values; + } +} + +class AbstractObjectNormalizerWithMetadataAndPhpDocExtractor extends AbstractObjectNormalizer +{ + public function __construct() + { + parent::__construct(new ClassMetadataFactory(new AttributeLoader()), null, new PropertyInfoExtractor([], [new PhpDocExtractor()])); + } + + protected function extractAttributes(object $object, ?string $format = null, array $context = []): array + { + return []; + } + + protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed + { + return null; + } + + protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void + { + } + + public function getSupportedTypes(?string $format): array + { + return [ + '*' => false, + ]; + } +} From d7e6907e0c9c7e606e0a057d03ec6a78b12bb32c Mon Sep 17 00:00:00 2001 From: HypeMC Date: Tue, 28 May 2024 01:30:34 +0200 Subject: [PATCH 031/104] [Serializer] Fix denormalizing a collection of union types --- Normalizer/AbstractObjectNormalizer.php | 7 +++- .../AbstractObjectNormalizerTest.php | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index e766e3245..df3d693f2 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -32,6 +32,7 @@ use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Exception\LogicException as TypeInfoLogicException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\IntersectionType; @@ -702,7 +703,11 @@ private function validateAndDenormalize(Type $type, string $currentClass, string } if ($collectionValueType) { - $collectionValueBaseType = $collectionValueType instanceof UnionType ? $collectionValueType->asNonNullable()->getBaseType() : $collectionValueType->getBaseType(); + try { + $collectionValueBaseType = $collectionValueType->getBaseType(); + } catch (TypeInfoLogicException) { + $collectionValueBaseType = Type::mixed(); + } if ($collectionValueBaseType instanceof ObjectType) { $typeIdentifier = TypeIdentifier::OBJECT; diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index fea4f6066..f41c0fdf3 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1147,6 +1147,25 @@ public function testDenormalizeCollectionOfScalarTypesPropertyWithPhpDocExtracto $this->assertEquals($expected, $normalizer->denormalize($data, ScalarCollectionDocBlockDummy::class)); } + + public function testDenormalizeCollectionOfUnionTypesPropertyWithPhpDocExtractor() + { + $normalizer = new AbstractObjectNormalizerWithMetadataAndPhpDocExtractor(); + $data = [ + 'values1' => [ + 'foo' => 'foo', + 'bar' => 222, + ], + 'values2' => [ + 'baz' => 'baz', + 'qux' => 333, + ], + ]; + $expected = new UnionCollectionDocBlockDummy($data['values1']); + $expected->values2 = $data['values2']; + + $this->assertEquals($expected, $normalizer->denormalize($data, UnionCollectionDocBlockDummy::class)); + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer @@ -1577,6 +1596,22 @@ public function getValues(): ?array } } +class UnionCollectionDocBlockDummy +{ + /** + * @param array $values1 + */ + public function __construct( + public array $values1, + ) { + } + + /** + * @var array + */ + public array $values2; +} + class AbstractObjectNormalizerWithMetadataAndPhpDocExtractor extends AbstractObjectNormalizer { public function __construct() @@ -1596,6 +1631,9 @@ protected function getAttributeValue(object $object, string $attribute, ?string protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void { + if (property_exists($object, $attribute)) { + $object->$attribute = $value; + } } public function getSupportedTypes(?string $format): array From 0128e9f9c91ac12bad70bfa82eb0dbdb9021b57f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 3 Jun 2024 15:27:28 +0200 Subject: [PATCH 032/104] use constructor property promotion --- Extractor/ObjectPropertyListExtractor.php | 8 ++++---- Mapping/Loader/FileLoader.php | 9 +++------ Normalizer/AbstractNormalizer.php | 11 +++++------ 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Extractor/ObjectPropertyListExtractor.php b/Extractor/ObjectPropertyListExtractor.php index 8422b0786..42a546805 100644 --- a/Extractor/ObjectPropertyListExtractor.php +++ b/Extractor/ObjectPropertyListExtractor.php @@ -18,12 +18,12 @@ */ final class ObjectPropertyListExtractor implements ObjectPropertyListExtractorInterface { - private PropertyListExtractorInterface $propertyListExtractor; private \Closure $objectClassResolver; - public function __construct(PropertyListExtractorInterface $propertyListExtractor, ?callable $objectClassResolver = null) - { - $this->propertyListExtractor = $propertyListExtractor; + public function __construct( + private PropertyListExtractorInterface $propertyListExtractor, + ?callable $objectClassResolver = null, + ) { $this->objectClassResolver = ($objectClassResolver ?? 'get_class')(...); } diff --git a/Mapping/Loader/FileLoader.php b/Mapping/Loader/FileLoader.php index 7fda4ebd5..ccd443b04 100644 --- a/Mapping/Loader/FileLoader.php +++ b/Mapping/Loader/FileLoader.php @@ -20,15 +20,14 @@ */ abstract class FileLoader implements LoaderInterface { - protected string $file; - /** * @param string $file The mapping file to load * * @throws MappingException if the mapping file does not exist or is not readable */ - public function __construct(string $file) - { + public function __construct( + protected string $file, + ) { if (!is_file($file)) { throw new MappingException(sprintf('The mapping file "%s" does not exist.', $file)); } @@ -36,7 +35,5 @@ public function __construct(string $file) if (!is_readable($file)) { throw new MappingException(sprintf('The mapping file "%s" is not readable.', $file)); } - - $this->file = $file; } } diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index 004581399..70714ac24 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -139,16 +139,15 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn self::CIRCULAR_REFERENCE_LIMIT => 1, self::IGNORED_ATTRIBUTES => [], ]; - protected ?ClassMetadataFactoryInterface $classMetadataFactory; - protected ?NameConverterInterface $nameConverter; /** * Sets the {@link ClassMetadataFactoryInterface} to use. */ - public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, array $defaultContext = []) - { - $this->classMetadataFactory = $classMetadataFactory; - $this->nameConverter = $nameConverter; + public function __construct( + protected ?ClassMetadataFactoryInterface $classMetadataFactory = null, + protected ?NameConverterInterface $nameConverter = null, + array $defaultContext = [], + ) { $this->defaultContext = array_merge($this->defaultContext, $defaultContext); $this->validateCallbackContext($this->defaultContext, 'default'); From 9a3acd7fa878f64de2dbdb20c39af4b954ebf1c2 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 5 Jun 2024 09:30:29 +0200 Subject: [PATCH 033/104] avoid calling undefined built-in is_*() functions --- Normalizer/AbstractObjectNormalizer.php | 23 +++++++++++++++---- .../AbstractObjectNormalizerTest.php | 22 ++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index df3d693f2..27c383177 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -773,11 +773,24 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return (float) $data; } - if ((TypeIdentifier::FALSE === $typeIdentifier && false === $data) || (TypeIdentifier::TRUE === $typeIdentifier && true === $data)) { - return $data; - } - - if (('is_'.$typeIdentifier->value)($data)) { + $dataMatchesExpectedType = match ($typeIdentifier) { + TypeIdentifier::ARRAY => \is_array($data), + TypeIdentifier::BOOL => \is_bool($data), + TypeIdentifier::CALLABLE => \is_callable($data), + TypeIdentifier::FALSE => false === $data, + TypeIdentifier::FLOAT => \is_float($data), + TypeIdentifier::INT => \is_int($data), + TypeIdentifier::ITERABLE => is_iterable($data), + TypeIdentifier::MIXED => true, + TypeIdentifier::NULL => null === $data, + TypeIdentifier::OBJECT => \is_object($data), + TypeIdentifier::RESOURCE => \is_resource($data), + TypeIdentifier::STRING => \is_string($data), + TypeIdentifier::TRUE => true === $data, + default => false, + }; + + if ($dataMatchesExpectedType) { return $data; } } catch (NotNormalizableValueException|InvalidArgumentException $e) { diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index f41c0fdf3..21ce5d768 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1132,7 +1132,7 @@ public function testNormalizationWithMaxDepthOnStdclassObjectDoesNotThrowWarning public function testDenormalizeCollectionOfScalarTypesPropertyWithPhpDocExtractor() { - $normalizer = new AbstractObjectNormalizerWithMetadataAndPhpDocExtractor(); + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); $data = [ 'type' => 'foo', 'values' => [ @@ -1150,7 +1150,7 @@ public function testDenormalizeCollectionOfScalarTypesPropertyWithPhpDocExtracto public function testDenormalizeCollectionOfUnionTypesPropertyWithPhpDocExtractor() { - $normalizer = new AbstractObjectNormalizerWithMetadataAndPhpDocExtractor(); + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); $data = [ 'values1' => [ 'foo' => 'foo', @@ -1166,6 +1166,15 @@ public function testDenormalizeCollectionOfUnionTypesPropertyWithPhpDocExtractor $this->assertEquals($expected, $normalizer->denormalize($data, UnionCollectionDocBlockDummy::class)); } + + public function testDenormalizeMixedProperty() + { + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); + $expected = new MixedPropertyDummy(); + $expected->foo = 'bar'; + + $this->assertEquals($expected, $normalizer->denormalize(['foo' => 'bar'], MixedPropertyDummy::class)); + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer @@ -1268,6 +1277,11 @@ class SnakeCaseNestedDummy public $fooBar; } +class MixedPropertyDummy +{ + public mixed $foo; +} + #[DiscriminatorMap(typeProperty: 'type', mapping: [ 'first' => FirstNestedDummyWithConstructorAndDiscriminator::class, 'second' => SecondNestedDummyWithConstructorAndDiscriminator::class, @@ -1612,11 +1626,11 @@ public function __construct( public array $values2; } -class AbstractObjectNormalizerWithMetadataAndPhpDocExtractor extends AbstractObjectNormalizer +class AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors extends AbstractObjectNormalizer { public function __construct() { - parent::__construct(new ClassMetadataFactory(new AttributeLoader()), null, new PropertyInfoExtractor([], [new PhpDocExtractor()])); + parent::__construct(new ClassMetadataFactory(new AttributeLoader()), null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()])); } protected function extractAttributes(object $object, ?string $format = null, array $context = []): array From 4b41b4b5e0e33aa69921416b77658f6f7a65b489 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 20 Jun 2024 17:52:34 +0200 Subject: [PATCH 034/104] Prefix all sprintf() calls --- Attribute/Context.php | 4 +- Attribute/DiscriminatorMap.php | 4 +- Attribute/Groups.php | 4 +- Attribute/MaxDepth.php | 2 +- Attribute/SerializedName.php | 2 +- Attribute/SerializedPath.php | 2 +- Command/DebugCommand.php | 4 +- Context/Encoder/CsvEncoderContextBuilder.php | 6 +-- .../AbstractNormalizerContextBuilder.php | 2 +- ...AbstractObjectNormalizerContextBuilder.php | 2 +- .../DateTimeNormalizerContextBuilder.php | 2 +- .../UidNormalizerContextBuilder.php | 2 +- .../UnwrappingDenormalizerContextBuilder.php | 2 +- Debug/TraceableEncoder.php | 4 +- Debug/TraceableNormalizer.php | 4 +- Encoder/ChainDecoder.php | 2 +- Encoder/ChainEncoder.php | 2 +- Encoder/CsvEncoder.php | 2 +- Encoder/JsonDecode.php | 2 +- Encoder/XmlEncoder.php | 6 +-- Exception/ExtraAttributesException.php | 2 +- Exception/UnexpectedPropertyException.php | 2 +- .../Factory/ClassMetadataFactoryCompiler.php | 2 +- Mapping/Factory/ClassResolverTrait.php | 2 +- .../Factory/CompiledClassMetadataFactory.php | 2 +- Mapping/Loader/AttributeLoader.php | 16 ++++---- Mapping/Loader/FileLoader.php | 4 +- Mapping/Loader/LoaderChain.php | 2 +- Mapping/Loader/XmlFileLoader.php | 2 +- Mapping/Loader/YamlFileLoader.php | 20 +++++----- NameConverter/MetadataAwareNameConverter.php | 4 +- Normalizer/AbstractNormalizer.php | 22 +++++----- Normalizer/AbstractObjectNormalizer.php | 40 +++++++++---------- Normalizer/ArrayDenormalizer.php | 8 ++-- .../ConstraintViolationListNormalizer.php | 4 +- Normalizer/DataUriNormalizer.php | 8 ++-- Normalizer/DateIntervalNormalizer.php | 2 +- Normalizer/DateTimeNormalizer.php | 8 ++-- Normalizer/JsonSerializableNormalizer.php | 4 +- Normalizer/MimeMessageNormalizer.php | 2 +- Normalizer/ProblemNormalizer.php | 2 +- Normalizer/PropertyNormalizer.php | 2 +- Normalizer/TranslatableNormalizer.php | 2 +- Normalizer/UidNormalizer.php | 4 +- Serializer.php | 16 ++++---- Tests/Annotation/ContextTest.php | 2 +- Tests/Encoder/XmlEncoderTest.php | 4 +- Tests/Mapping/Loader/AttributeLoaderTest.php | 2 +- .../Features/CircularReferenceTestTrait.php | 2 +- .../ConstructorArgumentsTestTrait.php | 4 +- Tests/Normalizer/ObjectNormalizerTest.php | 4 +- 51 files changed, 130 insertions(+), 130 deletions(-) diff --git a/Attribute/Context.php b/Attribute/Context.php index 5ea2d2eb5..892af481a 100644 --- a/Attribute/Context.php +++ b/Attribute/Context.php @@ -36,14 +36,14 @@ public function __construct( string|array $groups = [], ) { if (!$context && !$normalizationContext && !$denormalizationContext) { - throw new InvalidArgumentException(sprintf('At least one of the "context", "normalizationContext", or "denormalizationContext" options must be provided as a non-empty array to "%s".', static::class)); + throw new InvalidArgumentException(\sprintf('At least one of the "context", "normalizationContext", or "denormalizationContext" options must be provided as a non-empty array to "%s".', static::class)); } $this->groups = (array) $groups; foreach ($this->groups as $group) { if (!\is_string($group)) { - throw new InvalidArgumentException(sprintf('Parameter "groups" given to "%s" must be a string or an array of strings, "%s" given.', static::class, get_debug_type($group))); + throw new InvalidArgumentException(\sprintf('Parameter "groups" given to "%s" must be a string or an array of strings, "%s" given.', static::class, get_debug_type($group))); } } } diff --git a/Attribute/DiscriminatorMap.php b/Attribute/DiscriminatorMap.php index a77fc2984..48d0842aa 100644 --- a/Attribute/DiscriminatorMap.php +++ b/Attribute/DiscriminatorMap.php @@ -30,11 +30,11 @@ public function __construct( private readonly array $mapping, ) { if (!$typeProperty) { - throw new InvalidArgumentException(sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class)); + throw new InvalidArgumentException(\sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class)); } if (!$mapping) { - throw new InvalidArgumentException(sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class)); + throw new InvalidArgumentException(\sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class)); } } diff --git a/Attribute/Groups.php b/Attribute/Groups.php index 39914f971..8747949a4 100644 --- a/Attribute/Groups.php +++ b/Attribute/Groups.php @@ -32,12 +32,12 @@ public function __construct(string|array $groups) $this->groups = (array) $groups; if (!$this->groups) { - throw new InvalidArgumentException(sprintf('Parameter given to "%s" cannot be empty.', static::class)); + throw new InvalidArgumentException(\sprintf('Parameter given to "%s" cannot be empty.', static::class)); } foreach ($this->groups as $group) { if (!\is_string($group) || '' === $group) { - throw new InvalidArgumentException(sprintf('Parameter given to "%s" must be a string or an array of non-empty strings.', static::class)); + throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a string or an array of non-empty strings.', static::class)); } } } diff --git a/Attribute/MaxDepth.php b/Attribute/MaxDepth.php index 17562b6c2..16d6a8120 100644 --- a/Attribute/MaxDepth.php +++ b/Attribute/MaxDepth.php @@ -25,7 +25,7 @@ class MaxDepth public function __construct(private readonly int $maxDepth) { if ($maxDepth <= 0) { - throw new InvalidArgumentException(sprintf('Parameter given to "%s" must be a positive integer.', static::class)); + throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a positive integer.', static::class)); } } diff --git a/Attribute/SerializedName.php b/Attribute/SerializedName.php index 6acbebd03..f1c6cefe2 100644 --- a/Attribute/SerializedName.php +++ b/Attribute/SerializedName.php @@ -25,7 +25,7 @@ class SerializedName public function __construct(private readonly string $serializedName) { if ('' === $serializedName) { - throw new InvalidArgumentException(sprintf('Parameter given to "%s" must be a non-empty string.', self::class)); + throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a non-empty string.', self::class)); } } diff --git a/Attribute/SerializedPath.php b/Attribute/SerializedPath.php index 118e3adbe..71254bfb9 100644 --- a/Attribute/SerializedPath.php +++ b/Attribute/SerializedPath.php @@ -31,7 +31,7 @@ public function __construct(string $serializedPath) try { $this->serializedPath = new PropertyPath($serializedPath); } catch (InvalidPropertyPathException $pathException) { - throw new InvalidArgumentException(sprintf('Parameter given to "%s" must be a valid property path.', self::class)); + throw new InvalidArgumentException(\sprintf('Parameter given to "%s" must be a valid property path.', self::class)); } } diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index c85ee213e..3e70c93a5 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -49,7 +49,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!class_exists($class)) { $io = new SymfonyStyle($input, $output); - $io->error(sprintf('Class "%s" was not found.', $class)); + $io->error(\sprintf('Class "%s" was not found.', $class)); return Command::FAILURE; } @@ -62,7 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function dumpSerializerDataForClass(InputInterface $input, OutputInterface $output, string $class): void { $io = new SymfonyStyle($input, $output); - $title = sprintf('%s', $class); + $title = \sprintf('%s', $class); $rows = []; $dump = new Dumper($output); diff --git a/Context/Encoder/CsvEncoderContextBuilder.php b/Context/Encoder/CsvEncoderContextBuilder.php index f75759031..f449bc314 100644 --- a/Context/Encoder/CsvEncoderContextBuilder.php +++ b/Context/Encoder/CsvEncoderContextBuilder.php @@ -35,7 +35,7 @@ final class CsvEncoderContextBuilder implements ContextBuilderInterface public function withDelimiter(?string $delimiter): static { if (null !== $delimiter && 1 !== \strlen($delimiter)) { - throw new InvalidArgumentException(sprintf('The "%s" delimiter must be a single character.', $delimiter)); + throw new InvalidArgumentException(\sprintf('The "%s" delimiter must be a single character.', $delimiter)); } return $this->with(CsvEncoder::DELIMITER_KEY, $delimiter); @@ -51,7 +51,7 @@ public function withDelimiter(?string $delimiter): static public function withEnclosure(?string $enclosure): static { if (null !== $enclosure && 1 !== \strlen($enclosure)) { - throw new InvalidArgumentException(sprintf('The "%s" enclosure must be a single character.', $enclosure)); + throw new InvalidArgumentException(\sprintf('The "%s" enclosure must be a single character.', $enclosure)); } return $this->with(CsvEncoder::ENCLOSURE_KEY, $enclosure); @@ -67,7 +67,7 @@ public function withEnclosure(?string $enclosure): static public function withEscapeChar(?string $escapeChar): static { if (null !== $escapeChar && \strlen($escapeChar) > 1) { - throw new InvalidArgumentException(sprintf('The "%s" escape character must be empty or a single character.', $escapeChar)); + throw new InvalidArgumentException(\sprintf('The "%s" escape character must be empty or a single character.', $escapeChar)); } return $this->with(CsvEncoder::ESCAPE_CHAR_KEY, $escapeChar); diff --git a/Context/Normalizer/AbstractNormalizerContextBuilder.php b/Context/Normalizer/AbstractNormalizerContextBuilder.php index cb5b0544d..a63e1a507 100644 --- a/Context/Normalizer/AbstractNormalizerContextBuilder.php +++ b/Context/Normalizer/AbstractNormalizerContextBuilder.php @@ -87,7 +87,7 @@ public function withAttributes(?array $attributes): static foreach ($it as $attribute) { if (!\is_string($attribute)) { - throw new InvalidArgumentException(sprintf('Each attribute must be a string, "%s" given.', get_debug_type($attribute))); + throw new InvalidArgumentException(\sprintf('Each attribute must be a string, "%s" given.', get_debug_type($attribute))); } } diff --git a/Context/Normalizer/AbstractObjectNormalizerContextBuilder.php b/Context/Normalizer/AbstractObjectNormalizerContextBuilder.php index a27f00c5b..45b47bc1b 100644 --- a/Context/Normalizer/AbstractObjectNormalizerContextBuilder.php +++ b/Context/Normalizer/AbstractObjectNormalizerContextBuilder.php @@ -47,7 +47,7 @@ public function withDepthKeyPattern(?string $depthKeyPattern): static preg_match_all('/(?[a-z])/', $depthKeyPattern, $matches); if (2 !== \count($matches['specifier']) || 's' !== $matches['specifier'][0] || 's' !== $matches['specifier'][1]) { - throw new InvalidArgumentException(sprintf('The depth key pattern "%s" is not valid. You must set exactly two string placeholders.', $depthKeyPattern)); + throw new InvalidArgumentException(\sprintf('The depth key pattern "%s" is not valid. You must set exactly two string placeholders.', $depthKeyPattern)); } return $this->with(AbstractObjectNormalizer::DEPTH_KEY_PATTERN, $depthKeyPattern); diff --git a/Context/Normalizer/DateTimeNormalizerContextBuilder.php b/Context/Normalizer/DateTimeNormalizerContextBuilder.php index e2d289e60..de83b1245 100644 --- a/Context/Normalizer/DateTimeNormalizerContextBuilder.php +++ b/Context/Normalizer/DateTimeNormalizerContextBuilder.php @@ -55,7 +55,7 @@ public function withTimezone(\DateTimeZone|string|null $timezone): static try { $timezone = new \DateTimeZone($timezone); } catch (\Exception $e) { - throw new InvalidArgumentException(sprintf('The "%s" timezone is invalid.', $timezone), previous: $e); + throw new InvalidArgumentException(\sprintf('The "%s" timezone is invalid.', $timezone), previous: $e); } } diff --git a/Context/Normalizer/UidNormalizerContextBuilder.php b/Context/Normalizer/UidNormalizerContextBuilder.php index 1d889e502..b809fe3eb 100644 --- a/Context/Normalizer/UidNormalizerContextBuilder.php +++ b/Context/Normalizer/UidNormalizerContextBuilder.php @@ -33,7 +33,7 @@ final class UidNormalizerContextBuilder implements ContextBuilderInterface public function withNormalizationFormat(?string $normalizationFormat): static { if (null !== $normalizationFormat && !\in_array($normalizationFormat, UidNormalizer::NORMALIZATION_FORMATS, true)) { - throw new InvalidArgumentException(sprintf('The "%s" normalization format is not valid.', $normalizationFormat)); + throw new InvalidArgumentException(\sprintf('The "%s" normalization format is not valid.', $normalizationFormat)); } return $this->with(UidNormalizer::NORMALIZATION_FORMAT_KEY, $normalizationFormat); diff --git a/Context/Normalizer/UnwrappingDenormalizerContextBuilder.php b/Context/Normalizer/UnwrappingDenormalizerContextBuilder.php index 5beb4e984..2945cb961 100644 --- a/Context/Normalizer/UnwrappingDenormalizerContextBuilder.php +++ b/Context/Normalizer/UnwrappingDenormalizerContextBuilder.php @@ -45,7 +45,7 @@ public function withUnwrapPath(?string $unwrapPath): static try { new PropertyPath($unwrapPath); } catch (InvalidPropertyPathException $e) { - throw new InvalidArgumentException(sprintf('The "%s" property path is not valid.', $unwrapPath), previous: $e); + throw new InvalidArgumentException(\sprintf('The "%s" property path is not valid.', $unwrapPath), previous: $e); } return $this->with(UnwrappingDenormalizer::UNWRAP_PATH, $unwrapPath); diff --git a/Debug/TraceableEncoder.php b/Debug/TraceableEncoder.php index 0795d14ca..2d1ca62a5 100644 --- a/Debug/TraceableEncoder.php +++ b/Debug/TraceableEncoder.php @@ -36,7 +36,7 @@ public function __construct( public function encode(mixed $data, string $format, array $context = []): string { if (!$this->encoder instanceof EncoderInterface) { - throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, EncoderInterface::class)); + throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, EncoderInterface::class)); } $startTime = microtime(true); @@ -62,7 +62,7 @@ public function supportsEncoding(string $format, array $context = []): bool public function decode(string $data, string $format, array $context = []): mixed { if (!$this->encoder instanceof DecoderInterface) { - throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, DecoderInterface::class)); + throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, DecoderInterface::class)); } $startTime = microtime(true); diff --git a/Debug/TraceableNormalizer.php b/Debug/TraceableNormalizer.php index 50842a7b9..62e6663da 100644 --- a/Debug/TraceableNormalizer.php +++ b/Debug/TraceableNormalizer.php @@ -42,7 +42,7 @@ public function getSupportedTypes(?string $format): array public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { if (!$this->normalizer instanceof NormalizerInterface) { - throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, NormalizerInterface::class)); + throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, NormalizerInterface::class)); } $startTime = microtime(true); @@ -68,7 +68,7 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed { if (!$this->normalizer instanceof DenormalizerInterface) { - throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, DenormalizerInterface::class)); + throw new \BadMethodCallException(\sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, DenormalizerInterface::class)); } $startTime = microtime(true); diff --git a/Encoder/ChainDecoder.php b/Encoder/ChainDecoder.php index ad182a9a4..8df5cd8b8 100644 --- a/Encoder/ChainDecoder.php +++ b/Encoder/ChainDecoder.php @@ -78,6 +78,6 @@ private function getDecoder(string $format, array $context): DecoderInterface } } - throw new RuntimeException(sprintf('No decoder found for format "%s".', $format)); + throw new RuntimeException(\sprintf('No decoder found for format "%s".', $format)); } } diff --git a/Encoder/ChainEncoder.php b/Encoder/ChainEncoder.php index 5445761f3..3621471b4 100644 --- a/Encoder/ChainEncoder.php +++ b/Encoder/ChainEncoder.php @@ -101,6 +101,6 @@ private function getEncoder(string $format, array $context): EncoderInterface } } - throw new RuntimeException(sprintf('No encoder found for format "%s".', $format)); + throw new RuntimeException(\sprintf('No encoder found for format "%s".', $format)); } } diff --git a/Encoder/CsvEncoder.php b/Encoder/CsvEncoder.php index a3112ae36..462bd663b 100644 --- a/Encoder/CsvEncoder.php +++ b/Encoder/CsvEncoder.php @@ -241,7 +241,7 @@ private function getCsvOptions(array $context): array $asCollection = $context[self::AS_COLLECTION_KEY] ?? $this->defaultContext[self::AS_COLLECTION_KEY]; if (!\is_array($headers)) { - throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, get_debug_type($headers))); + throw new InvalidArgumentException(\sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, get_debug_type($headers))); } return [$delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas, $outputBom, $asCollection]; diff --git a/Encoder/JsonDecode.php b/Encoder/JsonDecode.php index 0dbc9f6df..b50538175 100644 --- a/Encoder/JsonDecode.php +++ b/Encoder/JsonDecode.php @@ -106,7 +106,7 @@ public function decode(string $data, string $format, array $context = []): mixed } if (!class_exists(JsonParser::class)) { - throw new UnsupportedException(sprintf('Enabling "%s" serializer option requires seld/jsonlint. Try running "composer require seld/jsonlint".', self::DETAILED_ERROR_MESSAGES)); + throw new UnsupportedException(\sprintf('Enabling "%s" serializer option requires seld/jsonlint. Try running "composer require seld/jsonlint".', self::DETAILED_ERROR_MESSAGES)); } throw new NotEncodableValueException((new JsonParser())->lint($data)?->getMessage() ?: $errorMessage); diff --git a/Encoder/XmlEncoder.php b/Encoder/XmlEncoder.php index 5dcb2ba7e..a57030b10 100644 --- a/Encoder/XmlEncoder.php +++ b/Encoder/XmlEncoder.php @@ -389,7 +389,7 @@ private function buildXml(\DOMNode $parentNode, mixed $data, string $format, arr if (\is_object($data)) { if (null === $this->serializer) { - throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.', __METHOD__)); + throw new BadMethodCallException(\sprintf('The serializer needs to be set to allow "%s()" to be used with object data.', __METHOD__)); } $data = $this->serializer->normalize($data, $format, $context); @@ -408,7 +408,7 @@ private function buildXml(\DOMNode $parentNode, mixed $data, string $format, arr return $this->appendNode($parentNode, $data, $format, $context, 'data'); } - throw new NotEncodableValueException('An unexpected value could not be serialized: '.(!\is_resource($data) ? var_export($data, true) : sprintf('%s resource', get_resource_type($data)))); + throw new NotEncodableValueException('An unexpected value could not be serialized: '.(!\is_resource($data) ? var_export($data, true) : \sprintf('%s resource', get_resource_type($data)))); } /** @@ -457,7 +457,7 @@ private function selectNodeType(\DOMNode $node, mixed $val, string $format, arra $node->appendChild($child); } elseif (\is_object($val)) { if (null === $this->serializer) { - throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.', __METHOD__)); + throw new BadMethodCallException(\sprintf('The serializer needs to be set to allow "%s()" to be used with object data.', __METHOD__)); } return $this->selectNodeType($node, $this->serializer->normalize($val, $format, $context), $format, $context); diff --git a/Exception/ExtraAttributesException.php b/Exception/ExtraAttributesException.php index 24b031ae3..639808b10 100644 --- a/Exception/ExtraAttributesException.php +++ b/Exception/ExtraAttributesException.php @@ -22,7 +22,7 @@ public function __construct( private readonly array $extraAttributes, ?\Throwable $previous = null, ) { - $msg = sprintf('Extra attributes are not allowed ("%s" %s unknown).', implode('", "', $extraAttributes), \count($extraAttributes) > 1 ? 'are' : 'is'); + $msg = \sprintf('Extra attributes are not allowed ("%s" %s unknown).', implode('", "', $extraAttributes), \count($extraAttributes) > 1 ? 'are' : 'is'); parent::__construct($msg, 0, $previous); } diff --git a/Exception/UnexpectedPropertyException.php b/Exception/UnexpectedPropertyException.php index 4f9ead9a6..03e42fca5 100644 --- a/Exception/UnexpectedPropertyException.php +++ b/Exception/UnexpectedPropertyException.php @@ -22,7 +22,7 @@ public function __construct( public readonly string $property, ?\Throwable $previous = null, ) { - $msg = sprintf('Property is not allowed ("%s" is unknown).', $this->property); + $msg = \sprintf('Property is not allowed ("%s" is unknown).', $this->property); parent::__construct($msg, 0, $previous); } diff --git a/Mapping/Factory/ClassMetadataFactoryCompiler.php b/Mapping/Factory/ClassMetadataFactoryCompiler.php index f01fe9ce2..1e9202b7d 100644 --- a/Mapping/Factory/ClassMetadataFactoryCompiler.php +++ b/Mapping/Factory/ClassMetadataFactoryCompiler.php @@ -57,7 +57,7 @@ private function generateDeclaredClassMetadata(array $classMetadatas): string $classMetadata->getClassDiscriminatorMapping()->getTypesMapping(), ] : null; - $compiled .= sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([ + $compiled .= \sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([ $attributesMetadata, $classDiscriminatorMapping, ])); diff --git a/Mapping/Factory/ClassResolverTrait.php b/Mapping/Factory/ClassResolverTrait.php index 7af722d55..d7037f894 100644 --- a/Mapping/Factory/ClassResolverTrait.php +++ b/Mapping/Factory/ClassResolverTrait.php @@ -31,7 +31,7 @@ private function getClass(object|string $value): string { if (\is_string($value)) { if (!class_exists($value) && !interface_exists($value, false)) { - throw new InvalidArgumentException(sprintf('The class or interface "%s" does not exist.', $value)); + throw new InvalidArgumentException(\sprintf('The class or interface "%s" does not exist.', $value)); } return ltrim($value, '\\'); diff --git a/Mapping/Factory/CompiledClassMetadataFactory.php b/Mapping/Factory/CompiledClassMetadataFactory.php index f4d41c1e6..ec25d7440 100644 --- a/Mapping/Factory/CompiledClassMetadataFactory.php +++ b/Mapping/Factory/CompiledClassMetadataFactory.php @@ -35,7 +35,7 @@ public function __construct( $compiledClassMetadata = require $compiledClassMetadataFile; if (!\is_array($compiledClassMetadata)) { - throw new \RuntimeException(sprintf('Compiled metadata must be of the type array, %s given.', \gettype($compiledClassMetadata))); + throw new \RuntimeException(\sprintf('Compiled metadata must be of the type array, %s given.', \gettype($compiledClassMetadata))); } $this->compiledClassMetadata = $compiledClassMetadata; diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index ddb467164..272e236b6 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -138,7 +138,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool foreach ($this->loadAttributes($method) as $annotation) { if ($annotation instanceof Groups) { if (!$accessorOrMutator) { - throw new MappingException(sprintf('Groups on "%s::%s()" cannot be added. Groups can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); + throw new MappingException(\sprintf('Groups on "%s::%s()" cannot be added. Groups can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } foreach ($annotation->getGroups() as $group) { @@ -146,19 +146,19 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool } } elseif ($annotation instanceof MaxDepth) { if (!$accessorOrMutator) { - throw new MappingException(sprintf('MaxDepth on "%s::%s()" cannot be added. MaxDepth can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); + throw new MappingException(\sprintf('MaxDepth on "%s::%s()" cannot be added. MaxDepth can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } $attributeMetadata->setMaxDepth($annotation->getMaxDepth()); } elseif ($annotation instanceof SerializedName) { if (!$accessorOrMutator) { - throw new MappingException(sprintf('SerializedName on "%s::%s()" cannot be added. SerializedName can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); + throw new MappingException(\sprintf('SerializedName on "%s::%s()" cannot be added. SerializedName can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } $attributeMetadata->setSerializedName($annotation->getSerializedName()); } elseif ($annotation instanceof SerializedPath) { if (!$accessorOrMutator) { - throw new MappingException(sprintf('SerializedPath on "%s::%s()" cannot be added. SerializedPath can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); + throw new MappingException(\sprintf('SerializedPath on "%s::%s()" cannot be added. SerializedPath can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } $attributeMetadata->setSerializedPath($annotation->getSerializedPath()); @@ -168,7 +168,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool } } elseif ($annotation instanceof Context) { if (!$accessorOrMutator) { - throw new MappingException(sprintf('Context on "%s::%s()" cannot be added. Context can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); + throw new MappingException(\sprintf('Context on "%s::%s()" cannot be added. Context can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } $this->setAttributeContextsForGroups($annotation, $attributeMetadata); @@ -193,12 +193,12 @@ private function loadAttributes(\ReflectionMethod|\ReflectionClass|\ReflectionPr } $on = match (true) { $reflector instanceof \ReflectionClass => ' on class '.$reflector->name, - $reflector instanceof \ReflectionMethod => sprintf(' on "%s::%s()"', $reflector->getDeclaringClass()->name, $reflector->name), - $reflector instanceof \ReflectionProperty => sprintf(' on "%s::$%s"', $reflector->getDeclaringClass()->name, $reflector->name), + $reflector instanceof \ReflectionMethod => \sprintf(' on "%s::%s()"', $reflector->getDeclaringClass()->name, $reflector->name), + $reflector instanceof \ReflectionProperty => \sprintf(' on "%s::$%s"', $reflector->getDeclaringClass()->name, $reflector->name), default => '', }; - throw new MappingException(sprintf('Could not instantiate attribute "%s"%s.', $attribute->getName(), $on), 0, $e); + throw new MappingException(\sprintf('Could not instantiate attribute "%s"%s.', $attribute->getName(), $on), 0, $e); } } } diff --git a/Mapping/Loader/FileLoader.php b/Mapping/Loader/FileLoader.php index ccd443b04..ac68d21e8 100644 --- a/Mapping/Loader/FileLoader.php +++ b/Mapping/Loader/FileLoader.php @@ -29,11 +29,11 @@ public function __construct( protected string $file, ) { if (!is_file($file)) { - throw new MappingException(sprintf('The mapping file "%s" does not exist.', $file)); + throw new MappingException(\sprintf('The mapping file "%s" does not exist.', $file)); } if (!is_readable($file)) { - throw new MappingException(sprintf('The mapping file "%s" is not readable.', $file)); + throw new MappingException(\sprintf('The mapping file "%s" is not readable.', $file)); } } } diff --git a/Mapping/Loader/LoaderChain.php b/Mapping/Loader/LoaderChain.php index e93a0a7b0..41abf8cec 100644 --- a/Mapping/Loader/LoaderChain.php +++ b/Mapping/Loader/LoaderChain.php @@ -38,7 +38,7 @@ public function __construct(private readonly array $loaders) { foreach ($loaders as $loader) { if (!$loader instanceof LoaderInterface) { - throw new MappingException(sprintf('Class "%s" is expected to implement LoaderInterface.', get_debug_type($loader))); + throw new MappingException(\sprintf('Class "%s" is expected to implement LoaderInterface.', get_debug_type($loader))); } } } diff --git a/Mapping/Loader/XmlFileLoader.php b/Mapping/Loader/XmlFileLoader.php index b36fb871d..44ba89df1 100644 --- a/Mapping/Loader/XmlFileLoader.php +++ b/Mapping/Loader/XmlFileLoader.php @@ -70,7 +70,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool try { $attributeMetadata->setSerializedPath(new PropertyPath((string) $attribute['serialized-path'])); } catch (InvalidPropertyPathException) { - throw new MappingException(sprintf('The "serialized-path" value must be a valid property path for the attribute "%s" of the class "%s".', $attributeName, $classMetadata->getName())); + throw new MappingException(\sprintf('The "serialized-path" value must be a valid property path for the attribute "%s" of the class "%s".', $attributeName, $classMetadata->getName())); } } diff --git a/Mapping/Loader/YamlFileLoader.php b/Mapping/Loader/YamlFileLoader.php index b0398355a..ca71cbcba 100644 --- a/Mapping/Loader/YamlFileLoader.php +++ b/Mapping/Loader/YamlFileLoader.php @@ -59,12 +59,12 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool if (isset($data['groups'])) { if (!\is_array($data['groups'])) { - throw new MappingException(sprintf('The "groups" key must be an array of strings in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); + throw new MappingException(\sprintf('The "groups" key must be an array of strings in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); } foreach ($data['groups'] as $group) { if (!\is_string($group)) { - throw new MappingException(sprintf('Group names must be strings in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); + throw new MappingException(\sprintf('Group names must be strings in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); } $attributeMetadata->addGroup($group); @@ -73,7 +73,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool if (isset($data['max_depth'])) { if (!\is_int($data['max_depth'])) { - throw new MappingException(sprintf('The "max_depth" value must be an integer in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); + throw new MappingException(\sprintf('The "max_depth" value must be an integer in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); } $attributeMetadata->setMaxDepth($data['max_depth']); @@ -81,7 +81,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool if (isset($data['serialized_name'])) { if (!\is_string($data['serialized_name']) || '' === $data['serialized_name']) { - throw new MappingException(sprintf('The "serialized_name" value must be a non-empty string in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); + throw new MappingException(\sprintf('The "serialized_name" value must be a non-empty string in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); } $attributeMetadata->setSerializedName($data['serialized_name']); @@ -91,13 +91,13 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool try { $attributeMetadata->setSerializedPath(new PropertyPath((string) $data['serialized_path'])); } catch (InvalidPropertyPathException) { - throw new MappingException(sprintf('The "serialized_path" value must be a valid property path in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); + throw new MappingException(\sprintf('The "serialized_path" value must be a valid property path in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); } } if (isset($data['ignore'])) { if (!\is_bool($data['ignore'])) { - throw new MappingException(sprintf('The "ignore" value must be a boolean in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); + throw new MappingException(\sprintf('The "ignore" value must be a boolean in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); } $attributeMetadata->setIgnore($data['ignore']); @@ -124,11 +124,11 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool if (isset($yaml['discriminator_map'])) { if (!isset($yaml['discriminator_map']['type_property'])) { - throw new MappingException(sprintf('The "type_property" key must be set for the discriminator map of the class "%s" in "%s".', $classMetadata->getName(), $this->file)); + throw new MappingException(\sprintf('The "type_property" key must be set for the discriminator map of the class "%s" in "%s".', $classMetadata->getName(), $this->file)); } if (!isset($yaml['discriminator_map']['mapping'])) { - throw new MappingException(sprintf('The "mapping" key must be set for the discriminator map of the class "%s" in "%s".', $classMetadata->getName(), $this->file)); + throw new MappingException(\sprintf('The "mapping" key must be set for the discriminator map of the class "%s" in "%s".', $classMetadata->getName(), $this->file)); } $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( @@ -153,7 +153,7 @@ public function getMappedClasses(): array private function getClassesFromYaml(): array { if (!stream_is_local($this->file)) { - throw new MappingException(sprintf('This is not a local file "%s".', $this->file)); + throw new MappingException(\sprintf('This is not a local file "%s".', $this->file)); } $this->yamlParser ??= new Parser(); @@ -165,7 +165,7 @@ private function getClassesFromYaml(): array } if (!\is_array($classes)) { - throw new MappingException(sprintf('The file "%s" must contain a YAML array.', $this->file)); + throw new MappingException(\sprintf('The file "%s" must contain a YAML array.', $this->file)); } return $classes; diff --git a/NameConverter/MetadataAwareNameConverter.php b/NameConverter/MetadataAwareNameConverter.php index bc693bd90..341e6353b 100644 --- a/NameConverter/MetadataAwareNameConverter.php +++ b/NameConverter/MetadataAwareNameConverter.php @@ -88,7 +88,7 @@ private function getCacheValueForNormalization(string $propertyName, string $cla } if (null !== $attributesMetadata[$propertyName]->getSerializedName() && null !== $attributesMetadata[$propertyName]->getSerializedPath()) { - throw new LogicException(sprintf('Found SerializedName and SerializedPath attributes on property "%s" of class "%s".', $propertyName, $class)); + throw new LogicException(\sprintf('Found SerializedName and SerializedPath attributes on property "%s" of class "%s".', $propertyName, $class)); } return $attributesMetadata[$propertyName]->getSerializedName() ?? null; @@ -132,7 +132,7 @@ private function getCacheValueForAttributesMetadata(string $class, array $contex } if (null !== $metadata->getSerializedName() && null !== $metadata->getSerializedPath()) { - throw new LogicException(sprintf('Found SerializedName and SerializedPath attributes on property "%s" of class "%s".', $name, $class)); + throw new LogicException(\sprintf('Found SerializedName and SerializedPath attributes on property "%s" of class "%s".', $name, $class)); } $metadataGroups = $metadata->getGroups(); diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index 70714ac24..d87bc0597 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -153,7 +153,7 @@ public function __construct( $this->validateCallbackContext($this->defaultContext, 'default'); if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) { - throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER)); + throw new InvalidArgumentException(\sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER)); } } @@ -199,7 +199,7 @@ protected function handleCircularReference(object $object, ?string $format = nul return $circularReferenceHandler($object, $format, $context); } - throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', get_debug_type($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT])); + throw new CircularReferenceException(\sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', get_debug_type($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT])); } /** @@ -216,7 +216,7 @@ protected function getAllowedAttributes(string|object $classOrObject, array $con $allowExtraAttributes = $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES]; if (!$this->classMetadataFactory) { if (!$allowExtraAttributes) { - throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', self::ALLOW_EXTRA_ATTRIBUTES)); + throw new LogicException(\sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', self::ALLOW_EXTRA_ATTRIBUTES)); } return false; @@ -346,7 +346,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex if ($constructorParameter->isVariadic()) { if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) { if (!\is_array($data[$key])) { - throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.', $class, $constructorParameter->name)); + throw new RuntimeException(\sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.', $class, $constructorParameter->name)); } $variadicParameters = []; @@ -399,7 +399,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex } $exception = NotNormalizableValueException::createForUnexpectedDataType( - sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name), + \sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name), null, [$constructorParameterType], $attributeContext['deserialization_path'] ?? null, @@ -410,7 +410,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex } if ($missingConstructorArguments) { - throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$%s".', $class, implode('", "$', $missingConstructorArguments)), 0, null, $missingConstructorArguments, $class); + throw new MissingConstructorArgumentsException(\sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$%s".', $class, implode('", "$', $missingConstructorArguments)), 0, null, $missingConstructorArguments, $class); } if (!$constructor->isConstructor()) { @@ -445,7 +445,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex unset($context['has_constructor']); if (!$reflectionClass->isInstantiable()) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Failed to create object because the class "%s" is not instantiable.', $class), $data, ['unknown'], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Failed to create object because the class "%s" is not instantiable.', $class), $data, ['unknown'], $context['deserialization_path'] ?? null); } return new $class(); @@ -462,13 +462,13 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara new \ReflectionClass($parameterClass); // throws a \ReflectionException if the class doesn't exist if (!$this->serializer instanceof DenormalizerInterface) { - throw new LogicException(sprintf('Cannot create an instance of "%s" from serialized data because the serializer inject in "%s" is not a denormalizer.', $parameterClass, static::class)); + throw new LogicException(\sprintf('Cannot create an instance of "%s" from serialized data because the serializer inject in "%s" is not a denormalizer.', $parameterClass, static::class)); } $parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format)); } } catch (\ReflectionException $e) { - throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e); + throw new RuntimeException(\sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e); } catch (MissingConstructorArgumentsException $e) { if (!$parameter->getType()->allowsNull()) { throw $e; @@ -510,12 +510,12 @@ final protected function validateCallbackContext(array $context, string $context } if (!\is_array($context[self::CALLBACKS])) { - throw new InvalidArgumentException(sprintf('The "%s"%s context option must be an array of callables.', self::CALLBACKS, '' !== $contextType ? " $contextType" : '')); + throw new InvalidArgumentException(\sprintf('The "%s"%s context option must be an array of callables.', self::CALLBACKS, '' !== $contextType ? " $contextType" : '')); } foreach ($context[self::CALLBACKS] as $attribute => $callback) { if (!\is_callable($callback)) { - throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s"%s context option.', $attribute, self::CALLBACKS, '' !== $contextType ? " $contextType" : '')); + throw new InvalidArgumentException(\sprintf('Invalid callback found for attribute "%s" in the "%s"%s context option.', $attribute, self::CALLBACKS, '' !== $contextType ? " $contextType" : '')); } } } diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 27c383177..4a0a84b9c 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -134,7 +134,7 @@ public function __construct( parent::__construct($classMetadataFactory, $nameConverter, $defaultContext); if (isset($this->defaultContext[self::MAX_DEPTH_HANDLER]) && !\is_callable($this->defaultContext[self::MAX_DEPTH_HANDLER])) { - throw new InvalidArgumentException(sprintf('The "%s" given in the default context is not callable.', self::MAX_DEPTH_HANDLER)); + throw new InvalidArgumentException(\sprintf('The "%s" given in the default context is not callable.', self::MAX_DEPTH_HANDLER)); } $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] = array_merge($this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] ?? [], [self::CIRCULAR_REFERENCE_LIMIT_COUNTERS]); @@ -174,7 +174,7 @@ public function normalize(mixed $object, ?string $format = null, array $context if (isset($context[self::MAX_DEPTH_HANDLER])) { $maxDepthHandler = $context[self::MAX_DEPTH_HANDLER]; if (!\is_callable($maxDepthHandler)) { - throw new InvalidArgumentException(sprintf('The "%s" given in the context is not callable.', self::MAX_DEPTH_HANDLER)); + throw new InvalidArgumentException(\sprintf('The "%s" given in the context is not callable.', self::MAX_DEPTH_HANDLER)); } } else { $maxDepthHandler = null; @@ -215,7 +215,7 @@ public function normalize(mixed $object, ?string $format = null, array $context } if (!$this->serializer instanceof NormalizerInterface) { - throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute)); + throw new LogicException(\sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute)); } $childContext = $this->createChildContext($attributeContext, $attribute, $format); @@ -341,7 +341,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $notConverted = $attribute; $attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context); if (isset($nestedData[$notConverted]) && !isset($originalNestedData[$attribute])) { - throw new LogicException(sprintf('Duplicate values for key "%s" found. One value is set via the SerializedPath attribute: "%s", the other one is set via the SerializedName attribute: "%s".', $notConverted, implode('->', $nestedAttributes[$notConverted]->getElements()), $attribute)); + throw new LogicException(\sprintf('Duplicate values for key "%s" found. One value is set via the SerializedPath attribute: "%s", the other one is set via the SerializedName attribute: "%s".', $notConverted, implode('->', $nestedAttributes[$notConverted]->getElements()), $attribute)); } } @@ -394,7 +394,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $this->setAttributeValue($object, $attribute, $value, $format, $attributeContext); } catch (PropertyAccessInvalidArgumentException $e) { $exception = NotNormalizableValueException::createForUnexpectedDataType( - sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $resolvedClass), + \sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $resolvedClass), $data, $e instanceof InvalidTypeException ? [$e->expectedType] : ['unknown'], $attributeContext['deserialization_path'] ?? null, @@ -485,14 +485,14 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass } elseif ('true' === $data || '1' === $data) { $data = true; } else { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null); } break; case LegacyType::BUILTIN_TYPE_INT: if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) { $data = (int) $data; } else { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null); } break; case LegacyType::BUILTIN_TYPE_FLOAT: @@ -504,7 +504,7 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass 'NaN' => \NAN, 'INF' => \INF, '-INF' => -\INF, - default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null), + default => throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null), }; } } @@ -547,7 +547,7 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass if (LegacyType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $class) { if (!$this->serializer instanceof DenormalizerInterface) { - throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); + throw new LogicException(\sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); } $childContext = $this->createChildContext($context, $attribute, $format); @@ -612,7 +612,7 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass return $data; } - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute); } /** @@ -678,14 +678,14 @@ private function validateAndDenormalize(Type $type, string $currentClass, string } elseif ('true' === $data || '1' === $data) { $data = true; } else { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::bool()], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::bool()], $context['deserialization_path'] ?? null); } break; case TypeIdentifier::INT: if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) { $data = (int) $data; } else { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::int()], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::int()], $context['deserialization_path'] ?? null); } break; case TypeIdentifier::FLOAT: @@ -697,7 +697,7 @@ private function validateAndDenormalize(Type $type, string $currentClass, string 'NaN' => \NAN, 'INF' => \INF, '-INF' => -\INF, - default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::float()], $context['deserialization_path'] ?? null), + default => throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::float()], $context['deserialization_path'] ?? null), }; } } @@ -754,7 +754,7 @@ private function validateAndDenormalize(Type $type, string $currentClass, string if (TypeIdentifier::OBJECT === $typeIdentifier && null !== $class) { if (!$this->serializer instanceof DenormalizerInterface) { - throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); + throw new LogicException(\sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); } $childContext = $this->createChildContext($context, $attribute, $format); @@ -828,7 +828,7 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return $data; } - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute); } /** @@ -915,7 +915,7 @@ private function updateData(array $data, string $attribute, mixed $attributeValu if (null !== $classMetadata && null !== $serializedPath = ($attributesMetadata[$attribute] ?? null)?->getSerializedPath()) { $propertyAccessor = PropertyAccess::createPropertyAccessor(); if ($propertyAccessor->isReadable($data, $serializedPath) && null !== $propertyAccessor->getValue($data, $serializedPath)) { - throw new LogicException(sprintf('The element you are trying to set is already populated: "%s".', (string) $serializedPath)); + throw new LogicException(\sprintf('The element you are trying to set is already populated: "%s".', (string) $serializedPath)); } $propertyAccessor->setValue($data, $serializedPath, $attributeValue); @@ -944,7 +944,7 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str return false; } - $key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute); + $key = \sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute); if (!isset($context[$key])) { $context[$key] = 1; @@ -1033,7 +1033,7 @@ private function getNestedAttributes(string $class): array } $pathIdentifier = implode(',', $serializedPath->getElements()); if (isset($serializedPaths[$pathIdentifier])) { - throw new LogicException(sprintf('Duplicate serialized path: "%s" used for properties "%s" and "%s".', $pathIdentifier, $serializedPaths[$pathIdentifier], $name)); + throw new LogicException(\sprintf('Duplicate serialized path: "%s" used for properties "%s" and "%s".', $pathIdentifier, $serializedPaths[$pathIdentifier], $name)); } $serializedPaths[$pathIdentifier] = $name; $properties[$name] = $serializedPath; @@ -1066,11 +1066,11 @@ private function getMappedClass(array $data, string $class, array $context): str } if (null === $type = $data[$mapping->getTypeProperty()] ?? null) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); } if (null === $mappedClass = $mapping->getClassForType($type)) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true); } return $mappedClass; diff --git a/Normalizer/ArrayDenormalizer.php b/Normalizer/ArrayDenormalizer.php index 1bd6c54b3..af5ffb6aa 100644 --- a/Normalizer/ArrayDenormalizer.php +++ b/Normalizer/ArrayDenormalizer.php @@ -48,7 +48,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!'); } if (!\is_array($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, ['array'], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, ['array'], $context['deserialization_path'] ?? null); } if (!str_ends_with($type, '[]')) { throw new InvalidArgumentException('Unsupported class: '.$type); @@ -67,7 +67,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a foreach ($data as $key => $value) { $subContext = $context; - $subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]"; + $subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? \sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]"; $this->validateKeyType($typeIdentifiers, $key, $subContext['deserialization_path']); @@ -80,7 +80,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { if (null === $this->denormalizer) { - throw new BadMethodCallException(sprintf('The nested denormalizer needs to be set to allow "%s()" to be used.', __METHOD__)); + throw new BadMethodCallException(\sprintf('The nested denormalizer needs to be set to allow "%s()" to be used.', __METHOD__)); } return str_ends_with($type, '[]') @@ -102,6 +102,6 @@ private function validateKeyType(array $typeIdentifiers, mixed $key, string $pat } } - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $typeIdentifiers), get_debug_type($key)), $key, $typeIdentifiers, $path, true); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $typeIdentifiers), get_debug_type($key)), $key, $typeIdentifiers, $path, true); } } diff --git a/Normalizer/ConstraintViolationListNormalizer.php b/Normalizer/ConstraintViolationListNormalizer.php index eeb6ab462..eda3b758e 100644 --- a/Normalizer/ConstraintViolationListNormalizer.php +++ b/Normalizer/ConstraintViolationListNormalizer.php @@ -69,7 +69,7 @@ public function normalize(mixed $object, ?string $format = null, array $context 'parameters' => $violation->getParameters(), ]; if (null !== $code = $violation->getCode()) { - $violationEntry['type'] = sprintf('urn:uuid:%s', $code); + $violationEntry['type'] = \sprintf('urn:uuid:%s', $code); } $constraint = $violation->getConstraint(); @@ -85,7 +85,7 @@ public function normalize(mixed $object, ?string $format = null, array $context $violations[] = $violationEntry; - $prefix = $propertyPath ? sprintf('%s: ', $propertyPath) : ''; + $prefix = $propertyPath ? \sprintf('%s: ', $propertyPath) : ''; $messages[] = $prefix.$violation->getMessage(); } diff --git a/Normalizer/DataUriNormalizer.php b/Normalizer/DataUriNormalizer.php index bd1dc822b..f8577f84f 100644 --- a/Normalizer/DataUriNormalizer.php +++ b/Normalizer/DataUriNormalizer.php @@ -68,10 +68,10 @@ public function normalize(mixed $object, ?string $format = null, array $context } if ('text' === explode('/', $mimeType, 2)[0]) { - return sprintf('data:%s,%s', $mimeType, rawurlencode($data)); + return \sprintf('data:%s,%s', $mimeType, rawurlencode($data)); } - return sprintf('data:%s;base64,%s', $mimeType, base64_encode($data)); + return \sprintf('data:%s;base64,%s', $mimeType, base64_encode($data)); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool @@ -97,7 +97,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a switch ($type) { case File::class: if (!class_exists(File::class)) { - throw new InvalidArgumentException(sprintf('Cannot denormalize to a "%s" without the HttpFoundation component installed. Try running "composer require symfony/http-foundation".', File::class)); + throw new InvalidArgumentException(\sprintf('Cannot denormalize to a "%s" without the HttpFoundation component installed. Try running "composer require symfony/http-foundation".', File::class)); } return new File($data, false); @@ -110,7 +110,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a throw NotNormalizableValueException::createForUnexpectedDataType($exception->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, false, $exception->getCode(), $exception); } - throw new InvalidArgumentException(sprintf('The class parameter "%s" is not supported. It must be one of "SplFileInfo", "SplFileObject" or "Symfony\Component\HttpFoundation\File\File".', $type)); + throw new InvalidArgumentException(\sprintf('The class parameter "%s" is not supported. It must be one of "SplFileInfo", "SplFileObject" or "Symfony\Component\HttpFoundation\File\File".', $type)); } public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool diff --git a/Normalizer/DateIntervalNormalizer.php b/Normalizer/DateIntervalNormalizer.php index 995d4f6a3..05d1a8529 100644 --- a/Normalizer/DateIntervalNormalizer.php +++ b/Normalizer/DateIntervalNormalizer.php @@ -85,7 +85,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } $valuePattern = '/^'.$signPattern.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?:(?P<$1>\d+)$2)?', preg_replace('/(T.*)$/', '($1)?', $dateIntervalFormat)).'$/'; if (!preg_match($valuePattern, $data)) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Value "%s" contains intervals not accepted by format "%s".', $data, $dateIntervalFormat), $data, ['string'], $context['deserialization_path'] ?? null, false); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Value "%s" contains intervals not accepted by format "%s".', $data, $dateIntervalFormat), $data, ['string'], $context['deserialization_path'] ?? null, false); } try { diff --git a/Normalizer/DateTimeNormalizer.php b/Normalizer/DateTimeNormalizer.php index 71ce26496..fc32f6f50 100644 --- a/Normalizer/DateTimeNormalizer.php +++ b/Normalizer/DateTimeNormalizer.php @@ -94,10 +94,10 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a if (\is_int($data) || \is_float($data)) { switch ($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY] ?? null) { case 'U': - $data = sprintf('%d', $data); + $data = \sprintf('%d', $data); break; case 'U.u': - $data = sprintf('%.6F', $data); + $data = \sprintf('%.6F', $data); break; } } @@ -121,7 +121,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $dateTimeErrors = $type::getLastErrors(); - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, ['string'], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, ['string'], $context['deserialization_path'] ?? null, true); } $defaultDateTimeFormat = $this->defaultContext[self::FORMAT_KEY] ?? null; @@ -155,7 +155,7 @@ private function formatDateTimeErrors(array $errors): array $formattedErrors = []; foreach ($errors as $pos => $message) { - $formattedErrors[] = sprintf('at position %d: %s', $pos, $message); + $formattedErrors[] = \sprintf('at position %d: %s', $pos, $message); } return $formattedErrors; diff --git a/Normalizer/JsonSerializableNormalizer.php b/Normalizer/JsonSerializableNormalizer.php index 1287627fb..31c224175 100644 --- a/Normalizer/JsonSerializableNormalizer.php +++ b/Normalizer/JsonSerializableNormalizer.php @@ -28,7 +28,7 @@ public function normalize(mixed $object, ?string $format = null, array $context } if (!$object instanceof \JsonSerializable) { - throw new InvalidArgumentException(sprintf('The object must implement "%s".', \JsonSerializable::class)); + throw new InvalidArgumentException(\sprintf('The object must implement "%s".', \JsonSerializable::class)); } if (!$this->serializer instanceof NormalizerInterface) { @@ -57,6 +57,6 @@ public function supportsDenormalization(mixed $data, string $type, ?string $form public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed { - throw new LogicException(sprintf('Cannot denormalize with "%s".', \JsonSerializable::class)); + throw new LogicException(\sprintf('Cannot denormalize with "%s".', \JsonSerializable::class)); } } diff --git a/Normalizer/MimeMessageNormalizer.php b/Normalizer/MimeMessageNormalizer.php index 7006ab321..633edf369 100644 --- a/Normalizer/MimeMessageNormalizer.php +++ b/Normalizer/MimeMessageNormalizer.php @@ -56,7 +56,7 @@ public function getSupportedTypes(?string $format): array public function setSerializer(SerializerInterface $serializer): void { if (!$serializer instanceof NormalizerInterface || !$serializer instanceof DenormalizerInterface) { - throw new LogicException(sprintf('The passed serializer should implement both NormalizerInterface and DenormalizerInterface, "%s" given.', get_debug_type($serializer))); + throw new LogicException(\sprintf('The passed serializer should implement both NormalizerInterface and DenormalizerInterface, "%s" given.', get_debug_type($serializer))); } $this->serializer = $serializer; $this->normalizer->setSerializer($serializer); diff --git a/Normalizer/ProblemNormalizer.php b/Normalizer/ProblemNormalizer.php index 04c680647..08aca6796 100644 --- a/Normalizer/ProblemNormalizer.php +++ b/Normalizer/ProblemNormalizer.php @@ -54,7 +54,7 @@ public function getSupportedTypes(?string $format): array public function normalize(mixed $object, ?string $format = null, array $context = []): array { if (!$object instanceof FlattenException) { - throw new InvalidArgumentException(sprintf('The object must implement "%s".', FlattenException::class)); + throw new InvalidArgumentException(\sprintf('The object must implement "%s".', FlattenException::class)); } $data = []; diff --git a/Normalizer/PropertyNormalizer.php b/Normalizer/PropertyNormalizer.php index e1d893be8..1619f35bf 100644 --- a/Normalizer/PropertyNormalizer.php +++ b/Normalizer/PropertyNormalizer.php @@ -162,7 +162,7 @@ protected function getAttributeValue(object $object, string $attribute, ?string || ($reflectionProperty->isProtected() && !\array_key_exists("\0*\0{$reflectionProperty->name}", $propertyValues)) || ($reflectionProperty->isPrivate() && !\array_key_exists("\0{$reflectionProperty->class}\0{$reflectionProperty->name}", $propertyValues)) ) { - throw new UninitializedPropertyException(sprintf('The property "%s::$%s" is not initialized.', $object::class, $reflectionProperty->name)); + throw new UninitializedPropertyException(\sprintf('The property "%s::$%s" is not initialized.', $object::class, $reflectionProperty->name)); } } diff --git a/Normalizer/TranslatableNormalizer.php b/Normalizer/TranslatableNormalizer.php index 398e00b79..463616e72 100644 --- a/Normalizer/TranslatableNormalizer.php +++ b/Normalizer/TranslatableNormalizer.php @@ -37,7 +37,7 @@ public function __construct( public function normalize(mixed $object, ?string $format = null, array $context = []): string { if (!$object instanceof TranslatableInterface) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The object must implement the "%s".', TranslatableInterface::class), $object, [TranslatableInterface::class]); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The object must implement the "%s".', TranslatableInterface::class), $object, [TranslatableInterface::class]); } return $object->trans($this->translator, $context[self::NORMALIZATION_LOCALE_KEY] ?? $this->defaultContext[self::NORMALIZATION_LOCALE_KEY]); diff --git a/Normalizer/UidNormalizer.php b/Normalizer/UidNormalizer.php index a6cc190a9..b107c9d36 100644 --- a/Normalizer/UidNormalizer.php +++ b/Normalizer/UidNormalizer.php @@ -56,7 +56,7 @@ public function normalize(mixed $object, ?string $format = null, array $context self::NORMALIZATION_FORMAT_BASE58 => $object->toBase58(), self::NORMALIZATION_FORMAT_BASE32 => $object->toBase32(), self::NORMALIZATION_FORMAT_RFC4122 => $object->toRfc4122(), - default => throw new LogicException(sprintf('The "%s" format is not valid.', $context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY])), + default => throw new LogicException(\sprintf('The "%s" format is not valid.', $context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY])), }; } @@ -70,7 +70,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a try { return $type::fromString($data); } catch (\InvalidArgumentException|\TypeError) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The data is not a valid "%s" string representation.', $type), $data, ['string'], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The data is not a valid "%s" string representation.', $type), $data, ['string'], $context['deserialization_path'] ?? null, true); } } diff --git a/Serializer.php b/Serializer.php index 9fba20f14..b907a5186 100644 --- a/Serializer.php +++ b/Serializer.php @@ -94,7 +94,7 @@ public function __construct( } if (!($normalizer instanceof NormalizerInterface || $normalizer instanceof DenormalizerInterface)) { - throw new InvalidArgumentException(sprintf('The class "%s" neither implements "%s" nor "%s".', get_debug_type($normalizer), NormalizerInterface::class, DenormalizerInterface::class)); + throw new InvalidArgumentException(\sprintf('The class "%s" neither implements "%s" nor "%s".', get_debug_type($normalizer), NormalizerInterface::class, DenormalizerInterface::class)); } } @@ -112,7 +112,7 @@ public function __construct( } if (!($encoder instanceof EncoderInterface || $encoder instanceof DecoderInterface)) { - throw new InvalidArgumentException(sprintf('The class "%s" neither implements "%s" nor "%s".', get_debug_type($encoder), EncoderInterface::class, DecoderInterface::class)); + throw new InvalidArgumentException(\sprintf('The class "%s" neither implements "%s" nor "%s".', get_debug_type($encoder), EncoderInterface::class, DecoderInterface::class)); } } $this->encoder = new ChainEncoder($realEncoders); @@ -122,7 +122,7 @@ public function __construct( final public function serialize(mixed $data, string $format, array $context = []): string { if (!$this->supportsEncoding($format, $context)) { - throw new UnsupportedFormatException(sprintf('Serialization for the format "%s" is not supported.', $format)); + throw new UnsupportedFormatException(\sprintf('Serialization for the format "%s" is not supported.', $format)); } if ($this->encoder->needsNormalization($format, $context)) { @@ -135,7 +135,7 @@ final public function serialize(mixed $data, string $format, array $context = [] final public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed { if (!$this->supportsDecoding($format, $context)) { - throw new UnsupportedFormatException(sprintf('Deserialization for the format "%s" is not supported.', $format)); + throw new UnsupportedFormatException(\sprintf('Deserialization for the format "%s" is not supported.', $format)); } $data = $this->decode($data, $format, $context); @@ -176,10 +176,10 @@ public function normalize(mixed $data, ?string $format = null, array $context = throw new LogicException('You must register at least one normalizer to be able to normalize objects.'); } - throw new NotNormalizableValueException(sprintf('Could not normalize object of type "%s", no supporting normalizer found.', get_debug_type($data))); + throw new NotNormalizableValueException(\sprintf('Could not normalize object of type "%s", no supporting normalizer found.', get_debug_type($data))); } - throw new NotNormalizableValueException('An unexpected value could not be normalized: '.(!\is_resource($data) ? var_export($data, true) : sprintf('"%s" resource', get_resource_type($data)))); + throw new NotNormalizableValueException('An unexpected value could not be normalized: '.(!\is_resource($data) ? var_export($data, true) : \sprintf('"%s" resource', get_resource_type($data)))); } /** @@ -197,7 +197,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a // Check for a denormalizer first, e.g. the data is wrapped if (!$normalizer && isset(self::SCALAR_TYPES[$type])) { if (!('is_'.$type)($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be of type "%s" ("%s" given).', $type, get_debug_type($data)), $data, [$type], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Data expected to be of type "%s" ("%s" given).', $type, get_debug_type($data)), $data, [$type], $context['deserialization_path'] ?? null, true); } return $data; @@ -208,7 +208,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } if (!$normalizer) { - throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type)); + throw new NotNormalizableValueException(\sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type)); } if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) { diff --git a/Tests/Annotation/ContextTest.php b/Tests/Annotation/ContextTest.php index 9584d6f1b..84ff41b8a 100644 --- a/Tests/Annotation/ContextTest.php +++ b/Tests/Annotation/ContextTest.php @@ -40,7 +40,7 @@ public function testThrowsOnEmptyContext() public function testInvalidGroupOption() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage(sprintf('Parameter "groups" given to "%s" must be a string or an array of strings, "stdClass" given', Context::class)); + $this->expectExceptionMessage(\sprintf('Parameter "groups" given to "%s" must be a string or an array of strings, "stdClass" given', Context::class)); new Context(context: ['foo' => 'bar'], groups: ['fine', new \stdClass()]); } diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index 5be6be232..911637620 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -1050,14 +1050,14 @@ private function createMockDateTimeNormalizer(): MockObject&NormalizerInterface private function createXmlWithDateTime(): string { - return sprintf(' + return \sprintf(' %s ', $this->exampleDateTimeString); } private function createXmlWithDateTimeField(): string { - return sprintf(' + return \sprintf(' ', $this->exampleDateTimeString); } diff --git a/Tests/Mapping/Loader/AttributeLoaderTest.php b/Tests/Mapping/Loader/AttributeLoaderTest.php index a76595506..5b6ef3de7 100644 --- a/Tests/Mapping/Loader/AttributeLoaderTest.php +++ b/Tests/Mapping/Loader/AttributeLoaderTest.php @@ -162,7 +162,7 @@ public function testLoadContextsPropertiesPromoted() public function testThrowsOnContextOnInvalidMethod() { $this->expectException(MappingException::class); - $this->expectExceptionMessage(sprintf('Context on "%s::badMethod()" cannot be added', BadMethodContextDummy::class)); + $this->expectExceptionMessage(\sprintf('Context on "%s::badMethod()" cannot be added', BadMethodContextDummy::class)); $loader = $this->getLoaderForContextMapping(); diff --git a/Tests/Normalizer/Features/CircularReferenceTestTrait.php b/Tests/Normalizer/Features/CircularReferenceTestTrait.php index d02e245f7..85720bcfe 100644 --- a/Tests/Normalizer/Features/CircularReferenceTestTrait.php +++ b/Tests/Normalizer/Features/CircularReferenceTestTrait.php @@ -42,7 +42,7 @@ public function testUnableToNormalizeCircularReference(array $defaultContext, ar $obj = $this->getSelfReferencingModel(); $this->expectException(CircularReferenceException::class); - $this->expectExceptionMessage(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', $obj::class, $expectedLimit)); + $this->expectExceptionMessage(\sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', $obj::class, $expectedLimit)); $normalizer->normalize($obj, null, $context); } diff --git a/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php b/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php index 72652f340..0a5f6f249 100644 --- a/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php +++ b/Tests/Normalizer/Features/ConstructorArgumentsTestTrait.php @@ -64,10 +64,10 @@ public function testConstructorWithMissingData() $normalizer = $this->getDenormalizerForConstructArguments(); try { $normalizer->denormalize($data, ConstructorArgumentsObject::class); - self::fail(sprintf('Failed asserting that exception of type "%s" is thrown.', MissingConstructorArgumentsException::class)); + self::fail(\sprintf('Failed asserting that exception of type "%s" is thrown.', MissingConstructorArgumentsException::class)); } catch (MissingConstructorArgumentsException $e) { self::assertSame(ConstructorArgumentsObject::class, $e->getClass()); - self::assertSame(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$foo", "$baz".', ConstructorArgumentsObject::class), $e->getMessage()); + self::assertSame(\sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$foo", "$baz".', ConstructorArgumentsObject::class), $e->getMessage()); self::assertSame(['foo', 'baz'], $e->getMissingConstructorArguments()); } } diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 19bbcde2c..8c564f003 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -786,12 +786,12 @@ public function testAdvancedNameConverter() $nameConverter = new class() implements AdvancedNameConverterInterface { public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { - return sprintf('%s-%s-%s-%s', $propertyName, $class, $format, $context['foo']); + return \sprintf('%s-%s-%s-%s', $propertyName, $class, $format, $context['foo']); } public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { - return sprintf('%s-%s-%s-%s', $propertyName, $class, $format, $context['foo']); + return \sprintf('%s-%s-%s-%s', $propertyName, $class, $format, $context['foo']); } }; From 289be0b7bf80214234d4f2d804d6c037e7b4ca50 Mon Sep 17 00:00:00 2001 From: hbgamra Date: Fri, 21 Jun 2024 14:38:03 +0200 Subject: [PATCH 035/104] Update ClassMetadata.php Optimize the getReflectionClass() function --- Mapping/ClassMetadata.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Mapping/ClassMetadata.php b/Mapping/ClassMetadata.php index 5cec14c91..179e6050d 100644 --- a/Mapping/ClassMetadata.php +++ b/Mapping/ClassMetadata.php @@ -78,11 +78,7 @@ public function merge(ClassMetadataInterface $classMetadata): void public function getReflectionClass(): \ReflectionClass { - if (!$this->reflClass) { - $this->reflClass = new \ReflectionClass($this->getName()); - } - - return $this->reflClass; + return $this->reflClass ??= new \ReflectionClass($this->getName()); } public function getClassDiscriminatorMapping(): ?ClassDiscriminatorMapping From 898e452d5d5d8a0c3175151e244b30c5b4fd6f4a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 21 Jun 2024 16:02:45 +0200 Subject: [PATCH 036/104] [Serializer] Use `SUPPORTED_TYPES` in Normalizers when available --- Normalizer/DataUriNormalizer.php | 6 +----- Normalizer/DateTimeNormalizer.php | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/Normalizer/DataUriNormalizer.php b/Normalizer/DataUriNormalizer.php index f8577f84f..5ee076be6 100644 --- a/Normalizer/DataUriNormalizer.php +++ b/Normalizer/DataUriNormalizer.php @@ -44,11 +44,7 @@ public function __construct(?MimeTypeGuesserInterface $mimeTypeGuesser = null) public function getSupportedTypes(?string $format): array { - return [ - \SplFileInfo::class => true, - \SplFileObject::class => true, - File::class => true, - ]; + return self::SUPPORTED_TYPES; } public function normalize(mixed $object, ?string $format = null, array $context = []): string diff --git a/Normalizer/DateTimeNormalizer.php b/Normalizer/DateTimeNormalizer.php index fc32f6f50..55b2e130e 100644 --- a/Normalizer/DateTimeNormalizer.php +++ b/Normalizer/DateTimeNormalizer.php @@ -50,11 +50,7 @@ public function setDefaultContext(array $defaultContext): void public function getSupportedTypes(?string $format): array { - return [ - \DateTimeInterface::class => true, - \DateTimeImmutable::class => true, - \DateTime::class => true, - ]; + return self::SUPPORTED_TYPES; } /** From f78926de1d52c57a27e72f4c3ebd1fe0ef7efc70 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 16 Jun 2024 17:17:26 +0200 Subject: [PATCH 037/104] chore: CS fixes --- Normalizer/BackedEnumNormalizer.php | 2 +- Normalizer/GetSetMethodNormalizer.php | 2 +- Normalizer/ObjectNormalizer.php | 2 +- Tests/Encoder/XmlEncoderTest.php | 12 +++--- .../ConstraintViolationListNormalizerTest.php | 30 +++++++-------- Tests/Normalizer/PropertyNormalizerTest.php | 37 ++++++++++++++----- Tests/Normalizer/UidNormalizerTest.php | 4 +- Tests/SerializerTest.php | 6 +-- 8 files changed, 56 insertions(+), 39 deletions(-) diff --git a/Normalizer/BackedEnumNormalizer.php b/Normalizer/BackedEnumNormalizer.php index 504047bc1..3d8e7e7c5 100644 --- a/Normalizer/BackedEnumNormalizer.php +++ b/Normalizer/BackedEnumNormalizer.php @@ -29,7 +29,7 @@ final class BackedEnumNormalizer implements NormalizerInterface, DenormalizerInt public function getSupportedTypes(?string $format): array { return [ - \BackedEnum::class => true, + \BackedEnum::class => true, ]; } diff --git a/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index 3a398ef33..61fdc3b4d 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -164,7 +164,7 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string return false; } - $class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject; + $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject; if (!isset(self::$reflectionCache[$class])) { self::$reflectionCache[$class] = new \ReflectionClass($class); diff --git a/Normalizer/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index 1b51b729c..10fe403eb 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -173,7 +173,7 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string return false; } - $class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject; + $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject; if ($context['_read_attributes'] ?? true) { if (!isset(self::$isReadableCache[$class.$attribute])) { diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index 911637620..5fc8fe983 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -671,8 +671,8 @@ public function testDecodeIgnoreComments() XML; $expected = ['person' => [ - ['firstname' => 'Benjamin', 'lastname' => 'Alexandre'], - ['firstname' => 'Damien', 'lastname' => 'Clay'], + ['firstname' => 'Benjamin', 'lastname' => 'Alexandre'], + ['firstname' => 'Damien', 'lastname' => 'Clay'], ]]; $this->assertEquals($expected, $this->encoder->decode($source, 'xml')); @@ -695,8 +695,8 @@ public function testDecodeIgnoreDocumentType() XML; $expected = ['person' => [ - ['firstname' => 'Benjamin', 'lastname' => 'Alexandre'], - ['firstname' => 'Damien', 'lastname' => 'Clay'], + ['firstname' => 'Benjamin', 'lastname' => 'Alexandre'], + ['firstname' => 'Damien', 'lastname' => 'Clay'], ]]; $this->assertEquals($expected, $this->encoder->decode( $source, @@ -730,8 +730,8 @@ public function testDecodePreserveComments() $this->encoder->setSerializer($serializer); $expected = ['person' => [ - ['firstname' => 'Benjamin', 'lastname' => 'Alexandre', '#comment' => ' This comment should be decoded. '], - ['firstname' => 'Damien', 'lastname' => 'Clay'], + ['firstname' => 'Benjamin', 'lastname' => 'Alexandre', '#comment' => ' This comment should be decoded. '], + ['firstname' => 'Damien', 'lastname' => 'Clay'], ]]; $this->assertEquals($expected, $this->encoder->decode($source, 'xml')); diff --git a/Tests/Normalizer/ConstraintViolationListNormalizerTest.php b/Tests/Normalizer/ConstraintViolationListNormalizerTest.php index bb69392f5..1d0afb3cb 100644 --- a/Tests/Normalizer/ConstraintViolationListNormalizerTest.php +++ b/Tests/Normalizer/ConstraintViolationListNormalizerTest.php @@ -50,23 +50,23 @@ public function testNormalize() 'detail' => 'd: a 4: 1', 'violations' => [ - [ - 'propertyPath' => 'd', - 'title' => 'a', - 'template' => 'b', - 'type' => 'urn:uuid:f', - 'parameters' => [ - 'value' => 'foo', - ], - ], - [ - 'propertyPath' => '4', - 'title' => '1', - 'template' => '2', - 'type' => 'urn:uuid:6', - 'parameters' => [], + [ + 'propertyPath' => 'd', + 'title' => 'a', + 'template' => 'b', + 'type' => 'urn:uuid:f', + 'parameters' => [ + 'value' => 'foo', ], ], + [ + 'propertyPath' => '4', + 'title' => '1', + 'template' => '2', + 'type' => 'urn:uuid:6', + 'parameters' => [], + ], + ], ]; $this->assertEquals($expected, $this->normalizer->normalize($list)); diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index b93a7bb9f..9773e65b7 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -340,12 +340,16 @@ public function testGroupsDenormalizeWithNameConverter() $this->assertEquals( $obj, - $this->normalizer->denormalize([ - 'bar' => null, - 'foo_bar' => '@dunglas', - 'symfony' => '@coopTilleuls', - 'coop_tilleuls' => 'les-tilleuls.coop', - ], GroupDummy::class, null, [PropertyNormalizer::GROUPS => ['name_converter']]) + $this->normalizer->denormalize( + [ + 'bar' => null, + 'foo_bar' => '@dunglas', + 'symfony' => '@coopTilleuls', + 'coop_tilleuls' => 'les-tilleuls.coop', + ], + GroupDummy::class, null, + [PropertyNormalizer::GROUPS => ['name_converter']] + ) ); } @@ -405,13 +409,19 @@ public function testDenormalizeNonExistingAttribute() { $this->assertEquals( new PropertyDummy(), - $this->normalizer->denormalize(['non_existing' => true], PropertyDummy::class) + $this->normalizer->denormalize( + ['non_existing' => true], + PropertyDummy::class + ) ); } public function testDenormalizeShouldIgnoreStaticProperty() { - $obj = $this->normalizer->denormalize(['outOfScope' => true], PropertyDummy::class); + $obj = $this->normalizer->denormalize( + ['outOfScope' => true], + PropertyDummy::class + ); $this->assertEquals(new PropertyDummy(), $obj); $this->assertEquals('out_of_scope', PropertyDummy::$outOfScope); @@ -450,7 +460,8 @@ public function testInheritedPropertiesSupport() public function testMultiDimensionObject() { $normalizer = $this->getDenormalizerForTypeEnforcement(); - $root = $normalizer->denormalize([ + $root = $normalizer->denormalize( + [ 'children' => [[ ['foo' => 'one', 'bar' => 'two'], ['foo' => 'three', 'bar' => 'four'], @@ -535,7 +546,13 @@ public function testDenormalizeWithDiscriminator() $denormalized = new PropertyDiscriminatedDummyTwo(); $denormalized->url = 'url'; - $this->assertEquals($denormalized, $normalizer->denormalize(['type' => 'two', 'url' => 'url'], PropertyDummyInterface::class)); + $this->assertEquals( + $denormalized, + $normalizer->denormalize( + ['type' => 'two', 'url' => 'url'], + PropertyDummyInterface::class + ) + ); } } diff --git a/Tests/Normalizer/UidNormalizerTest.php b/Tests/Normalizer/UidNormalizerTest.php index 1471074a0..734b15b48 100644 --- a/Tests/Normalizer/UidNormalizerTest.php +++ b/Tests/Normalizer/UidNormalizerTest.php @@ -47,8 +47,8 @@ public static function normalizeProvider() { $uidFormats = [null, 'canonical', 'base58', 'base32', 'rfc4122']; $data = [ - [ - UuidV1::fromString('9b7541de-6f87-11ea-ab3c-9da9a81562fc'), + [ + UuidV1::fromString('9b7541de-6f87-11ea-ab3c-9da9a81562fc'), '9b7541de-6f87-11ea-ab3c-9da9a81562fc', '9b7541de-6f87-11ea-ab3c-9da9a81562fc', 'LCQS8f2p5SDSiAt9V7ZYnF', diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 1bf5905ad..7d4cbc30d 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -1214,7 +1214,7 @@ public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMe 'useMessageForUser' => false, 'message' => 'The type of the "string" attribute for class "Symfony\\Component\\Serializer\\Tests\\Fixtures\\Php74Full" must be one of "string" ("null" given).', ], - ]; + ]; $this->assertSame($expected, $exceptionsAsArray); } @@ -1464,8 +1464,8 @@ public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruc try { $serializer->deserialize('{"get": "POST"}', DummyObjectWithEnumProperty::class, 'json', [ - DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, - ]); + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); } catch (\Throwable $e) { $this->assertInstanceOf(PartialDenormalizationException::class, $e); } From 62660267ad33e7cd9677a20cec7acc39937a58a2 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 26 Jun 2024 09:40:14 +0200 Subject: [PATCH 038/104] [Serializer] Fix access to wrong Type class --- Normalizer/AbstractObjectNormalizer.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 2d0671359..16cf59e8b 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -571,16 +571,16 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass } switch ($builtinType) { - case Type::BUILTIN_TYPE_ARRAY: - case Type::BUILTIN_TYPE_BOOL: - case Type::BUILTIN_TYPE_CALLABLE: - case Type::BUILTIN_TYPE_FLOAT: - case Type::BUILTIN_TYPE_INT: - case Type::BUILTIN_TYPE_ITERABLE: - case Type::BUILTIN_TYPE_NULL: - case Type::BUILTIN_TYPE_OBJECT: - case Type::BUILTIN_TYPE_RESOURCE: - case Type::BUILTIN_TYPE_STRING: + case LegacyType::BUILTIN_TYPE_ARRAY: + case LegacyType::BUILTIN_TYPE_BOOL: + case LegacyType::BUILTIN_TYPE_CALLABLE: + case LegacyType::BUILTIN_TYPE_FLOAT: + case LegacyType::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_ITERABLE: + case LegacyType::BUILTIN_TYPE_NULL: + case LegacyType::BUILTIN_TYPE_OBJECT: + case LegacyType::BUILTIN_TYPE_RESOURCE: + case LegacyType::BUILTIN_TYPE_STRING: if (('is_'.$builtinType)($data)) { return $data; } From 7d12e1a685349c87ae095461a92fe95ec3eed781 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Thu, 27 Jun 2024 18:09:31 +0200 Subject: [PATCH 039/104] [Serializer] Check if exception message in test is correct --- Tests/Fixtures/NotNormalizableDummy.php | 2 +- Tests/Normalizer/AbstractObjectNormalizerTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/Fixtures/NotNormalizableDummy.php b/Tests/Fixtures/NotNormalizableDummy.php index 41da0eac8..ef7a8c906 100644 --- a/Tests/Fixtures/NotNormalizableDummy.php +++ b/Tests/Fixtures/NotNormalizableDummy.php @@ -26,6 +26,6 @@ public function __construct() public function denormalize(DenormalizerInterface $denormalizer, $data, ?string $format = null, array $context = []): void { - throw new NotNormalizableValueException(); + throw new NotNormalizableValueException('Custom exception message'); } } diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index c5c1f6f0b..6f6256506 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -946,6 +946,7 @@ public function testDenormalizeUntypedFormat() public function testDenormalizeUntypedFormatNotNormalizable() { $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Custom exception message'); $serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); $serializer->denormalize(['value' => 'test'], DummyWithNotNormalizable::class, 'xml'); } From a61c583ee08b7e4d219cb0c8dc6b623fb56182bd Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 17 Jun 2024 23:31:30 +0200 Subject: [PATCH 040/104] forward exceptions caught in the AbstractObjectNormalizer --- Normalizer/AbstractObjectNormalizer.php | 30 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 16cf59e8b..e15c89e82 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -640,8 +640,11 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; + $isUnionType = $type->asNonNullable() instanceof UnionType; + $e = null; $extraAttributesException = null; $missingConstructorArgumentsException = null; + $isNullable = false; $types = match (true) { $type instanceof IntersectionType => throw new LogicException('Unable to handle intersection type.'), @@ -679,12 +682,19 @@ private function validateAndDenormalize(Type $type, string $currentClass, string // That's why we have to transform the values, if one of these non-string basic datatypes is expected. $typeIdentifier = $t->getTypeIdentifier(); if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { + if ('' === $data) { + if (TypeIdentifier::ARRAY === $typeIdentifier) { + return []; + } + + if (TypeIdentifier::STRING === $typeIdentifier) { + return ''; + } + + $isNullable = $isNullable ?: $type->isNullable(); + } + switch ($typeIdentifier) { - case TypeIdentifier::ARRAY: - if ('' === $data) { - return []; - } - break; case TypeIdentifier::BOOL: // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" if ('false' === $data || '0' === $data) { @@ -808,17 +818,17 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return $data; } } catch (NotNormalizableValueException|InvalidArgumentException $e) { - if (!$type instanceof UnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } } catch (ExtraAttributesException $e) { - if (!$type instanceof UnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } $extraAttributesException ??= $e; } catch (MissingConstructorArgumentsException $e) { - if (!$type instanceof UnionType) { + if (!$isUnionType && !$isNullable) { throw $e; } @@ -838,6 +848,10 @@ private function validateAndDenormalize(Type $type, string $currentClass, string throw $missingConstructorArgumentsException; } + if (!$isUnionType && $e) { + throw $e; + } + if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { return $data; } From 59699189f2136f6fdd9b485ad2d1f1b28c12adf6 Mon Sep 17 00:00:00 2001 From: Maximilian Zumbansen Date: Wed, 26 Jun 2024 14:46:59 +0200 Subject: [PATCH 041/104] [Serializer] [ObjectNormalizer] Use bool filter when FILTER_BOOL is set --- Normalizer/AbstractObjectNormalizer.php | 8 +++++ .../AbstractObjectNormalizerTest.php | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 2d0671359..ab9568457 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -566,6 +566,10 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass return (float) $data; } + if (LegacyType::BUILTIN_TYPE_BOOL === $builtinType && \is_string($data) && ($context[self::FILTER_BOOL] ?? false)) { + return filter_var($data, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE); + } + if ((LegacyType::BUILTIN_TYPE_FALSE === $builtinType && false === $data) || (LegacyType::BUILTIN_TYPE_TRUE === $builtinType && true === $data)) { return $data; } @@ -787,6 +791,10 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return (float) $data; } + if (TypeIdentifier::BOOL === $typeIdentifier && \is_string($data) && ($context[self::FILTER_BOOL] ?? false)) { + return filter_var($data, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE); + } + $dataMatchesExpectedType = match ($typeIdentifier) { TypeIdentifier::ARRAY => \is_array($data), TypeIdentifier::BOOL => \is_bool($data), diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index c5c1f6f0b..90e6be952 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1195,6 +1195,34 @@ public function provideBooleanTypesData() [['foo' => false], TruePropertyDummy::class], ]; } + + /** + * @dataProvider provideDenormalizeWithFilterBoolData + */ + public function testDenormalizeBooleanTypeWithFilterBool(array $data, ?bool $expectedFoo) + { + $normalizer = new AbstractObjectNormalizerWithMetadataAndPropertyTypeExtractors(); + + $dummy = $normalizer->denormalize($data, BoolPropertyDummy::class, null, [AbstractNormalizer::FILTER_BOOL => true]); + + $this->assertSame($expectedFoo, $dummy->foo); + } + + public function provideDenormalizeWithFilterBoolData(): array + { + return [ + [['foo' => 'true'], true], + [['foo' => '1'], true], + [['foo' => 'yes'], true], + [['foo' => 'false'], false], + [['foo' => '0'], false], + [['foo' => 'no'], false], + [['foo' => ''], false], + [['foo' => null], null], + [['foo' => 'null'], null], + [['foo' => 'something'], null], + ]; + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer @@ -1480,6 +1508,12 @@ class TruePropertyDummy public $foo; } +class BoolPropertyDummy +{ + /** @var null|bool */ + public $foo; +} + class SerializerCollectionDummy implements SerializerInterface, DenormalizerInterface { private array $normalizers; From 0ae173cc0ebdd73f15fad4005151e1eeac101df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 1 Jul 2024 02:16:34 +0200 Subject: [PATCH 042/104] Remove useless uniqid in tempnam calls --- Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php b/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php index 1826d3dc4..40dcb5015 100644 --- a/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php +++ b/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php @@ -27,7 +27,7 @@ final class ClassMetadataFactoryCompilerTest extends TestCase protected function setUp(): void { - $this->dumpPath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'php_serializer_metadata.'.uniqid('CompiledClassMetadataFactory').'.php'; + $this->dumpPath = tempnam(sys_get_temp_dir(), 'sf_serializer_metadata_'); } protected function tearDown(): void From faaab8b87521de97b2471db04569a99d3ff1f466 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 4 Jul 2024 11:48:29 +0200 Subject: [PATCH 043/104] [Serializer] Remove ArrayDenormalizer::setDenormalizer() --- Normalizer/ArrayDenormalizer.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Normalizer/ArrayDenormalizer.php b/Normalizer/ArrayDenormalizer.php index af5ffb6aa..23df0a1e2 100644 --- a/Normalizer/ArrayDenormalizer.php +++ b/Normalizer/ArrayDenormalizer.php @@ -29,11 +29,6 @@ class ArrayDenormalizer implements DenormalizerInterface, DenormalizerAwareInter { use DenormalizerAwareTrait; - public function setDenormalizer(DenormalizerInterface $denormalizer): void - { - $this->denormalizer = $denormalizer; - } - public function getSupportedTypes(?string $format): array { return ['object' => null, '*' => false]; From e439ec653b32cd8b75faaf639d1b3d1952ef2c95 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 6 Jul 2024 09:57:16 +0200 Subject: [PATCH 044/104] Update .gitattributes --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 84c7add05..14c3c3594 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore From f08629456bc65ceb7e25939f35b41433c6a24288 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 17 Jul 2024 10:14:50 +0200 Subject: [PATCH 045/104] do not use uniqid() for generating dev tool tokens --- Debug/TraceableSerializer.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Debug/TraceableSerializer.php b/Debug/TraceableSerializer.php index 9cf7bad80..f04f3c911 100644 --- a/Debug/TraceableSerializer.php +++ b/Debug/TraceableSerializer.php @@ -37,7 +37,7 @@ public function __construct( public function serialize(mixed $data, string $format, array $context = []): string { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); + $context[self::DEBUG_TRACE_ID] = $traceId = bin2hex(random_bytes(4)); $startTime = microtime(true); $result = $this->serializer->serialize($data, $format, $context); @@ -52,7 +52,7 @@ public function serialize(mixed $data, string $format, array $context = []): str public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); + $context[self::DEBUG_TRACE_ID] = $traceId = bin2hex(random_bytes(4)); $startTime = microtime(true); $result = $this->serializer->deserialize($data, $type, $format, $context); @@ -67,7 +67,7 @@ public function deserialize(mixed $data, string $type, string $format, array $co public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); + $context[self::DEBUG_TRACE_ID] = $traceId = bin2hex(random_bytes(4)); $startTime = microtime(true); $result = $this->serializer->normalize($object, $format, $context); @@ -82,7 +82,7 @@ public function normalize(mixed $object, ?string $format = null, array $context public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); + $context[self::DEBUG_TRACE_ID] = $traceId = bin2hex(random_bytes(4)); $startTime = microtime(true); $result = $this->serializer->denormalize($data, $type, $format, $context); @@ -97,7 +97,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a public function encode(mixed $data, string $format, array $context = []): string { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); + $context[self::DEBUG_TRACE_ID] = $traceId = bin2hex(random_bytes(4)); $startTime = microtime(true); $result = $this->serializer->encode($data, $format, $context); @@ -112,7 +112,7 @@ public function encode(mixed $data, string $format, array $context = []): string public function decode(string $data, string $format, array $context = []): mixed { - $context[self::DEBUG_TRACE_ID] = $traceId = uniqid('', true); + $context[self::DEBUG_TRACE_ID] = $traceId = bin2hex(random_bytes(4)); $startTime = microtime(true); $result = $this->serializer->decode($data, $format, $context); From 9a67fcf320561e96f94d62bbe0e169ac534a5718 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 26 Jul 2024 15:11:24 +0200 Subject: [PATCH 046/104] Sync .github/expected-missing-return-types.diff --- Normalizer/AbstractNormalizer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index ccada2ed6..aeae375fb 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -267,6 +267,8 @@ protected function getGroups(array $context): array /** * Is this attribute allowed? + * + * @return bool */ protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []) { From 51611e545131dee861f3c570ce1dbec2cae3c96b Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 31 Jul 2024 16:13:26 +0200 Subject: [PATCH 047/104] Remove unused code and unnecessary `else` branches --- Encoder/XmlEncoder.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Encoder/XmlEncoder.php b/Encoder/XmlEncoder.php index a57030b10..e1a816380 100644 --- a/Encoder/XmlEncoder.php +++ b/Encoder/XmlEncoder.php @@ -152,9 +152,8 @@ public function decode(string $data, string $format, array $context = []): mixed } $data = array_merge($this->parseXmlAttributes($rootNode, $context), ['#' => $rootNode->nodeValue]); - $data = $this->addXmlNamespaces($data, $rootNode, $dom); - return $data; + return $this->addXmlNamespaces($data, $rootNode, $dom); } public function supportsEncoding(string $format): bool From 1e26038da323d5146a309f1d5d8c3c8dd770f448 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Thu, 2 May 2024 14:41:09 +0200 Subject: [PATCH 048/104] [Serializer] Rework `XmlEncoderTest` --- Tests/Encoder/XmlEncoderTest.php | 399 +++++++++++++------------------ 1 file changed, 171 insertions(+), 228 deletions(-) diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index 5fc8fe983..ae96f2c14 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -39,46 +39,39 @@ protected function setUp(): void $this->encoder->setSerializer($serializer); } - public function testEncodeScalar() + /** + * @dataProvider validEncodeProvider + */ + public function testEncode(string $expected, mixed $data, array $context = []) { - $obj = new ScalarDummy(); - $obj->xmlFoo = 'foo'; - - $expected = ''."\n". - 'foo'."\n"; - - $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); + $this->assertSame($expected, $this->encoder->encode($data, 'xml', $context)); } - public function testEncodeArrayObject() - { - $obj = new \ArrayObject(['foo' => 'bar']); - - $expected = ''."\n". - 'bar'."\n"; - - $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); - } - - public function testEncodeEmptyArrayObject() + /** + * @return iterable + */ + public static function validEncodeProvider(): iterable { - $obj = new \ArrayObject(); + $obj = new ScalarDummy(); + $obj->xmlFoo = 'foo'; - $expected = ''."\n". - ''."\n"; + yield 'encode scalar' => [ + ''."\n" + .'foo'."\n", + $obj, + ]; - $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); - } + yield 'encode array object' => [ + ''."\n" + .'bar'."\n", + new \ArrayObject(['foo' => 'bar']), + ]; - public function testDocTypeIsNotAllowed() - { - $this->expectException(UnexpectedValueException::class); - $this->expectExceptionMessage('Document types are not allowed.'); - $this->encoder->decode('', 'foo'); - } + yield 'encode empty array object' => [ + ''."\n".''."\n", + new \ArrayObject(), + ]; - public function testAttributes() - { $obj = new ScalarDummy(); $obj->xmlFoo = [ 'foo-bar' => [ @@ -100,7 +93,9 @@ public function testAttributes() '@sring' => 'a', ], ]; - $expected = ''."\n". + + yield 'attributes' => [ + ''."\n". ''. ''. 'Test'. @@ -110,12 +105,10 @@ public function testAttributes() '3'. 'b'. ''. - ''."\n"; - $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); - } + ''."\n", + $obj, + ]; - public function testElementNameValid() - { $obj = new ScalarDummy(); $obj->xmlFoo = [ 'foo-bar' => 'a', @@ -123,70 +116,54 @@ public function testElementNameValid() 'föo_bär' => 'a', ]; - $expected = ''."\n". + yield 'element name valid' => [ + ''."\n". ''. 'a'. 'a'. 'a'. - ''."\n"; - - $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); - } + ''."\n", + $obj, + ]; - public function testEncodeSimpleXML() - { $xml = simplexml_load_string('Peter'); $array = ['person' => $xml]; - $expected = ''."\n". - 'Peter'."\n"; - - $this->assertEquals($expected, $this->encoder->encode($array, 'xml')); - } + yield 'encode SimpleXML' => [ + ''."\n". + 'Peter'."\n", + $array, + ]; - public function testEncodeXmlAttributes() - { $xml = simplexml_load_string('Peter'); $array = ['person' => $xml]; - $expected = ''."\n". - 'Peter'."\n"; - - $context = [ - 'xml_version' => '1.1', - 'xml_encoding' => 'utf-8', - 'xml_standalone' => true, + yield 'encode XML attributes' => [ + ''."\n". + 'Peter'."\n", + $array, + [ + 'xml_version' => '1.1', + 'xml_encoding' => 'utf-8', + 'xml_standalone' => true, + ], ]; - $this->assertSame($expected, $this->encoder->encode($array, 'xml', $context)); - } - - public function testEncodeRemovingEmptyTags() - { - $array = ['person' => ['firstname' => 'Peter', 'lastname' => null]]; - - $expected = ''."\n". - 'Peter'."\n"; - - $context = ['remove_empty_tags' => true]; - - $this->assertSame($expected, $this->encoder->encode($array, 'xml', $context)); - } - - public function testEncodeNotRemovingEmptyTags() - { - $array = ['person' => ['firstname' => 'Peter', 'lastname' => null]]; - - $expected = ''."\n". - 'Peter'."\n"; + yield 'encode remvoing empty tags' => [ + ''."\n". + 'Peter'."\n", + ['person' => ['firstname' => 'Peter', 'lastname' => null]], + ['remove_empty_tags' => true], + ]; - $this->assertSame($expected, $this->encoder->encode($array, 'xml')); - } + yield 'encode not removing empty tags' => [ + ''."\n". + 'Peter'."\n", + ['person' => ['firstname' => 'Peter', 'lastname' => null]], + ]; - public function testContext() - { - $array = ['person' => ['name' => 'George Abitbol', 'age' => null]]; - $expected = <<<'XML' + yield 'encode with context' => [ + <<<'XML' @@ -195,128 +172,139 @@ public function testContext() -XML; +XML, + ['person' => ['name' => 'George Abitbol', 'age' => null]], + [ + 'xml_format_output' => true, + 'save_options' => \LIBXML_NOEMPTYTAG, + ], + ]; - $context = [ - 'xml_format_output' => true, - 'save_options' => \LIBXML_NOEMPTYTAG, + yield 'encode scalar root attributes' => [ + ''."\n". + 'Paul'."\n", + [ + '#' => 'Paul', + '@eye-color' => 'brown', + ], ]; - $this->assertSame($expected, $this->encoder->encode($array, 'xml', $context)); - } + yield 'encode root attributes' => [ + ''."\n". + 'Paul'."\n", + [ + 'firstname' => 'Paul', + '@eye-color' => 'brown', + ], + ]; - public function testEncodeScalarRootAttributes() - { - $array = [ - '#' => 'Paul', - '@eye-color' => 'brown', + yield 'encode with CDATA wrapping with default pattern #1' => [ + ''."\n". + ']]>'."\n", + ['firstname' => 'Paul & Martha '], ]; - $expected = ''."\n". - 'Paul'."\n"; + yield 'encode with CDATA wrapping with default pattern #2' => [ + ''."\n". + 'O\'Donnel'."\n", + ['lastname' => 'O\'Donnel'], + ]; - $this->assertEquals($expected, $this->encoder->encode($array, 'xml')); - } + yield 'encode with CDATA wrapping with default pattern #3' => [ + ''."\n". + ''."\n", + ['firstname' => 'Paul & Martha'], + ]; - public function testEncodeRootAttributes() - { - $array = [ - 'firstname' => 'Paul', - '@eye-color' => 'brown', + yield 'encode with CDATA wrapping with custom pattern #1' => [ + ''."\n". + ']]>'."\n", + ['firstname' => 'Paul & Martha '], + ['cdata_wrapping_pattern' => '/[<>&"\']/'] ]; - $expected = ''."\n". - 'Paul'."\n"; + yield 'encode with CDATA wrapping with custom pattern #2' => [ + ''."\n". + ''."\n", + ['lastname' => 'O\'Donnel'], + ['cdata_wrapping_pattern' => '/[<>&"\']/'] + ]; - $this->assertEquals($expected, $this->encoder->encode($array, 'xml')); - } + yield 'encode with CDATA wrapping with custom pattern #3' => [ + ''."\n". + 'Paul and Martha'."\n", + ['firstname' => 'Paul and Martha'], + ['cdata_wrapping_pattern' => '/[<>&"\']/'] + ]; - /** - * @dataProvider encodeCdataWrappingWithDefaultPattern - */ - public function testEncodeCdataWrappingWithDefaultPattern($input, $expected) - { - $this->assertEquals($expected, $this->encoder->encode($input, 'xml')); - } + yield 'enable CDATA wrapping' => [ + ''."\n". + ']]>'."\n", + ['firstname' => 'Paul & Martha '], + ['cdata_wrapping' => true], + ]; - public static function encodeCdataWrappingWithDefaultPattern() - { - return [ - [ - ['firstname' => 'Paul and Martha'], - ''."\n".'Paul and Martha'."\n", - ], - [ - ['lastname' => 'O\'Donnel'], - ''."\n".'O\'Donnel'."\n", - ], - [ - ['firstname' => 'Paul & Martha '], - ''."\n".']]>'."\n", - ], + yield 'disable CDATA wrapping' => [ + ''."\n". + 'Paul & Martha <or Me>'."\n", + ['firstname' => 'Paul & Martha '], + ['cdata_wrapping' => false], ]; - } - /** - * @dataProvider encodeCdataWrappingWithCustomPattern - */ - public function testEncodeCdataWrappingWithCustomPattern($input, $expected) - { - $this->assertEquals($expected, $this->encoder->encode($input, 'xml', ['cdata_wrapping_pattern' => '/[<>&"\']/'])); - } + yield 'encode scalar with attribute' => [ + ''."\n". + 'Peter'."\n", + ['person' => ['@eye-color' => 'brown', '#' => 'Peter']], + ]; - public static function encodeCdataWrappingWithCustomPattern() - { - return [ - [ - ['firstname' => 'Paul and Martha'], - ''."\n".'Paul and Martha'."\n", - ], - [ - ['lastname' => 'O\'Donnel'], - ''."\n".''."\n", - ], - [ - ['firstname' => 'Paul & Martha '], - ''."\n".']]>'."\n", - ], + yield 'encode' => [ + self::getXmlSource(), + self::getObject(), + ]; + + yield 'encode with namespace' => [ + self::getNamespacedXmlSource(), + self::getNamespacedArray(), ]; } - public function testEnableCdataWrapping() + public function testEncodeSerializerXmlRootNodeNameOption() { + $options = ['xml_root_node_name' => 'test']; + $this->encoder = new XmlEncoder(); + $serializer = new Serializer([], ['xml' => new XmlEncoder()]); + $this->encoder->setSerializer($serializer); + $array = [ - 'firstname' => 'Paul & Martha ', + 'person' => ['@eye-color' => 'brown', '#' => 'Peter'], ]; $expected = ''."\n". - ']]>'."\n"; + 'Peter'."\n"; - $this->assertEquals($expected, $this->encoder->encode($array, 'xml', ['cdata_wrapping' => true])); + $this->assertSame($expected, $serializer->serialize($array, 'xml', $options)); } - public function testDisableCdataWrapping() + public function testEncodeTraversableWhenNormalizable() { - $array = [ - 'firstname' => 'Paul & Martha ', - ]; + $this->encoder = new XmlEncoder(); + $serializer = new Serializer([new CustomNormalizer()], ['xml' => new XmlEncoder()]); + $this->encoder->setSerializer($serializer); - $expected = ''."\n". - 'Paul & Martha <or Me>'."\n"; + $expected = <<<'XML' + +normalizedFoonormalizedBar - $this->assertEquals($expected, $this->encoder->encode($array, 'xml', ['cdata_wrapping' => false])); +XML; + + $this->assertSame($expected, $serializer->serialize(new NormalizableTraversableDummy(), 'xml')); } - public function testEncodeScalarWithAttribute() + public function testDocTypeIsNotAllowed() { - $array = [ - 'person' => ['@eye-color' => 'brown', '#' => 'Peter'], - ]; - - $expected = ''."\n". - 'Peter'."\n"; - - $this->assertEquals($expected, $this->encoder->encode($array, 'xml')); + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Document types are not allowed.'); + $this->encoder->decode('', 'foo'); } public function testDecodeScalar() @@ -324,7 +312,7 @@ public function testDecodeScalar() $source = ''."\n". 'foo'."\n"; - $this->assertEquals('foo', $this->encoder->decode($source, 'xml')); + $this->assertSame('foo', $this->encoder->decode($source, 'xml')); } public function testDecodeBigDigitAttributes() @@ -425,54 +413,6 @@ public function testDoesNotTypeCastStringsStartingWith0() $this->assertSame('018', $data['@a']); } - public function testEncode() - { - $source = $this->getXmlSource(); - $obj = $this->getObject(); - - $this->assertEquals($source, $this->encoder->encode($obj, 'xml')); - } - - public function testEncodeWithNamespace() - { - $source = $this->getNamespacedXmlSource(); - $array = $this->getNamespacedArray(); - - $this->assertEquals($source, $this->encoder->encode($array, 'xml')); - } - - public function testEncodeSerializerXmlRootNodeNameOption() - { - $options = ['xml_root_node_name' => 'test']; - $this->encoder = new XmlEncoder(); - $serializer = new Serializer([], ['xml' => new XmlEncoder()]); - $this->encoder->setSerializer($serializer); - - $array = [ - 'person' => ['@eye-color' => 'brown', '#' => 'Peter'], - ]; - - $expected = ''."\n". - 'Peter'."\n"; - - $this->assertEquals($expected, $serializer->serialize($array, 'xml', $options)); - } - - public function testEncodeTraversableWhenNormalizable() - { - $this->encoder = new XmlEncoder(); - $serializer = new Serializer([new CustomNormalizer()], ['xml' => new XmlEncoder()]); - $this->encoder->setSerializer($serializer); - - $expected = <<<'XML' - -normalizedFoonormalizedBar - -XML; - - $this->assertEquals($expected, $serializer->serialize(new NormalizableTraversableDummy(), 'xml')); - } - public function testEncodeException() { $this->expectException(NotEncodableValueException::class); @@ -816,7 +756,7 @@ public function testDecodeEmptyXml() $this->encoder->decode(' ', 'xml'); } - protected function getXmlSource() + protected static function getXmlSource(): string { return ''."\n". ''. @@ -829,7 +769,7 @@ protected function getXmlSource() ''."\n"; } - protected function getNamespacedXmlSource() + protected static function getNamespacedXmlSource(): string { return ''."\n". ''. @@ -842,7 +782,7 @@ protected function getNamespacedXmlSource() ''."\n"; } - protected function getNamespacedArray() + protected static function getNamespacedArray(): array { return [ '@xmlns' => 'http://www.w3.org/2005/Atom', @@ -876,7 +816,10 @@ protected function getNamespacedArray() ]; } - protected function getObject() + /** + * @return Dummy + */ + protected static function getObject(): object { $obj = new Dummy(); $obj->foo = 'foo'; From a6a73cb7ca3c2160bb97494ca881f4278652caf7 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 5 Aug 2024 09:12:25 +0200 Subject: [PATCH 049/104] Fix multiple CS errors --- Tests/Encoder/XmlEncoderTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index ae96f2c14..31d2ddfc6 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -220,21 +220,21 @@ public static function validEncodeProvider(): iterable ''."\n". ']]>'."\n", ['firstname' => 'Paul & Martha '], - ['cdata_wrapping_pattern' => '/[<>&"\']/'] + ['cdata_wrapping_pattern' => '/[<>&"\']/'], ]; yield 'encode with CDATA wrapping with custom pattern #2' => [ ''."\n". ''."\n", ['lastname' => 'O\'Donnel'], - ['cdata_wrapping_pattern' => '/[<>&"\']/'] + ['cdata_wrapping_pattern' => '/[<>&"\']/'], ]; yield 'encode with CDATA wrapping with custom pattern #3' => [ ''."\n". 'Paul and Martha'."\n", ['firstname' => 'Paul and Martha'], - ['cdata_wrapping_pattern' => '/[<>&"\']/'] + ['cdata_wrapping_pattern' => '/[<>&"\']/'], ]; yield 'enable CDATA wrapping' => [ From a03a64fd42072e9b798ec442d8e4cf4a3c3255a4 Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson <8751750+rynhndrcksn@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:24:13 -0700 Subject: [PATCH 050/104] fix denormalizing mixed collection values --- Normalizer/AbstractObjectNormalizer.php | 6 ++++ .../AbstractObjectNormalizerTest.php | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 3275d976a..63068420b 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -767,6 +767,12 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $class = null; } } + } elseif ($t instanceof ObjectType) { + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $t->getClassName(); + } else { + $typeIdentifier = $t->getTypeIdentifier(); + $class = null; } } else { if ($t instanceof ObjectType) { diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index e1b1031da..a666185dd 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1224,6 +1224,29 @@ public function provideDenormalizeWithFilterBoolData(): array [['foo' => 'something'], null], ]; } + + public function testDenormalizeArrayObject() + { + $normalizer = new class() extends AbstractObjectNormalizerDummy { + public function __construct() + { + parent::__construct(null, null, new PhpDocExtractor()); + } + + protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool + { + return true; + } + }; + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); + + $actual = $normalizer->denormalize(['foo' => ['array' => ['key' => 'value']]], DummyWithArrayObject::class); + + $this->assertInstanceOf(DummyWithArrayObject::class, $actual); + $this->assertInstanceOf(\ArrayObject::class, $actual->foo); + $this->assertSame(1, $actual->foo->count()); + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer @@ -1515,6 +1538,12 @@ class BoolPropertyDummy public $foo; } +class DummyWithArrayObject +{ + /** @var \ArrayObject */ + public $foo; +} + class SerializerCollectionDummy implements SerializerInterface, DenormalizerInterface { private array $normalizers; From c2b207d4ad885a52e7f9c845d27e363d2826f0a6 Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Thu, 1 Aug 2024 17:21:17 +0200 Subject: [PATCH 051/104] Code style change in `@PER-CS2.0` affecting `@Symfony` (parentheses for anonymous classes) --- Tests/Context/ContextBuilderTraitTest.php | 4 ++-- .../AbstractNormalizerContextBuilderTest.php | 2 +- ...ractObjectNormalizerContextBuilderTest.php | 2 +- .../Factory/CacheMetadataFactoryTest.php | 2 +- Tests/Normalizer/AbstractNormalizerTest.php | 2 +- .../AbstractObjectNormalizerTest.php | 20 +++++++++---------- .../Features/CallbacksTestTrait.php | 4 ++-- Tests/Normalizer/MapDenormalizationTest.php | 2 +- Tests/Normalizer/ObjectNormalizerTest.php | 4 ++-- Tests/SerializerTest.php | 6 +++--- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Tests/Context/ContextBuilderTraitTest.php b/Tests/Context/ContextBuilderTraitTest.php index 17ad231a0..edd3937e2 100644 --- a/Tests/Context/ContextBuilderTraitTest.php +++ b/Tests/Context/ContextBuilderTraitTest.php @@ -22,7 +22,7 @@ class ContextBuilderTraitTest extends TestCase { public function testWithContext() { - $contextBuilder = new class() implements ContextBuilderInterface { + $contextBuilder = new class implements ContextBuilderInterface { use ContextBuilderTrait; }; @@ -37,7 +37,7 @@ public function testWithContext() public function testWith() { - $contextBuilder = new class() { + $contextBuilder = new class { use ContextBuilderTrait; public function withFoo(string $value): static diff --git a/Tests/Context/Normalizer/AbstractNormalizerContextBuilderTest.php b/Tests/Context/Normalizer/AbstractNormalizerContextBuilderTest.php index 4b8f0cc3f..4e92c54d8 100644 --- a/Tests/Context/Normalizer/AbstractNormalizerContextBuilderTest.php +++ b/Tests/Context/Normalizer/AbstractNormalizerContextBuilderTest.php @@ -25,7 +25,7 @@ class AbstractNormalizerContextBuilderTest extends TestCase protected function setUp(): void { - $this->contextBuilder = new class() extends AbstractNormalizerContextBuilder {}; + $this->contextBuilder = new class extends AbstractNormalizerContextBuilder {}; } /** diff --git a/Tests/Context/Normalizer/AbstractObjectNormalizerContextBuilderTest.php b/Tests/Context/Normalizer/AbstractObjectNormalizerContextBuilderTest.php index 410f2972b..c13760118 100644 --- a/Tests/Context/Normalizer/AbstractObjectNormalizerContextBuilderTest.php +++ b/Tests/Context/Normalizer/AbstractObjectNormalizerContextBuilderTest.php @@ -25,7 +25,7 @@ class AbstractObjectNormalizerContextBuilderTest extends TestCase protected function setUp(): void { - $this->contextBuilder = new class() extends AbstractObjectNormalizerContextBuilder {}; + $this->contextBuilder = new class extends AbstractObjectNormalizerContextBuilder {}; } /** diff --git a/Tests/Mapping/Factory/CacheMetadataFactoryTest.php b/Tests/Mapping/Factory/CacheMetadataFactoryTest.php index 6db0b95ae..e18dd707f 100644 --- a/Tests/Mapping/Factory/CacheMetadataFactoryTest.php +++ b/Tests/Mapping/Factory/CacheMetadataFactoryTest.php @@ -68,7 +68,7 @@ public function testInvalidClassThrowsException() public function testAnonymousClass() { - $anonymousObject = new class() { + $anonymousObject = new class { }; $metadata = new ClassMetadata($anonymousObject::class); diff --git a/Tests/Normalizer/AbstractNormalizerTest.php b/Tests/Normalizer/AbstractNormalizerTest.php index 3108fe3c6..74ca3d6d6 100644 --- a/Tests/Normalizer/AbstractNormalizerTest.php +++ b/Tests/Normalizer/AbstractNormalizerTest.php @@ -271,7 +271,7 @@ public function testVariadicConstructorDenormalization() public static function getNormalizerWithCustomNameConverter() { $extractor = new PhpDocExtractor(); - $nameConverter = new class() implements NameConverterInterface { + $nameConverter = new class implements NameConverterInterface { public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { return ucfirst($propertyName); diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index e1b1031da..685641ff1 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -526,7 +526,7 @@ public function testDenormalizeWithDiscriminatorMapUsesCorrectClassname() { $factory = new ClassMetadataFactory(new AttributeLoader()); - $loaderMock = new class() implements ClassMetadataFactoryInterface { + $loaderMock = new class implements ClassMetadataFactoryInterface { public function getMetadataFor($value): ClassMetadataInterface { if (AbstractDummy::class === $value) { @@ -561,7 +561,7 @@ public function testDenormalizeWithDiscriminatorMapAndObjectToPopulateUsesCorrec { $factory = new ClassMetadataFactory(new AttributeLoader()); - $loaderMock = new class() implements ClassMetadataFactoryInterface { + $loaderMock = new class implements ClassMetadataFactoryInterface { public function getMetadataFor($value): ClassMetadataInterface { if (AbstractDummy::class === $value) { @@ -614,7 +614,7 @@ public function hasMetadataFor($value): bool public function testDenormalizeWithNestedDiscriminatorMap() { - $classDiscriminatorResolver = new class() implements ClassDiscriminatorResolverInterface { + $classDiscriminatorResolver = new class implements ClassDiscriminatorResolverInterface { public function getMappingForClass(string $class): ?ClassDiscriminatorMapping { return match ($class) { @@ -867,7 +867,7 @@ public function testDenormalizeWithNumberAsSerializedNameAndNoArrayReindex() '99' => 'baz', ]; - $obj = new class() { + $obj = new class { #[SerializedName('1')] public $foo; @@ -891,7 +891,7 @@ public function testDenormalizeWithCorrectOrderOfAttributeAndProperty() ], ]; - $obj = new class() { + $obj = new class { #[SerializedPath('[data][id]')] public $id; }; @@ -902,7 +902,7 @@ public function testDenormalizeWithCorrectOrderOfAttributeAndProperty() public function testNormalizeBasedOnAllowedAttributes() { - $normalizer = new class() extends AbstractObjectNormalizer { + $normalizer = new class extends AbstractObjectNormalizer { public function getSupportedTypes(?string $format): array { return ['*' => false]; @@ -982,7 +982,7 @@ public function testProvidingContextCacheKeyGeneratesSameChildContextCacheKey() $foobar->bar = 'bar'; $foobar->baz = 'baz'; - $normalizer = new class() extends AbstractObjectNormalizerDummy { + $normalizer = new class extends AbstractObjectNormalizerDummy { public $childContextCacheKey; protected function extractAttributes(object $object, ?string $format = null, array $context = []): array @@ -1022,7 +1022,7 @@ public function testChildContextKeepsOriginalContextCacheKey() $foobar->bar = 'bar'; $foobar->baz = 'baz'; - $normalizer = new class() extends AbstractObjectNormalizerDummy { + $normalizer = new class extends AbstractObjectNormalizerDummy { public $childContextCacheKey; protected function extractAttributes(object $object, ?string $format = null, array $context = []): array @@ -1057,7 +1057,7 @@ public function testChildContextCacheKeyStaysFalseWhenOriginalCacheKeyIsFalse() $foobar->bar = 'bar'; $foobar->baz = 'baz'; - $normalizer = new class() extends AbstractObjectNormalizerDummy { + $normalizer = new class extends AbstractObjectNormalizerDummy { public $childContextCacheKey; protected function extractAttributes(object $object, ?string $format = null, array $context = []): array @@ -1087,7 +1087,7 @@ protected function createChildContext(array $parentContext, string $attribute, ? public function testDenormalizeXmlScalar() { - $normalizer = new class() extends AbstractObjectNormalizer { + $normalizer = new class extends AbstractObjectNormalizer { public function __construct() { parent::__construct(null, new MetadataAwareNameConverter(new ClassMetadataFactory(new AttributeLoader()))); diff --git a/Tests/Normalizer/Features/CallbacksTestTrait.php b/Tests/Normalizer/Features/CallbacksTestTrait.php index 09e2f8867..3a9191ae8 100644 --- a/Tests/Normalizer/Features/CallbacksTestTrait.php +++ b/Tests/Normalizer/Features/CallbacksTestTrait.php @@ -59,7 +59,7 @@ public function testNormalizeCallbacksWithNoConstructorArgument($callbacks, $val { $normalizer = $this->getNormalizerForCallbacksWithPropertyTypeExtractor(); - $obj = new class() extends CallbacksObject { + $obj = new class extends CallbacksObject { public function __construct() { } @@ -101,7 +101,7 @@ public function testDenormalizeCallbacksWithNoConstructorArgument($callbacks, $v { $normalizer = $this->getNormalizerForCallbacksWithPropertyTypeExtractor(); - $objWithNoConstructorArgument = new class() extends CallbacksObject { + $objWithNoConstructorArgument = new class extends CallbacksObject { public function __construct() { } diff --git a/Tests/Normalizer/MapDenormalizationTest.php b/Tests/Normalizer/MapDenormalizationTest.php index ea4515955..75ecbccb9 100644 --- a/Tests/Normalizer/MapDenormalizationTest.php +++ b/Tests/Normalizer/MapDenormalizationTest.php @@ -187,7 +187,7 @@ public function testNullableAbstractObject() private function getSerializer() { - $loaderMock = new class() implements ClassMetadataFactoryInterface { + $loaderMock = new class implements ClassMetadataFactoryInterface { public function getMetadataFor($value): ClassMetadataInterface { if (AbstractDummyValue::class === $value) { diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 8a979e23f..323eba000 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -749,7 +749,7 @@ public function testDoesntHaveIssuesWithUnionConstTypes() $normalizer = new ObjectNormalizer(null, null, null, $extractor); $serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]); - $this->assertSame('bar', $serializer->denormalize(['foo' => 'bar'], (new class() { + $this->assertSame('bar', $serializer->denormalize(['foo' => 'bar'], (new class { /** @var self::*|null */ public $foo; })::class)->foo); @@ -784,7 +784,7 @@ public function testDenormalizeFalsePseudoType() public function testAdvancedNameConverter() { - $nameConverter = new class() implements AdvancedNameConverterInterface { + $nameConverter = new class implements AdvancedNameConverterInterface { public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { return \sprintf('%s-%s-%s-%s', $propertyName, $class, $format, $context['foo']); diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 7d4cbc30d..a600f0724 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -426,7 +426,7 @@ public function testDeserializeAndSerializeAbstractObjectsWithTheClassMetadataDi $example = new AbstractDummyFirstChild('foo-value', 'bar-value'); $example->setQuux(new DummyFirstChildQuux('quux')); - $loaderMock = new class() implements ClassMetadataFactoryInterface { + $loaderMock = new class implements ClassMetadataFactoryInterface { public function getMetadataFor($value): ClassMetadataInterface { if (AbstractDummy::class === $value) { @@ -607,10 +607,10 @@ public static function provideObjectOrCollectionTests() $data['c2'] = new \ArrayObject(['nested' => new \ArrayObject(['k' => 'v'])]); $data['d1'] = new \ArrayObject(['nested' => []]); $data['d2'] = new \ArrayObject(['nested' => ['k' => 'v']]); - $data['e1'] = new class() { + $data['e1'] = new class { public $map = []; }; - $data['e2'] = new class() { + $data['e2'] = new class { public $map = ['k' => 'v']; }; $data['f1'] = new class(new \ArrayObject()) { From bbd69a44cc5de4265b707f81116aadbcc580ccb4 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 12 Aug 2024 11:46:28 +0200 Subject: [PATCH 052/104] [Serializer] Remove useless calls to `func_get_arg()` --- NameConverter/MetadataAwareNameConverter.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/NameConverter/MetadataAwareNameConverter.php b/NameConverter/MetadataAwareNameConverter.php index bc693bd90..eec3b42ce 100644 --- a/NameConverter/MetadataAwareNameConverter.php +++ b/NameConverter/MetadataAwareNameConverter.php @@ -43,10 +43,6 @@ public function __construct( public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { - $class = 1 < \func_num_args() ? func_get_arg(1) : null; - $format = 2 < \func_num_args() ? func_get_arg(2) : null; - $context = 3 < \func_num_args() ? func_get_arg(3) : []; - if (null === $class) { return $this->normalizeFallback($propertyName, $class, $format, $context); } @@ -60,10 +56,6 @@ public function normalize(string $propertyName, ?string $class = null, ?string $ public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { - $class = 1 < \func_num_args() ? func_get_arg(1) : null; - $format = 2 < \func_num_args() ? func_get_arg(2) : null; - $context = 3 < \func_num_args() ? func_get_arg(3) : []; - if (null === $class) { return $this->denormalizeFallback($propertyName, $class, $format, $context); } From 5f9d7e2e164b02c400cfd7d83bb0fc11f33c218b Mon Sep 17 00:00:00 2001 From: HypeMC Date: Wed, 14 Aug 2024 23:21:30 +0200 Subject: [PATCH 053/104] [Serializer] Remove redundant @internal tags from traceable classes --- DataCollector/SerializerDataCollector.php | 2 +- Debug/TraceableEncoder.php | 2 +- Debug/TraceableNormalizer.php | 2 +- Debug/TraceableSerializer.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DataCollector/SerializerDataCollector.php b/DataCollector/SerializerDataCollector.php index 671239d28..2880dea37 100644 --- a/DataCollector/SerializerDataCollector.php +++ b/DataCollector/SerializerDataCollector.php @@ -21,7 +21,7 @@ /** * @author Mathias Arlaud * - * @internal + * @final */ class SerializerDataCollector extends DataCollector implements LateDataCollectorInterface { diff --git a/Debug/TraceableEncoder.php b/Debug/TraceableEncoder.php index 0795d14ca..afefee0ee 100644 --- a/Debug/TraceableEncoder.php +++ b/Debug/TraceableEncoder.php @@ -23,7 +23,7 @@ * * @author Mathias Arlaud * - * @internal + * @final */ class TraceableEncoder implements EncoderInterface, DecoderInterface, SerializerAwareInterface { diff --git a/Debug/TraceableNormalizer.php b/Debug/TraceableNormalizer.php index fc4db40ad..88ab4863d 100644 --- a/Debug/TraceableNormalizer.php +++ b/Debug/TraceableNormalizer.php @@ -25,7 +25,7 @@ * * @author Mathias Arlaud * - * @internal + * @final */ class TraceableNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, NormalizerAwareInterface, DenormalizerAwareInterface, CacheableSupportsMethodInterface { diff --git a/Debug/TraceableSerializer.php b/Debug/TraceableSerializer.php index 789ae65ca..dd22e8678 100644 --- a/Debug/TraceableSerializer.php +++ b/Debug/TraceableSerializer.php @@ -24,7 +24,7 @@ * * @author Mathias Arlaud * - * @internal + * @final */ class TraceableSerializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface { From b4ad9e64a83a45c2893f9d57996ffb13a9271183 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 16 Aug 2024 10:46:08 +0200 Subject: [PATCH 054/104] remove custom CSV escape character from tests --- Tests/Encoder/CsvEncoderTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/Encoder/CsvEncoderTest.php b/Tests/Encoder/CsvEncoderTest.php index 9b1bbfb28..ae6fb7a2a 100644 --- a/Tests/Encoder/CsvEncoderTest.php +++ b/Tests/Encoder/CsvEncoderTest.php @@ -158,7 +158,7 @@ public function testEncodeCustomSettings() $this->encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); @@ -184,7 +184,7 @@ public function testEncodeCustomSettingsPassedInContext() , $this->encoder->encode($value, 'csv', [ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ])); } @@ -194,7 +194,7 @@ public function testEncodeCustomSettingsPassedInConstructor() $encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); $value = ['a' => 'he\'llo', 'c' => ['d' => 'foo']]; @@ -583,7 +583,7 @@ public function testDecodeCustomSettings() $this->encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); @@ -605,7 +605,7 @@ public function testDecodeCustomSettingsPassedInContext() , 'csv', [ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ])); } @@ -615,7 +615,7 @@ public function testDecodeCustomSettingsPassedInConstructor() $encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', CsvEncoder::AS_COLLECTION_KEY => true, // Can be removed in 5.0 ]); From a75d03d7720417f8a654e73e8f02acdea8779cd0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 17 Aug 2024 09:51:47 +0200 Subject: [PATCH 055/104] clean up PHP version checks --- Tests/Encoder/CsvEncoderTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/Encoder/CsvEncoderTest.php b/Tests/Encoder/CsvEncoderTest.php index f487356cd..c0be73a8b 100644 --- a/Tests/Encoder/CsvEncoderTest.php +++ b/Tests/Encoder/CsvEncoderTest.php @@ -149,7 +149,7 @@ public function testEncodeCustomSettings() $this->encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', + CsvEncoder::ESCAPE_CHAR_KEY => '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); @@ -175,7 +175,7 @@ public function testEncodeCustomSettingsPassedInContext() , $this->encoder->encode($value, 'csv', [ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', + CsvEncoder::ESCAPE_CHAR_KEY => '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ])); } @@ -185,7 +185,7 @@ public function testEncodeCustomSettingsPassedInConstructor() $encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', + CsvEncoder::ESCAPE_CHAR_KEY => '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); $value = ['a' => 'he\'llo', 'c' => ['d' => 'foo']]; @@ -574,7 +574,7 @@ public function testDecodeCustomSettings() $this->encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', + CsvEncoder::ESCAPE_CHAR_KEY => '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); @@ -596,7 +596,7 @@ public function testDecodeCustomSettingsPassedInContext() , 'csv', [ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', + CsvEncoder::ESCAPE_CHAR_KEY => '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ])); } @@ -606,7 +606,7 @@ public function testDecodeCustomSettingsPassedInConstructor() $encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', + CsvEncoder::ESCAPE_CHAR_KEY => '', CsvEncoder::KEY_SEPARATOR_KEY => '-', CsvEncoder::AS_COLLECTION_KEY => true, // Can be removed in 5.0 ]); From a06245d66340b8ba54c836787e19a6b2f1d681b2 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 25 Jul 2024 11:31:45 +0200 Subject: [PATCH 056/104] [Serializer][Translation] Deprecate passing a non-empty CSV escape char --- CHANGELOG.md | 6 ++++ Context/Encoder/CsvEncoderContextBuilder.php | 4 +++ Encoder/CsvEncoder.php | 7 +++++ .../Encoder/CsvEncoderContextBuilderTest.php | 24 +++++++++++--- Tests/Encoder/CsvEncoderTest.php | 31 +++++++++++++++---- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3118834d8..8473d9e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +7.2 +--- + + * Deprecate the `csv_escape_char` context option of `CsvEncoder` and the `CsvEncoder::ESCAPE_CHAR_KEY` constant + * Deprecate `CsvEncoderContextBuilder::withEscapeChar()` method + 7.1 --- diff --git a/Context/Encoder/CsvEncoderContextBuilder.php b/Context/Encoder/CsvEncoderContextBuilder.php index f449bc314..9f0d6da6f 100644 --- a/Context/Encoder/CsvEncoderContextBuilder.php +++ b/Context/Encoder/CsvEncoderContextBuilder.php @@ -62,10 +62,14 @@ public function withEnclosure(?string $enclosure): static * * Must be empty or a single character. * + * @deprecated since Symfony 7.2, to be removed in 8.0 + * * @throws InvalidArgumentException */ public function withEscapeChar(?string $escapeChar): static { + trigger_deprecation('symfony/serializer', '7.2', 'The "%s" method is deprecated. It will be removed in 8.0.', __METHOD__); + if (null !== $escapeChar && \strlen($escapeChar) > 1) { throw new InvalidArgumentException(\sprintf('The "%s" escape character must be empty or a single character.', $escapeChar)); } diff --git a/Encoder/CsvEncoder.php b/Encoder/CsvEncoder.php index 462bd663b..3902b5613 100644 --- a/Encoder/CsvEncoder.php +++ b/Encoder/CsvEncoder.php @@ -25,6 +25,9 @@ class CsvEncoder implements EncoderInterface, DecoderInterface public const FORMAT = 'csv'; public const DELIMITER_KEY = 'csv_delimiter'; public const ENCLOSURE_KEY = 'csv_enclosure'; + /** + * @deprecated since Symfony 7.2, to be removed in 8.0 + */ public const ESCAPE_CHAR_KEY = 'csv_escape_char'; public const KEY_SEPARATOR_KEY = 'csv_key_separator'; public const HEADERS_KEY = 'csv_headers'; @@ -53,6 +56,10 @@ class CsvEncoder implements EncoderInterface, DecoderInterface public function __construct(array $defaultContext = []) { + if (\array_key_exists(self::ESCAPE_CHAR_KEY, $defaultContext)) { + trigger_deprecation('symfony/serializer', '7.2', 'Setting the "csv_escape_char" option is deprecated. The option will be removed in 8.0.'); + } + $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } diff --git a/Tests/Context/Encoder/CsvEncoderContextBuilderTest.php b/Tests/Context/Encoder/CsvEncoderContextBuilderTest.php index c71d41b63..bcaaf2a88 100644 --- a/Tests/Context/Encoder/CsvEncoderContextBuilderTest.php +++ b/Tests/Context/Encoder/CsvEncoderContextBuilderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Context\Encoder; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Exception\InvalidArgumentException; @@ -21,6 +22,8 @@ */ class CsvEncoderContextBuilderTest extends TestCase { + use ExpectDeprecationTrait; + private CsvEncoderContextBuilder $contextBuilder; protected function setUp(): void @@ -38,7 +41,6 @@ public function testWithers(array $values) $context = $this->contextBuilder ->withDelimiter($values[CsvEncoder::DELIMITER_KEY]) ->withEnclosure($values[CsvEncoder::ENCLOSURE_KEY]) - ->withEscapeChar($values[CsvEncoder::ESCAPE_CHAR_KEY]) ->withKeySeparator($values[CsvEncoder::KEY_SEPARATOR_KEY]) ->withHeaders($values[CsvEncoder::HEADERS_KEY]) ->withEscapedFormulas($values[CsvEncoder::ESCAPE_FORMULAS_KEY]) @@ -59,7 +61,6 @@ public static function withersDataProvider(): iterable yield 'With values' => [[ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => '"', - CsvEncoder::ESCAPE_CHAR_KEY => '\\', CsvEncoder::KEY_SEPARATOR_KEY => '_', CsvEncoder::HEADERS_KEY => ['h1', 'h2'], CsvEncoder::ESCAPE_FORMULAS_KEY => true, @@ -72,7 +73,6 @@ public static function withersDataProvider(): iterable yield 'With null values' => [[ CsvEncoder::DELIMITER_KEY => null, CsvEncoder::ENCLOSURE_KEY => null, - CsvEncoder::ESCAPE_CHAR_KEY => null, CsvEncoder::KEY_SEPARATOR_KEY => null, CsvEncoder::HEADERS_KEY => null, CsvEncoder::ESCAPE_FORMULAS_KEY => null, @@ -88,7 +88,6 @@ public function testWithersWithoutValue() $context = $this->contextBuilder ->withDelimiter(null) ->withEnclosure(null) - ->withEscapeChar(null) ->withKeySeparator(null) ->withHeaders(null) ->withEscapedFormulas(null) @@ -101,7 +100,6 @@ public function testWithersWithoutValue() $this->assertSame([ CsvEncoder::DELIMITER_KEY => null, CsvEncoder::ENCLOSURE_KEY => null, - CsvEncoder::ESCAPE_CHAR_KEY => null, CsvEncoder::KEY_SEPARATOR_KEY => null, CsvEncoder::HEADERS_KEY => null, CsvEncoder::ESCAPE_FORMULAS_KEY => null, @@ -124,9 +122,25 @@ public function testCannotSetMultipleBytesAsEnclosure() $this->contextBuilder->withEnclosure('ọ'); } + /** + * @group legacy + */ public function testCannotSetMultipleBytesAsEscapeChar() { + $this->expectDeprecation('Since symfony/serializer 7.2: The "Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder::withEscapeChar" method is deprecated. It will be removed in 8.0.'); + $this->expectException(InvalidArgumentException::class); $this->contextBuilder->withEscapeChar('ọ'); } + + /** + * @group legacy + */ + public function testWithEscapeCharIsDeprecated() + { + $this->expectDeprecation('Since symfony/serializer 7.2: The "Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder::withEscapeChar" method is deprecated. It will be removed in 8.0.'); + $context = $this->contextBuilder->withEscapeChar('\\'); + + $this->assertSame(['csv_escape_char' => '\\'], $context->toArray()); + } } diff --git a/Tests/Encoder/CsvEncoderTest.php b/Tests/Encoder/CsvEncoderTest.php index f487356cd..e250d1c61 100644 --- a/Tests/Encoder/CsvEncoderTest.php +++ b/Tests/Encoder/CsvEncoderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Encoder; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Exception\UnexpectedValueException; @@ -20,6 +21,8 @@ */ class CsvEncoderTest extends TestCase { + use ExpectDeprecationTrait; + private CsvEncoder $encoder; protected function setUp(): void @@ -149,7 +152,6 @@ public function testEncodeCustomSettings() $this->encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); @@ -175,7 +177,6 @@ public function testEncodeCustomSettingsPassedInContext() , $this->encoder->encode($value, 'csv', [ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ])); } @@ -185,7 +186,6 @@ public function testEncodeCustomSettingsPassedInConstructor() $encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); $value = ['a' => 'he\'llo', 'c' => ['d' => 'foo']]; @@ -574,7 +574,6 @@ public function testDecodeCustomSettings() $this->encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ]); @@ -596,7 +595,6 @@ public function testDecodeCustomSettingsPassedInContext() , 'csv', [ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', ])); } @@ -606,7 +604,6 @@ public function testDecodeCustomSettingsPassedInConstructor() $encoder = new CsvEncoder([ CsvEncoder::DELIMITER_KEY => ';', CsvEncoder::ENCLOSURE_KEY => "'", - CsvEncoder::ESCAPE_CHAR_KEY => \PHP_VERSION_ID < 70400 ? '|' : '', CsvEncoder::KEY_SEPARATOR_KEY => '-', CsvEncoder::AS_COLLECTION_KEY => true, // Can be removed in 5.0 ]); @@ -710,4 +707,26 @@ public function testEndOfLinePassedInConstructor() $encoder = new CsvEncoder([CsvEncoder::END_OF_LINE => "\r\n"]); $this->assertSame("foo,bar\r\nhello,test\r\n", $encoder->encode($value, 'csv')); } + + /** + * @group legacy + */ + public function testPassingNonEmptyEscapeCharIsDeprecated() + { + $this->expectDeprecation('Since symfony/serializer 7.2: Setting the "csv_escape_char" option is deprecated. The option will be removed in 8.0.'); + $encoder = new CsvEncoder(['csv_escape_char' => '@']); + + $this->assertSame( + [[ + 'A, B@"' => 'D', + 'C' => 'E', + ]], + $encoder->decode(<<<'CSV' + "A, B@"", "C" + "D", "E" + CSV, + 'csv' + ) + ); + } } From d0c1cd1603f2509a138a541c9b0005bb722466a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 22 Aug 2024 10:19:16 +0200 Subject: [PATCH 057/104] [Serializer] more precise type for CamelCaseToSnakeCaseNameConverter::$attributes --- NameConverter/CamelCaseToSnakeCaseNameConverter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NameConverter/CamelCaseToSnakeCaseNameConverter.php b/NameConverter/CamelCaseToSnakeCaseNameConverter.php index 47d69d3ab..033ec94b7 100644 --- a/NameConverter/CamelCaseToSnakeCaseNameConverter.php +++ b/NameConverter/CamelCaseToSnakeCaseNameConverter.php @@ -27,8 +27,8 @@ class CamelCaseToSnakeCaseNameConverter implements NameConverterInterface public const REQUIRE_SNAKE_CASE_PROPERTIES = 'require_snake_case_properties'; /** - * @param array|null $attributes The list of attributes to rename or null for all attributes - * @param bool $lowerCamelCase Use lowerCamelCase style + * @param string[]|null $attributes The list of attributes to rename or null for all attributes + * @param bool $lowerCamelCase Use lowerCamelCase style */ public function __construct( private ?array $attributes = null, From 0158b0e91b7cf7e744a6fb9acaeb613d1ca40dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 22 Aug 2024 11:39:57 +0200 Subject: [PATCH 058/104] [Serializer] Fix CamelCaseToSnakeCaseNameConverterTest::testDenormalizeWithContext --- Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php b/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php index fc9967f0c..d1edc2325 100644 --- a/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php +++ b/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php @@ -61,9 +61,9 @@ public static function attributeProvider() public function testDenormalizeWithContext() { $nameConverter = new CamelCaseToSnakeCaseNameConverter(null, true); - $denormalizedValue = $nameConverter->denormalize('last_name', null, null, [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES]); + $denormalizedValue = $nameConverter->denormalize('last_name', null, null, [CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES => true]); - $this->assertSame($denormalizedValue, 'lastName'); + $this->assertSame('lastName', $denormalizedValue); } public function testErrorDenormalizeWithContext() From e31367ee8f489d0180023525624883ed87e30d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 22 Aug 2024 11:18:18 +0200 Subject: [PATCH 059/104] [Serializer] Add SnakeCaseToCamelCaseNameConverter --- CHANGELOG.md | 1 + .../SnakeCaseToCamelCaseNameConverter.php | 78 +++++++++++++++++++ .../SnakeCaseToCamelCaseNameConverterTest.php | 64 +++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 NameConverter/SnakeCaseToCamelCaseNameConverter.php create mode 100644 Tests/NameConverter/SnakeCaseToCamelCaseNameConverterTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8473d9e9b..bb31b565a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Deprecate the `csv_escape_char` context option of `CsvEncoder` and the `CsvEncoder::ESCAPE_CHAR_KEY` constant * Deprecate `CsvEncoderContextBuilder::withEscapeChar()` method + * Add `SnakeCaseToCamelCaseNameConverter` 7.1 --- diff --git a/NameConverter/SnakeCaseToCamelCaseNameConverter.php b/NameConverter/SnakeCaseToCamelCaseNameConverter.php new file mode 100644 index 000000000..cb93d3e98 --- /dev/null +++ b/NameConverter/SnakeCaseToCamelCaseNameConverter.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\NameConverter; + +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; + +/** + * Underscore to camelCase name converter. + * + * @author Kévin Dunglas + */ +final readonly class SnakeCaseToCamelCaseNameConverter implements NameConverterInterface +{ + /** + * Require all properties to be written in camelCase. + */ + public const REQUIRE_CAMEL_CASE_PROPERTIES = 'require_camel_case_properties'; + + /** + * @param string[]|null $attributes The list of attributes to rename or null for all attributes + * @param bool $lowerCamelCase Use lowerCamelCase style + */ + public function __construct( + private ?array $attributes = null, + private bool $lowerCamelCase = true, + ) { + } + + /** + * @param class-string|null $class + * @param array $context + */ + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + if (null !== $this->attributes && !\in_array($propertyName, $this->attributes, true)) { + return $propertyName; + } + + $camelCasedName = preg_replace_callback( + '/(^|_|\.)+(.)/', + fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]), + $propertyName + ); + + if ($this->lowerCamelCase) { + $camelCasedName = lcfirst($camelCasedName); + } + + return $camelCasedName; + } + + /** + * @param class-string|null $class + * @param array $context + */ + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + if (($context[self::REQUIRE_CAMEL_CASE_PROPERTIES] ?? false) && $propertyName !== $this->normalize($propertyName, $class, $format, $context)) { + throw new UnexpectedPropertyException($propertyName); + } + + $snakeCased = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); + if (null === $this->attributes || \in_array($snakeCased, $this->attributes, true)) { + return $snakeCased; + } + + return $propertyName; + } +} diff --git a/Tests/NameConverter/SnakeCaseToCamelCaseNameConverterTest.php b/Tests/NameConverter/SnakeCaseToCamelCaseNameConverterTest.php new file mode 100644 index 000000000..2d2799e2c --- /dev/null +++ b/Tests/NameConverter/SnakeCaseToCamelCaseNameConverterTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\NameConverter; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; + +/** + * @author Kévin Dunglas + * @author Aurélien Pillevesse + */ +class SnakeCaseToCamelCaseNameConverterTest extends TestCase +{ + public function testInterface() + { + $attributeMetadata = new SnakeCaseToCamelCaseNameConverter(); + $this->assertInstanceOf(NameConverterInterface::class, $attributeMetadata); + } + + /** + * @dataProvider Symfony\Component\Serializer\Tests\NameConverter\CamelCaseToSnakeCaseNameConverterTest::attributeProvider + */ + public function testNormalize($underscored, $camelCased, $useLowerCamelCase) + { + $nameConverter = new SnakeCaseToCamelCaseNameConverter(null, $useLowerCamelCase); + $this->assertEquals($camelCased, $nameConverter->normalize($underscored)); + } + + /** + * @dataProvider Symfony\Component\Serializer\Tests\NameConverter\CamelCaseToSnakeCaseNameConverterTest::attributeProvider + */ + public function testDenormalize($underscored, $camelCased, $useLowerCamelCase) + { + $nameConverter = new SnakeCaseToCamelCaseNameConverter(null, $useLowerCamelCase); + $this->assertEquals($underscored, $nameConverter->denormalize($camelCased)); + } + + public function testDenormalizeWithContext() + { + $nameConverter = new SnakeCaseToCamelCaseNameConverter(null, true); + $denormalizedValue = $nameConverter->denormalize('lastName', null, null, [SnakeCaseToCamelCaseNameConverter::REQUIRE_CAMEL_CASE_PROPERTIES => true]); + + $this->assertSame('last_name', $denormalizedValue); + } + + public function testErrorDenormalizeWithContext() + { + $nameConverter = new SnakeCaseToCamelCaseNameConverter(null, true); + + $this->expectException(UnexpectedPropertyException::class); + $nameConverter->denormalize('last_name', null, null, [SnakeCaseToCamelCaseNameConverter::REQUIRE_CAMEL_CASE_PROPERTIES => true]); + } +} From bce9daacdce95cf5e774b17b10f9d8153c6e947d Mon Sep 17 00:00:00 2001 From: Attila Szeremi Date: Mon, 22 Jul 2024 10:09:14 +0200 Subject: [PATCH 060/104] [Serializer] Support subclasses of `DateTime` and `DateTimeImmutable` --- CHANGELOG.md | 1 + Normalizer/DateTimeNormalizer.php | 2 +- Tests/Normalizer/DateTimeNormalizerTest.php | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3118834d8..232c8eda7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * Add `CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES` context option * Deprecate `AbstractNormalizerContextBuilder::withDefaultContructorArguments(?array $defaultContructorArguments)`, use `withDefaultConstructorArguments(?array $defaultConstructorArguments)` instead (note the missing `s` character in Contructor word in deprecated method) * Add `XmlEncoder::CDATA_WRAPPING_PATTERN` context option + * Support subclasses of `\DateTime` and `\DateTimeImmutable` for denormalization 7.0 --- diff --git a/Normalizer/DateTimeNormalizer.php b/Normalizer/DateTimeNormalizer.php index 55b2e130e..dfc498c19 100644 --- a/Normalizer/DateTimeNormalizer.php +++ b/Normalizer/DateTimeNormalizer.php @@ -138,7 +138,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { - return isset(self::SUPPORTED_TYPES[$type]); + return is_a($type, \DateTimeInterface::class, true); } /** diff --git a/Tests/Normalizer/DateTimeNormalizerTest.php b/Tests/Normalizer/DateTimeNormalizerTest.php index 5dbf36fbe..a744325d4 100644 --- a/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/Tests/Normalizer/DateTimeNormalizerTest.php @@ -232,6 +232,8 @@ public function testSupportsDenormalization() $this->assertTrue($this->normalizer->supportsDenormalization('2016-01-01T00:00:00+00:00', \DateTimeInterface::class)); $this->assertTrue($this->normalizer->supportsDenormalization('2016-01-01T00:00:00+00:00', \DateTime::class)); $this->assertTrue($this->normalizer->supportsDenormalization('2016-01-01T00:00:00+00:00', \DateTimeImmutable::class)); + $this->assertTrue($this->normalizer->supportsDenormalization('2016-01-01T00:00:00+00:00', DateTimeImmutableChild::class)); + $this->assertTrue($this->normalizer->supportsDenormalization('2016-01-01T00:00:00+00:00', DateTimeChild::class)); $this->assertFalse($this->normalizer->supportsDenormalization('foo', 'Bar')); } @@ -241,6 +243,10 @@ public function testDenormalize() $this->assertEquals(new \DateTimeImmutable('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeImmutable::class)); $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTime::class)); $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize(' 2016-01-01T00:00:00+00:00 ', \DateTime::class)); + $this->assertEquals(new DateTimeImmutableChild('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', DateTimeImmutableChild::class)); + $this->assertEquals(new DateTimeImmutableChild('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', DateTimeImmutableChild::class)); + $this->assertEquals(new DateTimeChild('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', DateTimeChild::class)); + $this->assertEquals(new DateTimeChild('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize(' 2016-01-01T00:00:00+00:00 ', DateTimeChild::class)); $this->assertEquals(new \DateTimeImmutable('2023-05-06T17:35:34.000000+0000', new \DateTimeZone('UTC')), $this->normalizer->denormalize(1683394534, \DateTimeImmutable::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U'])); $this->assertEquals(new \DateTimeImmutable('2023-05-06T17:35:34.123400+0000', new \DateTimeZone('UTC')), $this->normalizer->denormalize(1683394534.1234, \DateTimeImmutable::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U.u'])); } @@ -387,3 +393,10 @@ public function testDenormalizeFormatMismatchThrowsException() $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeInterface::class, null, [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d|']); } } + +class DateTimeChild extends \DateTime +{ +} +class DateTimeImmutableChild extends \DateTimeImmutable +{ +} From a79b424babd6f328453610866bf12ffc353e26dc Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 30 Aug 2024 17:56:36 +0200 Subject: [PATCH 061/104] CS fixes --- CHANGELOG.md | 2 +- Tests/Normalizer/DateTimeNormalizerTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40d486a25..cb0466ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Deprecate the `csv_escape_char` context option of `CsvEncoder` and the `CsvEncoder::ESCAPE_CHAR_KEY` constant * Deprecate `CsvEncoderContextBuilder::withEscapeChar()` method * Add `SnakeCaseToCamelCaseNameConverter` + * Support subclasses of `\DateTime` and `\DateTimeImmutable` for denormalization 7.1 --- @@ -18,7 +19,6 @@ CHANGELOG * Add `CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES` context option * Deprecate `AbstractNormalizerContextBuilder::withDefaultContructorArguments(?array $defaultContructorArguments)`, use `withDefaultConstructorArguments(?array $defaultConstructorArguments)` instead (note the missing `s` character in Contructor word in deprecated method) * Add `XmlEncoder::CDATA_WRAPPING_PATTERN` context option - * Support subclasses of `\DateTime` and `\DateTimeImmutable` for denormalization 7.0 --- diff --git a/Tests/Normalizer/DateTimeNormalizerTest.php b/Tests/Normalizer/DateTimeNormalizerTest.php index a744325d4..81219652b 100644 --- a/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/Tests/Normalizer/DateTimeNormalizerTest.php @@ -397,6 +397,7 @@ public function testDenormalizeFormatMismatchThrowsException() class DateTimeChild extends \DateTime { } + class DateTimeImmutableChild extends \DateTimeImmutable { } From 88eeb782a3da661bf481ce6b57e99ef66aeedbdf Mon Sep 17 00:00:00 2001 From: Thomas Calvet Date: Wed, 11 Sep 2024 18:39:40 +0200 Subject: [PATCH 062/104] [Uid][Serializer][Validator] Mention RFC 9562 --- Normalizer/UidNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Normalizer/UidNormalizer.php b/Normalizer/UidNormalizer.php index 70b5e158d..aa2a8b4fe 100644 --- a/Normalizer/UidNormalizer.php +++ b/Normalizer/UidNormalizer.php @@ -24,7 +24,7 @@ final class UidNormalizer implements NormalizerInterface, DenormalizerInterface, public const NORMALIZATION_FORMAT_CANONICAL = 'canonical'; public const NORMALIZATION_FORMAT_BASE58 = 'base58'; public const NORMALIZATION_FORMAT_BASE32 = 'base32'; - public const NORMALIZATION_FORMAT_RFC4122 = 'rfc4122'; + public const NORMALIZATION_FORMAT_RFC4122 = 'rfc4122'; // RFC 9562 obsoleted RFC 4122 but the format is the same private $defaultContext = [ self::NORMALIZATION_FORMAT_KEY => self::NORMALIZATION_FORMAT_CANONICAL, From cb6cff4433f9d2a675eee3e15448c5dd2cacce79 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 12 Sep 2024 20:19:16 +0200 Subject: [PATCH 063/104] [Serializer][Uid] Add the `Uuid::FORMAT_RFC_9562` and `UidNormalizer::NORMALIZATION_FORMAT_RFC9562` constants --- CHANGELOG.md | 1 + Normalizer/UidNormalizer.php | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb0466ada..b62f84ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Deprecate `CsvEncoderContextBuilder::withEscapeChar()` method * Add `SnakeCaseToCamelCaseNameConverter` * Support subclasses of `\DateTime` and `\DateTimeImmutable` for denormalization + * Add the `UidNormalizer::NORMALIZATION_FORMAT_RFC9562` constant 7.1 --- diff --git a/Normalizer/UidNormalizer.php b/Normalizer/UidNormalizer.php index b107c9d36..2e370bdd7 100644 --- a/Normalizer/UidNormalizer.php +++ b/Normalizer/UidNormalizer.php @@ -23,6 +23,7 @@ final class UidNormalizer implements NormalizerInterface, DenormalizerInterface public const NORMALIZATION_FORMAT_BASE58 = 'base58'; public const NORMALIZATION_FORMAT_BASE32 = 'base32'; public const NORMALIZATION_FORMAT_RFC4122 = 'rfc4122'; + public const NORMALIZATION_FORMAT_RFC9562 = self::NORMALIZATION_FORMAT_RFC4122; public const NORMALIZATION_FORMATS = [ self::NORMALIZATION_FORMAT_CANONICAL, self::NORMALIZATION_FORMAT_BASE58, From 5613373d6742561a0349ceee29bde4e8e245e83a Mon Sep 17 00:00:00 2001 From: Mihai Stancu Date: Tue, 10 Sep 2024 20:07:29 +0300 Subject: [PATCH 064/104] [Serializer] Fix for method named `get()` --- Normalizer/ObjectNormalizer.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Normalizer/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index a663083db..a8c887e50 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -100,14 +100,19 @@ protected function extractAttributes(object $object, ?string $format = null, arr $name = $reflMethod->name; $attributeName = null; - if (str_starts_with($name, 'get') || str_starts_with($name, 'has') || str_starts_with($name, 'can')) { + if (3 < \strlen($name) && match ($name[0]) { + 'g' => str_starts_with($name, 'get'), + 'h' => str_starts_with($name, 'has'), + 'c' => str_starts_with($name, 'can'), + default => false, + }) { // getters, hassers and canners $attributeName = substr($name, 3); if (!$reflClass->hasProperty($attributeName)) { $attributeName = lcfirst($attributeName); } - } elseif (str_starts_with($name, 'is')) { + } elseif ('is' !== $name && str_starts_with($name, 'is')) { // issers $attributeName = substr($name, 2); From a2e5a855105a0335fff5966684fc613c312f68cc Mon Sep 17 00:00:00 2001 From: valtzu Date: Fri, 13 Sep 2024 19:09:13 +0300 Subject: [PATCH 065/104] Fix `TemplateType` handling in `AbstractObjectNormalizer` --- .../AbstractObjectNormalizerTest.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index a666185dd..26f9be4ad 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\PropertyInfo\Type as LegacyType; @@ -37,6 +38,7 @@ use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; @@ -1247,6 +1249,52 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string $this->assertInstanceOf(\ArrayObject::class, $actual->foo); $this->assertSame(1, $actual->foo->count()); } + + public function testTemplateTypeWhenAnObjectIsPassedToDenormalize() + { + $normalizer = new class ( + classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()), + propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [new PhpStanExtractor(), new ReflectionExtractor()]) + ) extends AbstractObjectNormalizerDummy { + protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool + { + return true; + } + }; + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); + + $denormalizedData = $normalizer->denormalize(['value' => new DummyGenericsValue()], DummyGenericsValueWrapper::class); + + $this->assertInstanceOf(DummyGenericsValueWrapper::class, $denormalizedData); + $this->assertInstanceOf(DummyGenericsValue::class, $denormalizedData->value); + + $this->assertSame('dummy', $denormalizedData->value->type); + } + + public function testDenormalizeTemplateType() + { + $normalizer = new class ( + classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()), + propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [new PhpStanExtractor(), new ReflectionExtractor()]) + ) extends AbstractObjectNormalizerDummy { + protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool + { + return true; + } + }; + $serializer = new Serializer([new ArrayDenormalizer(), $normalizer]); + $normalizer->setSerializer($serializer); + + $denormalizedData = $normalizer->denormalize(['value' => ['type' => 'dummy'], 'values' => [['type' => 'dummy']]], DummyGenericsValueWrapper::class); + + $this->assertInstanceOf(DummyGenericsValueWrapper::class, $denormalizedData); + $this->assertInstanceOf(DummyGenericsValue::class, $denormalizedData->value); + $this->assertContainsOnlyInstancesOf(DummyGenericsValue::class, $denormalizedData->values); + $this->assertCount(1, $denormalizedData->values); + $this->assertSame('dummy', $denormalizedData->value->type); + $this->assertSame('dummy', $denormalizedData->values[0]->type); + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer @@ -1753,3 +1801,31 @@ public function getSupportedTypes(?string $format): array ]; } } + +#[DiscriminatorMap('type', ['dummy' => DummyGenericsValue::class])] +abstract class AbstractDummyGenericsValue +{ + public function __construct( + public string $type, + ) { + } +} + +class DummyGenericsValue extends AbstractDummyGenericsValue +{ + public function __construct() + { + parent::__construct('dummy'); + } +} + +/** + * @template T of AbstractDummyGenericsValue + */ +class DummyGenericsValueWrapper +{ + /** @var T */ + public mixed $value; + /** @var T[] */ + public array $values; +} From 60335737a9fd2c972126ce43344437d637085e77 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 18 Sep 2024 17:33:32 +0200 Subject: [PATCH 066/104] [Serializer] Catch `NotNormalizableValueException` for variadic parameters --- Normalizer/AbstractNormalizer.php | 11 ++++++++- Tests/Fixtures/DummyWithVariadicParameter.php | 24 +++++++++++++++++++ Tests/SerializerTest.php | 15 ++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 Tests/Fixtures/DummyWithVariadicParameter.php diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index aeae375fb..c28a1f6cd 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -358,7 +358,16 @@ protected function instantiateObject(array &$data, string $class, array &$contex $variadicParameters = []; foreach ($data[$key] as $parameterKey => $parameterData) { - $variadicParameters[$parameterKey] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format); + try { + $variadicParameters[$parameterKey] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format); + } catch (NotNormalizableValueException $exception) { + if (!isset($context['not_normalizable_value_exceptions'])) { + throw $exception; + } + + $context['not_normalizable_value_exceptions'][] = $exception; + $params[$paramName] = $parameterData; + } } $params = array_merge(array_values($params), $variadicParameters); diff --git a/Tests/Fixtures/DummyWithVariadicParameter.php b/Tests/Fixtures/DummyWithVariadicParameter.php new file mode 100644 index 000000000..827111921 --- /dev/null +++ b/Tests/Fixtures/DummyWithVariadicParameter.php @@ -0,0 +1,24 @@ + + * + * 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; + +use Symfony\Component\Uid\Uuid; + +class DummyWithVariadicParameter +{ + public array $variadic; + + public function __construct(Uuid ...$variadic) + { + $this->variadic = $variadic; + } +} diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index f7d364b7d..8f60ae1d4 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -61,6 +61,8 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor; use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull; +use Symfony\Component\Serializer\Tests\Fixtures\DummyWithVariadicParameter; +use Symfony\Component\Serializer\Tests\Fixtures\DummyWithVariadicProperty; use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\FooImplementationDummy; use Symfony\Component\Serializer\Tests\Fixtures\FooInterfaceDummyDenormalizer; @@ -1637,6 +1639,19 @@ public function testPartialDenormalizationWithMissingConstructorTypes() $this->assertSame($expected, $exceptionsAsArray); } + + public function testPartialDenormalizationWithInvalidVariadicParameter() + { + $json = '{"variadic": ["a random string"]}'; + + $serializer = new Serializer([new UidNormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]); + + $this->expectException(PartialDenormalizationException::class); + + $serializer->deserialize($json, DummyWithVariadicParameter::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + } } class Model From 498bda022c46e0382e0a7d2aa0dbf9f250d07c11 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 18 Sep 2024 13:33:46 +0200 Subject: [PATCH 067/104] Miscellaneous tests improvements --- Tests/Normalizer/PropertyNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index 9773e65b7..e0e2f9dae 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -478,7 +478,7 @@ public function testMultiDimensionObject() RootDummy::class, 'any' ); - $this->assertEquals($root::class, RootDummy::class); + $this->assertSame(RootDummy::class, $root::class); // children (two dimension array) $this->assertCount(1, $root->children); From d93eac1ffd8e3b85070971cd2d95f2e9f7794575 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 20 May 2024 06:59:56 +0200 Subject: [PATCH 068/104] [Serializer] Introduce named serializers --- CHANGELOG.md | 3 + DataCollector/SerializerDataCollector.php | 90 +-- Debug/TraceableEncoder.php | 5 +- Debug/TraceableNormalizer.php | 5 +- Debug/TraceableSerializer.php | 13 +- DependencyInjection/SerializerPass.php | 152 ++++- .../SerializerDataCollectorTest.php | 174 ++++-- Tests/Debug/TraceableEncoderTest.php | 26 +- Tests/Debug/TraceableNormalizerTest.php | 26 +- Tests/Debug/TraceableSerializerTest.php | 20 +- .../SerializerPassTest.php | 520 +++++++++++++++++- composer.json | 2 +- 12 files changed, 905 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b62f84ba2..79a14b50f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ CHANGELOG * Add `SnakeCaseToCamelCaseNameConverter` * Support subclasses of `\DateTime` and `\DateTimeImmutable` for denormalization * Add the `UidNormalizer::NORMALIZATION_FORMAT_RFC9562` constant + * Add support for configuring multiple serializer instances with different + default contexts, name converters, sets of normalizers and encoders + * Add support for collection profiles of multiple serializer instances 7.1 --- diff --git a/DataCollector/SerializerDataCollector.php b/DataCollector/SerializerDataCollector.php index 2880dea37..e87c51ca1 100644 --- a/DataCollector/SerializerDataCollector.php +++ b/DataCollector/SerializerDataCollector.php @@ -25,11 +25,22 @@ */ class SerializerDataCollector extends DataCollector implements LateDataCollectorInterface { + private const DATA_TEMPLATE = [ + 'serialize' => [], + 'deserialize' => [], + 'normalize' => [], + 'denormalize' => [], + 'encode' => [], + 'decode' => [], + ]; + + private array $dataGroupedByName; private array $collected = []; public function reset(): void { $this->data = []; + unset($this->dataGroupedByName); $this->collected = []; } @@ -43,14 +54,14 @@ public function getName(): string return 'serializer'; } - public function getData(): Data|array + public function getData(?string $name = null): Data|array { - return $this->data; + return null === $name ? $this->data : $this->getDataGroupedByName()[$name]; } - public function getHandledCount(): int + public function getHandledCount(?string $name = null): int { - return array_sum(array_map('count', $this->data)); + return array_sum(array_map('count', $this->getData($name))); } public function getTotalTime(): float @@ -64,110 +75,108 @@ public function getTotalTime(): float return $totalTime; } - public function collectSerialize(string $traceId, mixed $data, string $format, array $context, float $time, array $caller): void + public function getSerializerNames(): array + { + return array_keys($this->getDataGroupedByName()); + } + + public function collectSerialize(string $traceId, mixed $data, string $format, array $context, float $time, array $caller, string $name): void { unset($context[TraceableSerializer::DEBUG_TRACE_ID]); $this->collected[$traceId] = array_merge( $this->collected[$traceId] ?? [], - compact('data', 'format', 'context', 'time', 'caller'), + compact('data', 'format', 'context', 'time', 'caller', 'name'), ['method' => 'serialize'], ); } - public function collectDeserialize(string $traceId, mixed $data, string $type, string $format, array $context, float $time, array $caller): void + public function collectDeserialize(string $traceId, mixed $data, string $type, string $format, array $context, float $time, array $caller, string $name): void { unset($context[TraceableSerializer::DEBUG_TRACE_ID]); $this->collected[$traceId] = array_merge( $this->collected[$traceId] ?? [], - compact('data', 'format', 'type', 'context', 'time', 'caller'), + compact('data', 'format', 'type', 'context', 'time', 'caller', 'name'), ['method' => 'deserialize'], ); } - public function collectNormalize(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller): void + public function collectNormalize(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller, string $name): void { unset($context[TraceableSerializer::DEBUG_TRACE_ID]); $this->collected[$traceId] = array_merge( $this->collected[$traceId] ?? [], - compact('data', 'format', 'context', 'time', 'caller'), + compact('data', 'format', 'context', 'time', 'caller', 'name'), ['method' => 'normalize'], ); } - public function collectDenormalize(string $traceId, mixed $data, string $type, ?string $format, array $context, float $time, array $caller): void + public function collectDenormalize(string $traceId, mixed $data, string $type, ?string $format, array $context, float $time, array $caller, string $name): void { unset($context[TraceableSerializer::DEBUG_TRACE_ID]); $this->collected[$traceId] = array_merge( $this->collected[$traceId] ?? [], - compact('data', 'format', 'type', 'context', 'time', 'caller'), + compact('data', 'format', 'type', 'context', 'time', 'caller', 'name'), ['method' => 'denormalize'], ); } - public function collectEncode(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller): void + public function collectEncode(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller, string $name): void { unset($context[TraceableSerializer::DEBUG_TRACE_ID]); $this->collected[$traceId] = array_merge( $this->collected[$traceId] ?? [], - compact('data', 'format', 'context', 'time', 'caller'), + compact('data', 'format', 'context', 'time', 'caller', 'name'), ['method' => 'encode'], ); } - public function collectDecode(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller): void + public function collectDecode(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller, string $name): void { unset($context[TraceableSerializer::DEBUG_TRACE_ID]); $this->collected[$traceId] = array_merge( $this->collected[$traceId] ?? [], - compact('data', 'format', 'context', 'time', 'caller'), + compact('data', 'format', 'context', 'time', 'caller', 'name'), ['method' => 'decode'], ); } - public function collectNormalization(string $traceId, string $normalizer, float $time): void + public function collectNormalization(string $traceId, string $normalizer, float $time, string $name): void { $method = 'normalize'; - $this->collected[$traceId]['normalization'][] = compact('normalizer', 'method', 'time'); + $this->collected[$traceId]['normalization'][] = compact('normalizer', 'method', 'time', 'name'); } - public function collectDenormalization(string $traceId, string $normalizer, float $time): void + public function collectDenormalization(string $traceId, string $normalizer, float $time, string $name): void { $method = 'denormalize'; - $this->collected[$traceId]['normalization'][] = compact('normalizer', 'method', 'time'); + $this->collected[$traceId]['normalization'][] = compact('normalizer', 'method', 'time', 'name'); } - public function collectEncoding(string $traceId, string $encoder, float $time): void + public function collectEncoding(string $traceId, string $encoder, float $time, string $name): void { $method = 'encode'; - $this->collected[$traceId]['encoding'][] = compact('encoder', 'method', 'time'); + $this->collected[$traceId]['encoding'][] = compact('encoder', 'method', 'time', 'name'); } - public function collectDecoding(string $traceId, string $encoder, float $time): void + public function collectDecoding(string $traceId, string $encoder, float $time, string $name): void { $method = 'decode'; - $this->collected[$traceId]['encoding'][] = compact('encoder', 'method', 'time'); + $this->collected[$traceId]['encoding'][] = compact('encoder', 'method', 'time', 'name'); } public function lateCollect(): void { - $this->data = [ - 'serialize' => [], - 'deserialize' => [], - 'normalize' => [], - 'denormalize' => [], - 'encode' => [], - 'decode' => [], - ]; + $this->data = self::DATA_TEMPLATE; foreach ($this->collected as $collected) { if (!isset($collected['data'])) { @@ -184,6 +193,7 @@ public function lateCollect(): void 'normalization' => [], 'encoding' => [], 'caller' => $collected['caller'] ?? null, + 'name' => $collected['name'], ]; if (isset($collected['normalization'])) { @@ -220,6 +230,22 @@ public function lateCollect(): void } } + private function getDataGroupedByName(): array + { + if (!isset($this->dataGroupedByName)) { + $this->dataGroupedByName = []; + + foreach ($this->data as $method => $items) { + foreach ($items as $item) { + $this->dataGroupedByName[$item['name']] ??= self::DATA_TEMPLATE; + $this->dataGroupedByName[$item['name']][$method][] = $item; + } + } + } + + return $this->dataGroupedByName; + } + private function getMethodLocation(string $class, string $method): array { $reflection = new \ReflectionClass($class); diff --git a/Debug/TraceableEncoder.php b/Debug/TraceableEncoder.php index 42bf4868a..39e75e34f 100644 --- a/Debug/TraceableEncoder.php +++ b/Debug/TraceableEncoder.php @@ -30,6 +30,7 @@ class TraceableEncoder implements EncoderInterface, DecoderInterface, Serializer public function __construct( private EncoderInterface|DecoderInterface $encoder, private SerializerDataCollector $dataCollector, + private readonly string $serializerName = 'default', ) { } @@ -44,7 +45,7 @@ public function encode(mixed $data, string $format, array $context = []): string $time = microtime(true) - $startTime; if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { - $this->dataCollector->collectEncoding($traceId, $this->encoder::class, $time); + $this->dataCollector->collectEncoding($traceId, $this->encoder::class, $time, $this->serializerName); } return $encoded; @@ -70,7 +71,7 @@ public function decode(string $data, string $format, array $context = []): mixed $time = microtime(true) - $startTime; if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { - $this->dataCollector->collectDecoding($traceId, $this->encoder::class, $time); + $this->dataCollector->collectDecoding($traceId, $this->encoder::class, $time, $this->serializerName); } return $encoded; diff --git a/Debug/TraceableNormalizer.php b/Debug/TraceableNormalizer.php index c80636752..1b143e295 100644 --- a/Debug/TraceableNormalizer.php +++ b/Debug/TraceableNormalizer.php @@ -31,6 +31,7 @@ class TraceableNormalizer implements NormalizerInterface, DenormalizerInterface, public function __construct( private NormalizerInterface|DenormalizerInterface $normalizer, private SerializerDataCollector $dataCollector, + private readonly string $serializerName = 'default', ) { } @@ -50,7 +51,7 @@ public function normalize(mixed $object, ?string $format = null, array $context $time = microtime(true) - $startTime; if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { - $this->dataCollector->collectNormalization($traceId, $this->normalizer::class, $time); + $this->dataCollector->collectNormalization($traceId, $this->normalizer::class, $time, $this->serializerName); } return $normalized; @@ -76,7 +77,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $time = microtime(true) - $startTime; if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { - $this->dataCollector->collectDenormalization($traceId, $this->normalizer::class, $time); + $this->dataCollector->collectDenormalization($traceId, $this->normalizer::class, $time, $this->serializerName); } return $denormalized; diff --git a/Debug/TraceableSerializer.php b/Debug/TraceableSerializer.php index ab766bf43..a05bf4bf8 100644 --- a/Debug/TraceableSerializer.php +++ b/Debug/TraceableSerializer.php @@ -32,6 +32,7 @@ class TraceableSerializer implements SerializerInterface, NormalizerInterface, D public function __construct( private SerializerInterface&NormalizerInterface&DenormalizerInterface&EncoderInterface&DecoderInterface $serializer, private SerializerDataCollector $dataCollector, + private readonly string $serializerName = 'default', ) { } @@ -45,7 +46,7 @@ public function serialize(mixed $data, string $format, array $context = []): str $caller = $this->getCaller(__FUNCTION__, SerializerInterface::class); - $this->dataCollector->collectSerialize($traceId, $data, $format, $context, $time, $caller); + $this->dataCollector->collectSerialize($traceId, $data, $format, $context, $time, $caller, $this->serializerName); return $result; } @@ -60,7 +61,7 @@ public function deserialize(mixed $data, string $type, string $format, array $co $caller = $this->getCaller(__FUNCTION__, SerializerInterface::class); - $this->dataCollector->collectDeserialize($traceId, $data, $type, $format, $context, $time, $caller); + $this->dataCollector->collectDeserialize($traceId, $data, $type, $format, $context, $time, $caller, $this->serializerName); return $result; } @@ -75,7 +76,7 @@ public function normalize(mixed $object, ?string $format = null, array $context $caller = $this->getCaller(__FUNCTION__, NormalizerInterface::class); - $this->dataCollector->collectNormalize($traceId, $object, $format, $context, $time, $caller); + $this->dataCollector->collectNormalize($traceId, $object, $format, $context, $time, $caller, $this->serializerName); return $result; } @@ -90,7 +91,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $caller = $this->getCaller(__FUNCTION__, DenormalizerInterface::class); - $this->dataCollector->collectDenormalize($traceId, $data, $type, $format, $context, $time, $caller); + $this->dataCollector->collectDenormalize($traceId, $data, $type, $format, $context, $time, $caller, $this->serializerName); return $result; } @@ -105,7 +106,7 @@ public function encode(mixed $data, string $format, array $context = []): string $caller = $this->getCaller(__FUNCTION__, EncoderInterface::class); - $this->dataCollector->collectEncode($traceId, $data, $format, $context, $time, $caller); + $this->dataCollector->collectEncode($traceId, $data, $format, $context, $time, $caller, $this->serializerName); return $result; } @@ -120,7 +121,7 @@ public function decode(string $data, string $format, array $context = []): mixed $caller = $this->getCaller(__FUNCTION__, DecoderInterface::class); - $this->dataCollector->collectDecode($traceId, $data, $format, $context, $time, $caller); + $this->dataCollector->collectDecode($traceId, $data, $format, $context, $time, $caller, $this->serializerName); return $result; } diff --git a/DependencyInjection/SerializerPass.php b/DependencyInjection/SerializerPass.php index 2a429054b..bc1c6e10e 100644 --- a/DependencyInjection/SerializerPass.php +++ b/DependencyInjection/SerializerPass.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Debug\TraceableNormalizer; +use Symfony\Component\Serializer\SerializerInterface; /** * Adds all services with the tags "serializer.encoder" and "serializer.normalizer" as @@ -31,44 +32,177 @@ class SerializerPass implements CompilerPassInterface { use PriorityTaggedServiceTrait; + private const NAME_CONVERTER_METADATA_AWARE_ID = 'serializer.name_converter.metadata_aware'; + public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('serializer')) { return; } - if (!$normalizers = $this->findAndSortTaggedServices('serializer.normalizer', $container)) { + $namedSerializers = $container->hasParameter('.serializer.named_serializers') + ? $container->getParameter('.serializer.named_serializers') : []; + + $this->createNamedSerializerTags($container, 'serializer.normalizer', 'include_built_in_normalizers', $namedSerializers); + $this->createNamedSerializerTags($container, 'serializer.encoder', 'include_built_in_encoders', $namedSerializers); + + if (!$normalizers = $this->findAndSortTaggedServices('serializer.normalizer.default', $container)) { throw new RuntimeException('You must tag at least one service as "serializer.normalizer" to use the "serializer" service.'); } - if (!$encoders = $this->findAndSortTaggedServices('serializer.encoder', $container)) { + if (!$encoders = $this->findAndSortTaggedServices('serializer.encoder.default', $container)) { throw new RuntimeException('You must tag at least one service as "serializer.encoder" to use the "serializer" service.'); } if ($container->hasParameter('serializer.default_context')) { $defaultContext = $container->getParameter('serializer.default_context'); - foreach (array_merge($normalizers, $encoders) as $service) { - $definition = $container->getDefinition($service); - $definition->setBindings(['array $defaultContext' => new BoundArgument($defaultContext, false)] + $definition->getBindings()); + $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $defaultContext); + $container->getParameterBag()->remove('serializer.default_context'); + } + + $this->configureSerializer($container, 'serializer', $normalizers, $encoders, 'default'); + + if ($namedSerializers) { + $this->configureNamedSerializers($container); + } + } + + private function createNamedSerializerTags(ContainerBuilder $container, string $tagName, string $configName, array $namedSerializers): void + { + $serializerNames = array_keys($namedSerializers); + $withBuiltIn = array_filter($serializerNames, fn (string $name) => $namedSerializers[$name][$configName] ?? false); + + foreach ($container->findTaggedServiceIds($tagName) as $serviceId => $tags) { + $definition = $container->getDefinition($serviceId); + + foreach ($tags as $tag) { + $names = (array) ($tag['serializer'] ?? []); + + if (!$names) { + $names = ['default']; + } elseif (\in_array('*', $names, true)) { + $names = array_unique(['default', ...$serializerNames]); + } + + if ($tag['built_in'] ?? false) { + $names = array_unique(['default', ...$names, ...$withBuiltIn]); + } + + unset($tag['serializer'], $tag['built_in']); + + foreach ($names as $name) { + $definition->addTag($tagName.'.'.$name, $tag); + } } + } + } - $container->getParameterBag()->remove('serializer.default_context'); + private function bindDefaultContext(ContainerBuilder $container, array $services, array $defaultContext): void + { + foreach ($services as $id) { + $definition = $container->getDefinition((string) $id); + $definition->setBindings(['array $defaultContext' => new BoundArgument($defaultContext, false)] + $definition->getBindings()); } + } + private function configureSerializer(ContainerBuilder $container, string $id, array $normalizers, array $encoders, string $serializerName): void + { if ($container->getParameter('kernel.debug') && $container->hasDefinition('serializer.data_collector')) { foreach ($normalizers as $i => $normalizer) { $normalizers[$i] = $container->register('.debug.serializer.normalizer.'.$normalizer, TraceableNormalizer::class) - ->setArguments([$normalizer, new Reference('serializer.data_collector')]); + ->setArguments([$normalizer, new Reference('serializer.data_collector'), $serializerName]); } foreach ($encoders as $i => $encoder) { $encoders[$i] = $container->register('.debug.serializer.encoder.'.$encoder, TraceableEncoder::class) - ->setArguments([$encoder, new Reference('serializer.data_collector')]); + ->setArguments([$encoder, new Reference('serializer.data_collector'), $serializerName]); } } - $serializerDefinition = $container->getDefinition('serializer'); + $serializerDefinition = $container->getDefinition($id); $serializerDefinition->replaceArgument(0, $normalizers); $serializerDefinition->replaceArgument(1, $encoders); } + + private function configureNamedSerializers(ContainerBuilder $container): void + { + $defaultSerializerNameConverter = $container->hasParameter('.serializer.name_converter') + ? $container->getParameter('.serializer.name_converter') : null; + + foreach ($container->getParameter('.serializer.named_serializers') as $serializerName => $config) { + $config += ['default_context' => [], 'name_converter' => null]; + $serializerId = 'serializer.'.$serializerName; + + if (!$normalizers = $this->findAndSortTaggedServices('serializer.normalizer.'.$serializerName, $container)) { + throw new RuntimeException(\sprintf('The named serializer "%1$s" requires at least one registered normalizer. Tag the normalizers as "serializer.normalizer" with the "serializer" attribute set to "%1$s".', $serializerName)); + } + + if (!$encoders = $this->findAndSortTaggedServices('serializer.encoder.'.$serializerName, $container)) { + throw new RuntimeException(\sprintf('The named serializer "%1$s" requires at least one registered encoder. Tag the encoders as "serializer.encoder" with the "serializer" attribute set to "%1$s".', $serializerName)); + } + + $config['name_converter'] = $defaultSerializerNameConverter !== $config['name_converter'] + ? $this->buildChildNameConverterDefinition($container, $config['name_converter']) + : self::NAME_CONVERTER_METADATA_AWARE_ID; + + $normalizers = $this->buildChildDefinitions($container, $serializerName, $normalizers, $config); + $encoders = $this->buildChildDefinitions($container, $serializerName, $encoders, $config); + + $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $config['default_context']); + + $container->registerChild($serializerId, 'serializer'); + $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer'); + + $this->configureSerializer($container, $serializerId, $normalizers, $encoders, $serializerName); + + if ($container->getParameter('kernel.debug') && $container->hasDefinition('debug.serializer')) { + $container->registerChild($debugId = 'debug.'.$serializerId, 'debug.serializer') + ->setDecoratedService($serializerId) + ->replaceArgument(0, new Reference($debugId.'.inner')) + ->replaceArgument(2, $serializerName); + } + } + } + + private function buildChildNameConverterDefinition(ContainerBuilder $container, ?string $nameConverter): ?string + { + $childId = self::NAME_CONVERTER_METADATA_AWARE_ID.'.'.ContainerBuilder::hash($nameConverter); + + if (!$container->hasDefinition($childId)) { + $childDefinition = $container->registerChild($childId, self::NAME_CONVERTER_METADATA_AWARE_ID.'.abstract'); + if (null !== $nameConverter) { + $childDefinition->addArgument(new Reference($nameConverter)); + } + } + + return $childId; + } + + private function buildChildDefinitions(ContainerBuilder $container, string $serializerName, array $services, array $config): array + { + foreach ($services as &$id) { + $childId = $id.'.'.$serializerName; + + $definition = $container->registerChild($childId, (string) $id); + + if (null !== $nameConverterIndex = $this->findNameConverterIndex($container, (string) $id)) { + $definition->replaceArgument($nameConverterIndex, new Reference($config['name_converter'])); + } + + $id = new Reference($childId); + } + + return $services; + } + + private function findNameConverterIndex(ContainerBuilder $container, string $id): int|string|null + { + foreach ($container->getDefinition($id)->getArguments() as $index => $argument) { + if ($argument instanceof Reference && self::NAME_CONVERTER_METADATA_AWARE_ID === (string) $argument) { + return $index; + } + } + + return null; + } } diff --git a/Tests/DataCollector/SerializerDataCollectorTest.php b/Tests/DataCollector/SerializerDataCollectorTest.php index aebde8efa..6a26565a8 100644 --- a/Tests/DataCollector/SerializerDataCollectorTest.php +++ b/Tests/DataCollector/SerializerDataCollectorTest.php @@ -25,8 +25,8 @@ public function testCollectSerialize() $dataCollector = new SerializerDataCollector(); $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; - $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller); - $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0, $caller); + $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); + $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); $dataCollector->lateCollect(); $collectedData = $this->castCollectedData($dataCollector->getData()); @@ -41,6 +41,7 @@ public function testCollectSerialize() 'normalization' => [], 'encoding' => [], 'caller' => $caller, + 'name' => 'default', ]], $collectedData['serialize']); $this->assertSame([[ @@ -53,6 +54,7 @@ public function testCollectSerialize() 'normalization' => [], 'encoding' => [], 'caller' => $caller, + 'name' => 'default', ]], $collectedData['deserialize']); } @@ -61,8 +63,8 @@ public function testCollectNormalize() $dataCollector = new SerializerDataCollector(); $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; - $dataCollector->collectNormalize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller); - $dataCollector->collectDenormalize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0, $caller); + $dataCollector->collectNormalize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); + $dataCollector->collectDenormalize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); $dataCollector->lateCollect(); $collectedData = $this->castCollectedData($dataCollector->getData()); @@ -77,6 +79,7 @@ public function testCollectNormalize() 'normalization' => [], 'encoding' => [], 'caller' => $caller, + 'name' => 'default', ]], $collectedData['normalize']); $this->assertSame([[ @@ -89,6 +92,7 @@ public function testCollectNormalize() 'normalization' => [], 'encoding' => [], 'caller' => $caller, + 'name' => 'default', ]], $collectedData['denormalize']); } @@ -97,8 +101,8 @@ public function testCollectEncode() $dataCollector = new SerializerDataCollector(); $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; - $dataCollector->collectEncode('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller); - $dataCollector->collectDecode('traceIdTwo', 'data', 'format', ['foo' => 'bar'], 1.0, $caller); + $dataCollector->collectEncode('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); + $dataCollector->collectDecode('traceIdTwo', 'data', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); $dataCollector->lateCollect(); $collectedData = $this->castCollectedData($dataCollector->getData()); @@ -113,6 +117,7 @@ public function testCollectEncode() 'normalization' => [], 'encoding' => [], 'caller' => $caller, + 'name' => 'default', ]], $collectedData['encode']); $this->assertSame([[ @@ -125,6 +130,7 @@ public function testCollectEncode() 'normalization' => [], 'encoding' => [], 'caller' => $caller, + 'name' => 'default', ]], $collectedData['decode']); } @@ -133,18 +139,18 @@ public function testCollectNormalization() $dataCollector = new SerializerDataCollector(); $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; - $dataCollector->collectNormalize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 20.0, $caller); - $dataCollector->collectDenormalize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 20.0, $caller); + $dataCollector->collectNormalize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 20.0, $caller, 'default'); + $dataCollector->collectDenormalize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 20.0, $caller, 'default'); - $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 1.0); - $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 2.0); - $dataCollector->collectNormalization('traceIdOne', ObjectNormalizer::class, 5.0); - $dataCollector->collectNormalization('traceIdOne', ObjectNormalizer::class, 10.0); + $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 1.0, 'default'); + $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 2.0, 'default'); + $dataCollector->collectNormalization('traceIdOne', ObjectNormalizer::class, 5.0, 'default'); + $dataCollector->collectNormalization('traceIdOne', ObjectNormalizer::class, 10.0, 'default'); - $dataCollector->collectNormalization('traceIdTwo', DateTimeNormalizer::class, 1.0); - $dataCollector->collectNormalization('traceIdTwo', DateTimeNormalizer::class, 2.0); - $dataCollector->collectNormalization('traceIdTwo', ObjectNormalizer::class, 5.0); - $dataCollector->collectNormalization('traceIdTwo', ObjectNormalizer::class, 10.0); + $dataCollector->collectNormalization('traceIdTwo', DateTimeNormalizer::class, 1.0, 'default'); + $dataCollector->collectNormalization('traceIdTwo', DateTimeNormalizer::class, 2.0, 'default'); + $dataCollector->collectNormalization('traceIdTwo', ObjectNormalizer::class, 5.0, 'default'); + $dataCollector->collectNormalization('traceIdTwo', ObjectNormalizer::class, 10.0, 'default'); $dataCollector->lateCollect(); $collectedData = $dataCollector->getData(); @@ -189,18 +195,18 @@ public function testCollectEncoding() $dataCollector = new SerializerDataCollector(); $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; - $dataCollector->collectEncode('traceIdOne', 'data', 'format', ['foo' => 'bar'], 20.0, $caller); - $dataCollector->collectDecode('traceIdTwo', 'data', 'format', ['foo' => 'bar'], 20.0, $caller); + $dataCollector->collectEncode('traceIdOne', 'data', 'format', ['foo' => 'bar'], 20.0, $caller, 'default'); + $dataCollector->collectDecode('traceIdTwo', 'data', 'format', ['foo' => 'bar'], 20.0, $caller, 'default'); - $dataCollector->collectEncoding('traceIdOne', JsonEncoder::class, 1.0); - $dataCollector->collectEncoding('traceIdOne', JsonEncoder::class, 2.0); - $dataCollector->collectEncoding('traceIdOne', CsvEncoder::class, 5.0); - $dataCollector->collectEncoding('traceIdOne', CsvEncoder::class, 10.0); + $dataCollector->collectEncoding('traceIdOne', JsonEncoder::class, 1.0, 'default'); + $dataCollector->collectEncoding('traceIdOne', JsonEncoder::class, 2.0, 'default'); + $dataCollector->collectEncoding('traceIdOne', CsvEncoder::class, 5.0, 'default'); + $dataCollector->collectEncoding('traceIdOne', CsvEncoder::class, 10.0, 'default'); - $dataCollector->collectDecoding('traceIdTwo', JsonEncoder::class, 1.0); - $dataCollector->collectDecoding('traceIdTwo', JsonEncoder::class, 2.0); - $dataCollector->collectDecoding('traceIdTwo', CsvEncoder::class, 5.0); - $dataCollector->collectDecoding('traceIdTwo', CsvEncoder::class, 10.0); + $dataCollector->collectDecoding('traceIdTwo', JsonEncoder::class, 1.0, 'default'); + $dataCollector->collectDecoding('traceIdTwo', JsonEncoder::class, 2.0, 'default'); + $dataCollector->collectDecoding('traceIdTwo', CsvEncoder::class, 5.0, 'default'); + $dataCollector->collectDecoding('traceIdTwo', CsvEncoder::class, 10.0, 'default'); $dataCollector->lateCollect(); $collectedData = $dataCollector->getData(); @@ -245,13 +251,13 @@ public function testCountHandled() $dataCollector = new SerializerDataCollector(); $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; - $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller); - $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0, $caller); - $dataCollector->collectNormalize('traceIdThree', 'data', 'format', ['foo' => 'bar'], 20.0, $caller); - $dataCollector->collectDenormalize('traceIdFour', 'data', 'type', 'format', ['foo' => 'bar'], 20.0, $caller); - $dataCollector->collectEncode('traceIdFive', 'data', 'format', ['foo' => 'bar'], 20.0, $caller); - $dataCollector->collectDecode('traceIdSix', 'data', 'format', ['foo' => 'bar'], 20.0, $caller); - $dataCollector->collectSerialize('traceIdSeven', 'data', 'format', ['foo' => 'bar'], 1.0, $caller); + $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); + $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); + $dataCollector->collectNormalize('traceIdThree', 'data', 'format', ['foo' => 'bar'], 20.0, $caller, 'default'); + $dataCollector->collectDenormalize('traceIdFour', 'data', 'type', 'format', ['foo' => 'bar'], 20.0, $caller, 'default'); + $dataCollector->collectEncode('traceIdFive', 'data', 'format', ['foo' => 'bar'], 20.0, $caller, 'default'); + $dataCollector->collectDecode('traceIdSix', 'data', 'format', ['foo' => 'bar'], 20.0, $caller, 'default'); + $dataCollector->collectSerialize('traceIdSeven', 'data', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); $dataCollector->lateCollect(); @@ -264,13 +270,13 @@ public function testGetTotalTime() $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; - $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller); - $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 2.0, $caller); - $dataCollector->collectNormalize('traceIdThree', 'data', 'format', ['foo' => 'bar'], 3.0, $caller); - $dataCollector->collectDenormalize('traceIdFour', 'data', 'type', 'format', ['foo' => 'bar'], 4.0, $caller); - $dataCollector->collectEncode('traceIdFive', 'data', 'format', ['foo' => 'bar'], 5.0, $caller); - $dataCollector->collectDecode('traceIdSix', 'data', 'format', ['foo' => 'bar'], 6.0, $caller); - $dataCollector->collectSerialize('traceIdSeven', 'data', 'format', ['foo' => 'bar'], 7.0, $caller); + $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); + $dataCollector->collectDeserialize('traceIdTwo', 'data', 'type', 'format', ['foo' => 'bar'], 2.0, $caller, 'default'); + $dataCollector->collectNormalize('traceIdThree', 'data', 'format', ['foo' => 'bar'], 3.0, $caller, 'default'); + $dataCollector->collectDenormalize('traceIdFour', 'data', 'type', 'format', ['foo' => 'bar'], 4.0, $caller, 'default'); + $dataCollector->collectEncode('traceIdFive', 'data', 'format', ['foo' => 'bar'], 5.0, $caller, 'default'); + $dataCollector->collectDecode('traceIdSix', 'data', 'format', ['foo' => 'bar'], 6.0, $caller, 'default'); + $dataCollector->collectSerialize('traceIdSeven', 'data', 'format', ['foo' => 'bar'], 7.0, $caller, 'default'); $dataCollector->lateCollect(); @@ -282,7 +288,7 @@ public function testReset() $dataCollector = new SerializerDataCollector(); $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; - $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller); + $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 1.0, $caller, 'default'); $dataCollector->lateCollect(); $this->assertNotSame([], $dataCollector->getData()); @@ -295,10 +301,10 @@ public function testDoNotCollectPartialTraces() { $dataCollector = new SerializerDataCollector(); - $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 1.0); - $dataCollector->collectDenormalization('traceIdTwo', DateTimeNormalizer::class, 1.0); - $dataCollector->collectEncoding('traceIdThree', CsvEncoder::class, 10.0); - $dataCollector->collectDecoding('traceIdFour', JsonEncoder::class, 1.0); + $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 1.0, 'default'); + $dataCollector->collectDenormalization('traceIdTwo', DateTimeNormalizer::class, 1.0, 'default'); + $dataCollector->collectEncoding('traceIdThree', CsvEncoder::class, 10.0, 'default'); + $dataCollector->collectDecoding('traceIdFour', JsonEncoder::class, 1.0, 'default'); $dataCollector->lateCollect(); @@ -312,6 +318,84 @@ public function testDoNotCollectPartialTraces() $this->assertSame([], $data['decode']); } + public function testNamedSerializers() + { + $dataCollector = new SerializerDataCollector(); + + $caller = ['name' => 'Foo.php', 'file' => 'src/Foo.php', 'line' => 123]; + $dataCollector->collectNormalization('traceIdOne', DateTimeNormalizer::class, 3.0, 'default'); + $dataCollector->collectEncoding('traceIdOne', CsvEncoder::class, 4.0, 'default'); + $dataCollector->collectSerialize('traceIdOne', 'data', 'format', ['foo' => 'bar'], 7.0, $caller, 'default'); + $dataCollector->collectNormalization('traceIdTwo', ObjectNormalizer::class, 3.0, 'default'); + $dataCollector->collectNormalize('traceIdTwo', 'data', 'format', ['foo' => 'bar'], 5.0, $caller, 'default'); + + $dataCollector->collectEncoding('traceIdThree', JsonEncoder::class, 4.0, 'api'); + $dataCollector->collectEncode('traceIdThree', 'data', 'format', ['foo' => 'bar'], 5.0, $caller, 'api'); + $dataCollector->collectDenormalization('traceIdFour', DateTimeNormalizer::class, 3.0, 'api'); + $dataCollector->collectDecoding('traceIdFour', CsvEncoder::class, 4.0, 'api'); + $dataCollector->collectDeserialize('traceIdFour', 'data', 'type', 'format', ['foo' => 'bar'], 7.0, $caller, 'api'); + $dataCollector->collectDenormalization('traceIdFive', ObjectNormalizer::class, 3.0, 'api'); + $dataCollector->collectDenormalize('traceIdFive', 'data', 'type', 'format', ['foo' => 'bar'], 5.0, $caller, 'api'); + $dataCollector->collectDecoding('traceIdSix', JsonEncoder::class, 4.0, 'api'); + $dataCollector->collectDecode('traceIdSix', 'data', 'format', ['foo' => 'bar'], 5.0, $caller, 'api'); + + $dataCollector->lateCollect(); + + $this->assertSame(6, $dataCollector->getHandledCount()); + + $collectedData = $dataCollector->getData(); + + $this->assertSame('default', $collectedData['serialize'][0]['name']); + $this->assertSame('DateTimeNormalizer', $collectedData['serialize'][0]['normalizer']['class']); + $this->assertSame('CsvEncoder', $collectedData['serialize'][0]['encoder']['class']); + $this->assertSame('default', $collectedData['normalize'][0]['name']); + $this->assertSame('ObjectNormalizer', $collectedData['normalize'][0]['normalizer']['class']); + + $this->assertSame('api', $collectedData['encode'][0]['name']); + $this->assertSame('JsonEncoder', $collectedData['encode'][0]['encoder']['class']); + $this->assertSame('api', $collectedData['deserialize'][0]['name']); + $this->assertSame('DateTimeNormalizer', $collectedData['deserialize'][0]['normalizer']['class']); + $this->assertSame('CsvEncoder', $collectedData['deserialize'][0]['encoder']['class']); + $this->assertSame('api', $collectedData['denormalize'][0]['name']); + $this->assertSame('ObjectNormalizer', $collectedData['denormalize'][0]['normalizer']['class']); + $this->assertSame('api', $collectedData['decode'][0]['name']); + $this->assertSame('JsonEncoder', $collectedData['decode'][0]['encoder']['class']); + + $this->assertSame(['default', 'api'], $dataCollector->getSerializerNames()); + + $this->assertSame(2, $dataCollector->getHandledCount('default')); + + $collectedData = $dataCollector->getData('default'); + + $this->assertSame('default', $collectedData['serialize'][0]['name']); + $this->assertSame('DateTimeNormalizer', $collectedData['serialize'][0]['normalizer']['class']); + $this->assertSame('CsvEncoder', $collectedData['serialize'][0]['encoder']['class']); + $this->assertSame('default', $collectedData['normalize'][0]['name']); + $this->assertSame('ObjectNormalizer', $collectedData['normalize'][0]['normalizer']['class']); + + $this->assertEmpty($collectedData['encode']); + $this->assertEmpty($collectedData['deserialize']); + $this->assertEmpty($collectedData['denormalize']); + $this->assertEmpty($collectedData['decode']); + + $this->assertSame(4, $dataCollector->getHandledCount('api')); + + $collectedData = $dataCollector->getData('api'); + + $this->assertEmpty($collectedData['serialize']); + $this->assertEmpty($collectedData['normalize']); + + $this->assertSame('api', $collectedData['encode'][0]['name']); + $this->assertSame('JsonEncoder', $collectedData['encode'][0]['encoder']['class']); + $this->assertSame('api', $collectedData['deserialize'][0]['name']); + $this->assertSame('DateTimeNormalizer', $collectedData['deserialize'][0]['normalizer']['class']); + $this->assertSame('CsvEncoder', $collectedData['deserialize'][0]['encoder']['class']); + $this->assertSame('api', $collectedData['denormalize'][0]['name']); + $this->assertSame('ObjectNormalizer', $collectedData['denormalize'][0]['normalizer']['class']); + $this->assertSame('api', $collectedData['decode'][0]['name']); + $this->assertSame('JsonEncoder', $collectedData['decode'][0]['encoder']['class']); + } + /** * Cast cloned vars to be able to test nested values. */ diff --git a/Tests/Debug/TraceableEncoderTest.php b/Tests/Debug/TraceableEncoderTest.php index ec38c0ef5..2ac0b8f1f 100644 --- a/Tests/Debug/TraceableEncoderTest.php +++ b/Tests/Debug/TraceableEncoderTest.php @@ -36,12 +36,14 @@ public function testForwardsToEncoder() ->with('data', 'format', $this->isType('array')) ->willReturn('decoded'); - $this->assertSame('encoded', (new TraceableEncoder($encoder, new SerializerDataCollector()))->encode('data', 'format')); - $this->assertSame('decoded', (new TraceableEncoder($decoder, new SerializerDataCollector()))->decode('data', 'format')); + $this->assertSame('encoded', (new TraceableEncoder($encoder, new SerializerDataCollector(), 'default'))->encode('data', 'format')); + $this->assertSame('decoded', (new TraceableEncoder($decoder, new SerializerDataCollector(), 'default'))->decode('data', 'format')); } public function testCollectEncodingData() { + $serializerName = uniqid('name', true); + $encoder = $this->createMock(EncoderInterface::class); $decoder = $this->createMock(DecoderInterface::class); @@ -49,14 +51,14 @@ public function testCollectEncodingData() $dataCollector ->expects($this->once()) ->method('collectEncoding') - ->with($this->isType('string'), $encoder::class, $this->isType('float')); + ->with($this->isType('string'), $encoder::class, $this->isType('float'), $serializerName); $dataCollector ->expects($this->once()) ->method('collectDecoding') - ->with($this->isType('string'), $decoder::class, $this->isType('float')); + ->with($this->isType('string'), $decoder::class, $this->isType('float'), $serializerName); - (new TraceableEncoder($encoder, $dataCollector))->encode('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']); - (new TraceableEncoder($decoder, $dataCollector))->decode('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']); + (new TraceableEncoder($encoder, $dataCollector, $serializerName))->encode('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']); + (new TraceableEncoder($decoder, $dataCollector, $serializerName))->decode('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']); } public function testNotCollectEncodingDataIfNoDebugTraceId() @@ -68,22 +70,22 @@ public function testNotCollectEncodingDataIfNoDebugTraceId() $dataCollector->expects($this->never())->method('collectEncoding'); $dataCollector->expects($this->never())->method('collectDecoding'); - (new TraceableEncoder($encoder, $dataCollector))->encode('data', 'format'); - (new TraceableEncoder($decoder, $dataCollector))->decode('data', 'format'); + (new TraceableEncoder($encoder, $dataCollector, 'default'))->encode('data', 'format'); + (new TraceableEncoder($decoder, $dataCollector, 'default'))->decode('data', 'format'); } public function testCannotEncodeIfNotEncoder() { $this->expectException(\BadMethodCallException::class); - (new TraceableEncoder($this->createMock(DecoderInterface::class), new SerializerDataCollector()))->encode('data', 'format'); + (new TraceableEncoder($this->createMock(DecoderInterface::class), new SerializerDataCollector(), 'default'))->encode('data', 'format'); } public function testCannotDecodeIfNotDecoder() { $this->expectException(\BadMethodCallException::class); - (new TraceableEncoder($this->createMock(EncoderInterface::class), new SerializerDataCollector()))->decode('data', 'format'); + (new TraceableEncoder($this->createMock(EncoderInterface::class), new SerializerDataCollector(), 'default'))->decode('data', 'format'); } public function testSupports() @@ -94,8 +96,8 @@ public function testSupports() $decoder = $this->createMock(DecoderInterface::class); $decoder->method('supportsDecoding')->willReturn(true); - $traceableEncoder = new TraceableEncoder($encoder, new SerializerDataCollector()); - $traceableDecoder = new TraceableEncoder($decoder, new SerializerDataCollector()); + $traceableEncoder = new TraceableEncoder($encoder, new SerializerDataCollector(), 'default'); + $traceableDecoder = new TraceableEncoder($decoder, new SerializerDataCollector(), 'default'); $this->assertTrue($traceableEncoder->supportsEncoding('data')); $this->assertTrue($traceableDecoder->supportsDecoding('data')); diff --git a/Tests/Debug/TraceableNormalizerTest.php b/Tests/Debug/TraceableNormalizerTest.php index 307bc7b6f..56c161392 100644 --- a/Tests/Debug/TraceableNormalizerTest.php +++ b/Tests/Debug/TraceableNormalizerTest.php @@ -38,12 +38,14 @@ public function testForwardsToNormalizer() ->with('data', 'type', 'format', $this->isType('array')) ->willReturn('denormalized'); - $this->assertSame('normalized', (new TraceableNormalizer($normalizer, new SerializerDataCollector()))->normalize('data', 'format')); - $this->assertSame('denormalized', (new TraceableNormalizer($denormalizer, new SerializerDataCollector()))->denormalize('data', 'type', 'format')); + $this->assertSame('normalized', (new TraceableNormalizer($normalizer, new SerializerDataCollector(), 'default'))->normalize('data', 'format')); + $this->assertSame('denormalized', (new TraceableNormalizer($denormalizer, new SerializerDataCollector(), 'default'))->denormalize('data', 'type', 'format')); } public function testCollectNormalizationData() { + $serializerName = uniqid('name', true); + $normalizer = $this->createMock(NormalizerInterface::class); $normalizer->method('getSupportedTypes')->willReturn(['*' => false]); $denormalizer = $this->createMock(DenormalizerInterface::class); @@ -53,14 +55,14 @@ public function testCollectNormalizationData() $dataCollector ->expects($this->once()) ->method('collectNormalization') - ->with($this->isType('string'), $normalizer::class, $this->isType('float')); + ->with($this->isType('string'), $normalizer::class, $this->isType('float'), $serializerName); $dataCollector ->expects($this->once()) ->method('collectDenormalization') - ->with($this->isType('string'), $denormalizer::class, $this->isType('float')); + ->with($this->isType('string'), $denormalizer::class, $this->isType('float'), $serializerName); - (new TraceableNormalizer($normalizer, $dataCollector))->normalize('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']); - (new TraceableNormalizer($denormalizer, $dataCollector))->denormalize('data', 'type', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']); + (new TraceableNormalizer($normalizer, $dataCollector, $serializerName))->normalize('data', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']); + (new TraceableNormalizer($denormalizer, $dataCollector, $serializerName))->denormalize('data', 'type', 'format', [TraceableSerializer::DEBUG_TRACE_ID => 'debug']); } public function testNotCollectNormalizationDataIfNoDebugTraceId() @@ -74,22 +76,22 @@ public function testNotCollectNormalizationDataIfNoDebugTraceId() $dataCollector->expects($this->never())->method('collectNormalization'); $dataCollector->expects($this->never())->method('collectDenormalization'); - (new TraceableNormalizer($normalizer, $dataCollector))->normalize('data', 'format'); - (new TraceableNormalizer($denormalizer, $dataCollector))->denormalize('data', 'type', 'format'); + (new TraceableNormalizer($normalizer, $dataCollector, 'default'))->normalize('data', 'format'); + (new TraceableNormalizer($denormalizer, $dataCollector, 'default'))->denormalize('data', 'type', 'format'); } public function testCannotNormalizeIfNotNormalizer() { $this->expectException(\BadMethodCallException::class); - (new TraceableNormalizer($this->createMock(DenormalizerInterface::class), new SerializerDataCollector()))->normalize('data'); + (new TraceableNormalizer($this->createMock(DenormalizerInterface::class), new SerializerDataCollector(), 'default'))->normalize('data'); } public function testCannotDenormalizeIfNotDenormalizer() { $this->expectException(\BadMethodCallException::class); - (new TraceableNormalizer($this->createMock(NormalizerInterface::class), new SerializerDataCollector()))->denormalize('data', 'type'); + (new TraceableNormalizer($this->createMock(NormalizerInterface::class), new SerializerDataCollector(), 'default'))->denormalize('data', 'type'); } public function testSupports() @@ -102,8 +104,8 @@ public function testSupports() $denormalizer->method('getSupportedTypes')->willReturn(['*' => false]); $denormalizer->method('supportsDenormalization')->willReturn(true); - $traceableNormalizer = new TraceableNormalizer($normalizer, new SerializerDataCollector()); - $traceableDenormalizer = new TraceableNormalizer($denormalizer, new SerializerDataCollector()); + $traceableNormalizer = new TraceableNormalizer($normalizer, new SerializerDataCollector(), 'default'); + $traceableDenormalizer = new TraceableNormalizer($denormalizer, new SerializerDataCollector(), 'default'); $this->assertTrue($traceableNormalizer->supportsNormalization('data')); $this->assertTrue($traceableDenormalizer->supportsDenormalization('data', 'type')); diff --git a/Tests/Debug/TraceableSerializerTest.php b/Tests/Debug/TraceableSerializerTest.php index ea3c851c6..d697b270f 100644 --- a/Tests/Debug/TraceableSerializerTest.php +++ b/Tests/Debug/TraceableSerializerTest.php @@ -56,7 +56,7 @@ public function testForwardsToSerializer() ->with('data', 'format', $this->isType('array')) ->willReturn('decoded'); - $traceableSerializer = new TraceableSerializer($serializer, new SerializerDataCollector()); + $traceableSerializer = new TraceableSerializer($serializer, new SerializerDataCollector(), 'default'); $this->assertSame('serialized', $traceableSerializer->serialize('data', 'format')); $this->assertSame('deserialized', $traceableSerializer->deserialize('data', 'type', 'format')); @@ -68,33 +68,35 @@ public function testForwardsToSerializer() public function testCollectData() { + $serializerName = uniqid('name', true); + $dataCollector = $this->createMock(SerializerDataCollector::class); $dataCollector ->expects($this->once()) ->method('collectSerialize') - ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float')); + ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float'), $this->isType('array'), $serializerName); $dataCollector ->expects($this->once()) ->method('collectDeserialize') - ->with($this->isType('string'), 'data', 'type', 'format', $this->isType('array'), $this->isType('float')); + ->with($this->isType('string'), 'data', 'type', 'format', $this->isType('array'), $this->isType('float'), $this->isType('array'), $serializerName); $dataCollector ->expects($this->once()) ->method('collectNormalize') - ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float')); + ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float'), $this->isType('array'), $serializerName); $dataCollector ->expects($this->once()) ->method('collectDenormalize') - ->with($this->isType('string'), 'data', 'type', 'format', $this->isType('array'), $this->isType('float')); + ->with($this->isType('string'), 'data', 'type', 'format', $this->isType('array'), $this->isType('float'), $this->isType('array'), $serializerName); $dataCollector ->expects($this->once()) ->method('collectEncode') - ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float')); + ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float'), $this->isType('array'), $serializerName); $dataCollector ->expects($this->once()) ->method('collectDecode') - ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float')); + ->with($this->isType('string'), 'data', 'format', $this->isType('array'), $this->isType('float'), $this->isType('array'), $serializerName); - $traceableSerializer = new TraceableSerializer(new Serializer(), $dataCollector); + $traceableSerializer = new TraceableSerializer(new Serializer(), $dataCollector, $serializerName); $traceableSerializer->serialize('data', 'format'); $traceableSerializer->deserialize('data', 'type', 'format'); @@ -117,7 +119,7 @@ public function testAddDebugTraceIdInContext() }); } - $traceableSerializer = new TraceableSerializer($serializer, new SerializerDataCollector()); + $traceableSerializer = new TraceableSerializer($serializer, new SerializerDataCollector(), 'default'); $traceableSerializer->serialize('data', 'format'); $traceableSerializer->deserialize('data', 'format', 'type'); diff --git a/Tests/DependencyInjection/SerializerPassTest.php b/Tests/DependencyInjection/SerializerPassTest.php index 037eafdb6..b721b1ba4 100644 --- a/Tests/DependencyInjection/SerializerPassTest.php +++ b/Tests/DependencyInjection/SerializerPassTest.php @@ -17,7 +17,9 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Debug\TraceableNormalizer; +use Symfony\Component\Serializer\Debug\TraceableSerializer; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; +use Symfony\Component\Serializer\SerializerInterface; /** * Tests for the SerializerPass class. @@ -94,7 +96,7 @@ public function testBindSerializerDefaultContext() $this->assertEquals($bindings['array $defaultContext'], new BoundArgument(['enable_max_depth' => true], false)); } - public function testNormalizersAndEncodersAreDecoredAndOrderedWhenCollectingData() + public function testNormalizersAndEncodersAreDecoratedAndOrderedWhenCollectingData() { $container = new ContainerBuilder(); @@ -114,9 +116,525 @@ public function testNormalizersAndEncodersAreDecoredAndOrderedWhenCollectingData $this->assertEquals(TraceableNormalizer::class, $traceableNormalizerDefinition->getClass()); $this->assertEquals(new Reference('n'), $traceableNormalizerDefinition->getArgument(0)); $this->assertEquals(new Reference('serializer.data_collector'), $traceableNormalizerDefinition->getArgument(1)); + $this->assertSame('default', $traceableNormalizerDefinition->getArgument(2)); $this->assertEquals(TraceableEncoder::class, $traceableEncoderDefinition->getClass()); $this->assertEquals(new Reference('e'), $traceableEncoderDefinition->getArgument(0)); $this->assertEquals(new Reference('serializer.data_collector'), $traceableEncoderDefinition->getArgument(1)); + $this->assertSame('default', $traceableEncoderDefinition->getArgument(2)); + } + + /** + * @dataProvider provideDefaultSerializerTagsData + */ + public function testDefaultSerializerTagsAreResolvedCorrectly( + array $normalizerTagAttributes, + array $encoderTagAttributes, + array $expectedNormalizerTags, + array $expectedEncoderTags, + ) { + $container = new ContainerBuilder(); + + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', []); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n0')->addTag('serializer.normalizer', ['serializer' => 'default']); + $container->register('e0')->addTag('serializer.encoder', ['serializer' => 'default']); + + $normalizerDefinition = $container->register('n1')->addTag('serializer.normalizer', $normalizerTagAttributes); + $encoderDefinition = $container->register('e1')->addTag('serializer.encoder', $encoderTagAttributes); + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertSame($expectedNormalizerTags, $normalizerDefinition->getTag('serializer.normalizer.default')); + $this->assertSame($expectedEncoderTags, $encoderDefinition->getTag('serializer.encoder.default')); + } + + public static function provideDefaultSerializerTagsData(): iterable + { + yield 'include no name' => [ + [], + [], + [[]], + [[]], + ]; + + yield 'include name' => [ + ['serializer' => 'default'], + ['serializer' => 'default'], + [[]], + [[]], + ]; + + yield 'include built-in with different name' => [ + ['built_in' => true, 'serializer' => 'api'], + ['built_in' => true, 'serializer' => 'api'], + [[]], + [[]], + ]; + + yield 'include no name with priority' => [ + ['priority' => 200], + ['priority' => 100], + [['priority' => 200]], + [['priority' => 100]], + ]; + + yield 'include name with priority' => [ + ['serializer' => 'default', 'priority' => 200], + ['serializer' => 'default', 'priority' => 100], + [['priority' => 200]], + [['priority' => 100]], + ]; + + yield 'include wildcard' => [ + ['serializer' => '*'], + ['serializer' => '*'], + [[]], + [[]], + ]; + + yield 'is unique when built-in with name' => [ + ['built_in' => true, 'serializer' => 'default'], + ['built_in' => true, 'serializer' => 'default'], + [[]], + [[]], + ]; + + yield 'do not include different name' => [ + ['serializer' => 'api'], + ['serializer' => 'api'], + [], + [], + ]; + } + + /** + * @dataProvider provideNamedSerializerTagsData + */ + public function testNamedSerializerTagsAreResolvedCorrectly( + array $config, + array $normalizerTagAttributes, + array $encoderTagAttributes, + array $expectedNormalizerTags, + array $expectedEncoderTags, + ) { + $container = new ContainerBuilder(); + + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', ['api' => $config]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n0')->addTag('serializer.normalizer', ['serializer' => ['default', 'api']]); + $container->register('e0')->addTag('serializer.encoder', ['serializer' => ['default', 'api']]); + + $normalizerDefinition = $container->register('n1')->addTag('serializer.normalizer', $normalizerTagAttributes); + $encoderDefinition = $container->register('e1')->addTag('serializer.encoder', $encoderTagAttributes); + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertSame($expectedNormalizerTags, $normalizerDefinition->getTag('serializer.normalizer.api')); + $this->assertSame($expectedEncoderTags, $encoderDefinition->getTag('serializer.encoder.api')); + } + + public static function provideNamedSerializerTagsData(): iterable + { + yield 'include built-in' => [ + ['include_built_in_normalizers' => true, 'include_built_in_encoders' => true], + ['built_in' => true], + ['built_in' => true], + [[]], + [[]], + ]; + + yield 'include built-in normalizers only' => [ + ['include_built_in_normalizers' => true, 'include_built_in_encoders' => false], + ['built_in' => true], + ['built_in' => true], + [[]], + [], + ]; + + yield 'include built-in encoders only' => [ + ['include_built_in_normalizers' => false, 'include_built_in_encoders' => true], + ['built_in' => true], + ['built_in' => true], + [], + [[]], + ]; + + yield 'include name' => [ + ['include_built_in_normalizers' => false, 'include_built_in_encoders' => false], + ['serializer' => 'api'], + ['serializer' => 'api'], + [[]], + [[]], + ]; + + yield 'include name with priority' => [ + ['include_built_in_normalizers' => false, 'include_built_in_encoders' => false], + ['serializer' => 'api', 'priority' => 200], + ['serializer' => 'api', 'priority' => 100], + [['priority' => 200]], + [['priority' => 100]], + ]; + + yield 'include wildcard' => [ + ['include_built_in_normalizers' => false, 'include_built_in_encoders' => false], + ['serializer' => '*'], + ['serializer' => '*'], + [[]], + [[]], + ]; + + yield 'do not include when include built-in not set' => [ + [], + ['built_in' => true], + ['built_in' => true], + [], + [], + ]; + + yield 'do not include not built-in and no name' => [ + ['include_built_in_normalizers' => false, 'include_built_in_encoders' => false], + [], + [], + [], + [], + ]; + + yield 'do not include different name' => [ + ['include_built_in_normalizers' => false, 'include_built_in_encoders' => false], + ['serializer' => 'api2'], + ['serializer' => 'api2'], + [], + [], + ]; + } + + public function testMultipleNamedSerializerTagsAreResolvedCorrectly() + { + $container = new ContainerBuilder(); + + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => [], + 'api2' => [], + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n0')->addTag('serializer.normalizer', ['serializer' => 'default']); + $container->register('e0')->addTag('serializer.encoder', ['serializer' => 'default']); + + $normalizerDefinition = $container->register('n1')->addTag('serializer.normalizer', ['serializer' => ['api', 'api2']]); + $encoderDefinition = $container->register('e1') + ->addTag('serializer.encoder', ['serializer' => ['api', 'api2']]) + ->addTag('serializer.encoder', ['serializer' => ['api', 'api2'], 'priority' => 100]) + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertTrue($normalizerDefinition->hasTag('serializer.normalizer.api')); + $this->assertCount(1, $normalizerDefinition->getTag('serializer.normalizer.api')); + $this->assertTrue($normalizerDefinition->hasTag('serializer.normalizer.api2')); + $this->assertCount(1, $normalizerDefinition->getTag('serializer.normalizer.api2')); + + $this->assertTrue($encoderDefinition->hasTag('serializer.encoder.api')); + $this->assertCount(2, $encoderDefinition->getTag('serializer.encoder.api')); + $this->assertTrue($encoderDefinition->hasTag('serializer.encoder.api2')); + $this->assertCount(2, $encoderDefinition->getTag('serializer.encoder.api2')); + } + + public function testThrowExceptionWhenNoNormalizersForNamedSerializers() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => [], + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n0')->addTag('serializer.normalizer'); + $container->register('e0')->addTag('serializer.encoder', ['serializer' => '*']); + + $serializerPass = new SerializerPass(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The named serializer "api" requires at least one registered normalizer. Tag the normalizers as "serializer.normalizer" with the "serializer" attribute set to "api".'); + + $serializerPass->process($container); + } + + public function testThrowExceptionWhenNoEncodersForNamedSerializers() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => [], + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n0')->addTag('serializer.normalizer', ['serializer' => '*']); + $container->register('e0')->addTag('serializer.encoder'); + + $serializerPass = new SerializerPass(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The named serializer "api" requires at least one registered encoder. Tag the encoders as "serializer.encoder" with the "serializer" attribute set to "api".'); + + $serializerPass->process($container); + } + + /** + * @testWith [null] + * ["some.converter"] + */ + public function testChildNameConverterIsNotBuiltWhenExpected(?string $nameConverter) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.name_converter', $nameConverter); + $container->setParameter('.serializer.named_serializers', [ + 'api' => ['name_converter' => $nameConverter], + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n')->addTag('serializer.normalizer', ['serializer' => '*']); + $container->register('e')->addTag('serializer.encoder', ['serializer' => '*']); + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertFalse($container->hasDefinition('serializer.name_converter.metadata_aware.'.ContainerBuilder::hash($nameConverter))); + } + + /** + * @dataProvider provideChildNameConverterCases + */ + public function testChildNameConverterIsBuiltWhenExpected( + ?string $defaultSerializerNameConverter, + ?string $namedSerializerNameConverter, + string $nameConverterIdExists, + string $nameConverterIdDoesNotExist, + array $nameConverterArguments, + ) { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.name_converter', $defaultSerializerNameConverter); + $container->setParameter('.serializer.named_serializers', [ + 'api' => ['name_converter' => $namedSerializerNameConverter], + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n')->addTag('serializer.normalizer', ['serializer' => '*']); + $container->register('e')->addTag('serializer.encoder', ['serializer' => '*']); + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertFalse($container->hasDefinition($nameConverterIdExists)); + $this->assertTrue($container->hasDefinition($nameConverterIdDoesNotExist)); + $this->assertEquals($nameConverterArguments, $container->getDefinition($nameConverterIdDoesNotExist)->getArguments()); + } + + public static function provideChildNameConverterCases(): iterable + { + $withNull = 'serializer.name_converter.metadata_aware.'.ContainerBuilder::hash(null); + $withConverter = 'serializer.name_converter.metadata_aware.'.ContainerBuilder::hash('some.converter'); + + yield [null, 'some.converter', $withNull, $withConverter, [new Reference('some.converter')]]; + yield ['some.converter', null, $withConverter, $withNull, []]; + } + + /** + * @dataProvider provideDifferentNamedSerializerConfigsCases + */ + public function testNamedSerializersCreateNewServices( + array $defaultSerializerDefaultContext, + ?string $defaultSerializerNameConverter, + array $namedSerializerConfig, + string $nameConverterId, + ) { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('serializer.default_context', $defaultSerializerDefaultContext); + $container->setParameter('.serializer.name_converter', $defaultSerializerNameConverter); + $container->setParameter('.serializer.named_serializers', [ + 'api' => $namedSerializerConfig, + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n') + ->addArgument(new Reference('serializer.name_converter.metadata_aware')) + ->addTag('serializer.normalizer', ['serializer' => '*']) + ; + $container->register('e') + ->addArgument(new Reference('serializer.name_converter.metadata_aware')) + ->addTag('serializer.encoder', ['serializer' => '*']) + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertEquals([new Reference('n.api')], $container->getDefinition('serializer.api')->getArgument(0)); + $this->assertEquals(new Reference($nameConverterId), $container->getDefinition('n.api')->getArgument(0)); + $this->assertEquals([new Reference('e.api')], $container->getDefinition('serializer.api')->getArgument(1)); + $this->assertEquals(new Reference($nameConverterId), $container->getDefinition('e.api')->getArgument(0)); + } + + public static function provideDifferentNamedSerializerConfigsCases(): iterable + { + yield [ + ['a' => true, 'b' => 3], + null, + ['default_context' => ['c' => 3, 'a' => true]], + 'serializer.name_converter.metadata_aware', + ]; + yield [ + [], + 'some.converter', + ['name_converter' => null], + 'serializer.name_converter.metadata_aware.'.ContainerBuilder::hash(null), + ]; + yield [ + ['a' => true, 'b' => 3], + null, + ['default_context' => ['c' => 3, 'a' => true], 'name_converter' => 'some.converter'], + 'serializer.name_converter.metadata_aware.'.ContainerBuilder::hash('some.converter'), + ]; + } + + public function testServicesAreOrderedAccordingToPriorityForNamedSerializers() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => [], + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n2') + ->addTag('serializer.normalizer', ['serializer' => '*', 'priority' => 100]) + ->addTag('serializer.encoder', ['serializer' => '*', 'priority' => 100]) + ; + $container->register('n1') + ->addTag('serializer.normalizer', ['serializer' => 'api', 'priority' => 200]) + ->addTag('serializer.encoder', ['serializer' => 'api', 'priority' => 200]) + ; + $container->register('n3') + ->addTag('serializer.normalizer', ['serializer' => 'api']) + ->addTag('serializer.encoder', ['serializer' => 'api']) + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertTrue($container->hasDefinition('serializer.api')); + $definition = $container->getDefinition('serializer.api'); + + $expected = [ + new Reference('n1.api'), + new Reference('n2.api'), + new Reference('n3.api'), + ]; + $this->assertEquals($expected, $definition->getArgument(0)); + $this->assertEquals($expected, $definition->getArgument(1)); + } + + public function testBindSerializerDefaultContextToNamedSerializers() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => ['default_context' => $defaultContext = ['enable_max_depth' => true]], + ]); + + $container->register('serializer')->setArguments([null, null]); + $definition = $container->register('n1') + ->addTag('serializer.normalizer', ['serializer' => '*']) + ->addTag('serializer.encoder', ['serializer' => '*']) + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertEmpty($definition->getBindings()); + + $bindings = $container->getDefinition('n1.api')->getBindings(); + $this->assertArrayHasKey('array $defaultContext', $bindings); + $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($defaultContext, false)); + } + + public function testNamedSerializersAreRegistered() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => [], + 'api2' => [], + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n')->addTag('serializer.normalizer', ['serializer' => '*']); + $container->register('e')->addTag('serializer.encoder', ['serializer' => '*']); + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertFalse($container->hasAlias(\sprintf('%s $defaultSerializer', SerializerInterface::class))); + + $this->assertTrue($container->hasDefinition('serializer.api')); + $this->assertTrue($container->hasAlias(\sprintf('%s $apiSerializer', SerializerInterface::class))); + $this->assertTrue($container->hasDefinition('serializer.api2')); + $this->assertTrue($container->hasAlias(\sprintf('%s $api2Serializer', SerializerInterface::class))); + } + + public function testNormalizersAndEncodersAreDecoratedAndOrderedWhenCollectingDataForNamedSerializers() + { + $container = new ContainerBuilder(); + + $container->setParameter('kernel.debug', true); + $container->setParameter('.serializer.named_serializers', [ + 'api' => ['default_context' => ['enable_max_depth' => true]], + ]); + $container->register('serializer.data_collector'); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n')->addTag('serializer.normalizer', ['serializer' => '*']); + $container->register('e')->addTag('serializer.encoder', ['serializer' => '*']); + + $container->register('debug.serializer', TraceableSerializer::class) + ->setDecoratedService('serializer') + ->setArguments([ + new Reference('debug.serializer.inner'), + new Reference('serializer.data_collector'), + 'default', + ]) + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $traceableNormalizerDefinition = $container->getDefinition('.debug.serializer.normalizer.n.api'); + $traceableEncoderDefinition = $container->getDefinition('.debug.serializer.encoder.e.api'); + + $traceableSerializerDefinition = $container->getDefinition('debug.serializer.api'); + $this->assertSame('serializer.api', $traceableSerializerDefinition->getDecoratedService()[0]); + $this->assertEquals(new Reference('debug.serializer.api.inner'), $traceableSerializerDefinition->getArgument(0)); + $this->assertSame('api', $traceableSerializerDefinition->getArgument(2)); + + $this->assertEquals(TraceableNormalizer::class, $traceableNormalizerDefinition->getClass()); + $this->assertEquals(new Reference('n.api'), $traceableNormalizerDefinition->getArgument(0)); + $this->assertEquals(new Reference('serializer.data_collector'), $traceableNormalizerDefinition->getArgument(1)); + $this->assertSame('api', $traceableNormalizerDefinition->getArgument(2)); + + $this->assertEquals(TraceableEncoder::class, $traceableEncoderDefinition->getClass()); + $this->assertEquals(new Reference('e.api'), $traceableEncoderDefinition->getArgument(0)); + $this->assertEquals(new Reference('serializer.data_collector'), $traceableEncoderDefinition->getArgument(1)); + $this->assertSame('api', $traceableEncoderDefinition->getArgument(2)); } } diff --git a/composer.json b/composer.json index 0092a9643..4d3af9358 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "symfony/cache": "^6.4|^7.0", "symfony/config": "^6.4|^7.0", "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dependency-injection": "^7.2", "symfony/error-handler": "^6.4|^7.0", "symfony/filesystem": "^6.4|^7.0", "symfony/form": "^6.4|^7.0", From 267cddf01bc284d6079818e71294b7452e2e4dab Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 19 Sep 2024 09:56:35 +0200 Subject: [PATCH 069/104] Switch to ExpectUserDeprecationMessageTrait --- Tests/Context/Encoder/CsvEncoderContextBuilderTest.php | 8 ++++---- Tests/Encoder/CsvEncoderTest.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/Context/Encoder/CsvEncoderContextBuilderTest.php b/Tests/Context/Encoder/CsvEncoderContextBuilderTest.php index bcaaf2a88..fe39feb81 100644 --- a/Tests/Context/Encoder/CsvEncoderContextBuilderTest.php +++ b/Tests/Context/Encoder/CsvEncoderContextBuilderTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Context\Encoder; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Exception\InvalidArgumentException; @@ -22,7 +22,7 @@ */ class CsvEncoderContextBuilderTest extends TestCase { - use ExpectDeprecationTrait; + use ExpectUserDeprecationMessageTrait; private CsvEncoderContextBuilder $contextBuilder; @@ -127,7 +127,7 @@ public function testCannotSetMultipleBytesAsEnclosure() */ public function testCannotSetMultipleBytesAsEscapeChar() { - $this->expectDeprecation('Since symfony/serializer 7.2: The "Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder::withEscapeChar" method is deprecated. It will be removed in 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/serializer 7.2: The "Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder::withEscapeChar" method is deprecated. It will be removed in 8.0.'); $this->expectException(InvalidArgumentException::class); $this->contextBuilder->withEscapeChar('ọ'); @@ -138,7 +138,7 @@ public function testCannotSetMultipleBytesAsEscapeChar() */ public function testWithEscapeCharIsDeprecated() { - $this->expectDeprecation('Since symfony/serializer 7.2: The "Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder::withEscapeChar" method is deprecated. It will be removed in 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/serializer 7.2: The "Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder::withEscapeChar" method is deprecated. It will be removed in 8.0.'); $context = $this->contextBuilder->withEscapeChar('\\'); $this->assertSame(['csv_escape_char' => '\\'], $context->toArray()); diff --git a/Tests/Encoder/CsvEncoderTest.php b/Tests/Encoder/CsvEncoderTest.php index e250d1c61..048d790b0 100644 --- a/Tests/Encoder/CsvEncoderTest.php +++ b/Tests/Encoder/CsvEncoderTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Encoder; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Exception\UnexpectedValueException; @@ -21,7 +21,7 @@ */ class CsvEncoderTest extends TestCase { - use ExpectDeprecationTrait; + use ExpectUserDeprecationMessageTrait; private CsvEncoder $encoder; @@ -713,7 +713,7 @@ public function testEndOfLinePassedInConstructor() */ public function testPassingNonEmptyEscapeCharIsDeprecated() { - $this->expectDeprecation('Since symfony/serializer 7.2: Setting the "csv_escape_char" option is deprecated. The option will be removed in 8.0.'); + $this->expectUserDeprecationMessage('Since symfony/serializer 7.2: Setting the "csv_escape_char" option is deprecated. The option will be removed in 8.0.'); $encoder = new CsvEncoder(['csv_escape_char' => '@']); $this->assertSame( From df514ef72c3540217c9819e4479aa55acc431d27 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 19 Sep 2024 23:14:15 +0200 Subject: [PATCH 070/104] Make more data providers static --- Tests/Normalizer/AbstractObjectNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 26f9be4ad..8ec4ac1d3 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1211,7 +1211,7 @@ public function testDenormalizeBooleanTypeWithFilterBool(array $data, ?bool $exp $this->assertSame($expectedFoo, $dummy->foo); } - public function provideDenormalizeWithFilterBoolData(): array + public static function provideDenormalizeWithFilterBoolData(): array { return [ [['foo' => 'true'], true], From 0f100dfa5b3bc8b052d15940d1168e5fa1e1a59a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 20 Sep 2024 09:50:07 +0200 Subject: [PATCH 071/104] Mutate remaining data providers to static ones --- Tests/Normalizer/AbstractObjectNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index b700f6ee7..e413be0c1 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -701,7 +701,7 @@ public function testDenormalizeBooleanTypesWithNotMatchingData(array $data, stri $normalizer->denormalize($data, $type); } - public function provideBooleanTypesData() + public static function provideBooleanTypesData() { return [ [['foo' => true], FalsePropertyDummy::class], From 71d6e1f70f00752d1469d0f5e83b0a51716f288b Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 20 Sep 2024 13:43:16 +0200 Subject: [PATCH 072/104] fix tests --- Tests/Normalizer/AbstractObjectNormalizerTest.php | 5 +++++ composer.json | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index d7e0f7d25..b4f5c103c 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -15,6 +15,7 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyDocBlockExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Attribute\Context; @@ -1274,6 +1275,10 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string public function testDenormalizeTemplateType() { + if (!interface_exists(PropertyDocBlockExtractorInterface::class)) { + $this->markTestSkipped('The PropertyInfo component before Symfony 7.1 does not support template types.'); + } + $normalizer = new class ( classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()), propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [new PhpStanExtractor(), new ReflectionExtractor()]) diff --git a/composer.json b/composer.json index 0092a9643..948fa36fa 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ }, "require-dev": { "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "phpstan/phpdoc-parser": "^1.0", "seld/jsonlint": "^1.10", "symfony/cache": "^6.4|^7.0", "symfony/config": "^6.4|^7.0", @@ -37,7 +38,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.1", + "symfony/type-info": "^7.1.5", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", @@ -50,6 +51,7 @@ "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", + "symfony/type-info": "<7.1.5", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4" From c86076815cd39e140b30ef68f02c21539ed0e4da Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 23 Sep 2024 11:30:23 +0200 Subject: [PATCH 073/104] make data provider static --- Tests/Normalizer/Features/ContextMetadataTestTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Normalizer/Features/ContextMetadataTestTrait.php b/Tests/Normalizer/Features/ContextMetadataTestTrait.php index 10f5a0030..787157628 100644 --- a/Tests/Normalizer/Features/ContextMetadataTestTrait.php +++ b/Tests/Normalizer/Features/ContextMetadataTestTrait.php @@ -77,7 +77,7 @@ public function testContextMetadataContextDenormalize(string $contextMetadataDum self::assertEquals('2011-07-28', $dummy->date->format('Y-m-d'), 'a specific denormalization context is used for this group'); } - public function contextMetadataDummyProvider(): array + public static function contextMetadataDummyProvider(): array { return [ [ContextMetadataDummy::class], From 2946530731d67a6a45aef8379a0879807616059e Mon Sep 17 00:00:00 2001 From: Aurimas Date: Tue, 24 Sep 2024 09:20:27 +0300 Subject: [PATCH 074/104] [Serializer] Readd AdvancedNameConverterInterface to MetadataAwareNameConverter. --- NameConverter/MetadataAwareNameConverter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NameConverter/MetadataAwareNameConverter.php b/NameConverter/MetadataAwareNameConverter.php index eec3b42ce..327d92dc1 100644 --- a/NameConverter/MetadataAwareNameConverter.php +++ b/NameConverter/MetadataAwareNameConverter.php @@ -18,7 +18,7 @@ /** * @author Fabien Bourigault */ -final class MetadataAwareNameConverter implements NameConverterInterface +final class MetadataAwareNameConverter implements AdvancedNameConverterInterface { /** * @var array> From 8f02a9837542c71b91f40a6cf7f38fa67b2b5f29 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 18 Sep 2024 19:28:51 +0200 Subject: [PATCH 075/104] [Serializer] Deprecate `AdvancedNameConverterInterface` --- CHANGELOG.md | 1 + NameConverter/AdvancedNameConverterInterface.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a14b50f..a04c323d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * Add support for configuring multiple serializer instances with different default contexts, name converters, sets of normalizers and encoders * Add support for collection profiles of multiple serializer instances + * Deprecate `AdvancedNameConverterInterface`, use `NameConverterInterface` instead 7.1 --- diff --git a/NameConverter/AdvancedNameConverterInterface.php b/NameConverter/AdvancedNameConverterInterface.php index 1e74f4d20..975d28fd3 100644 --- a/NameConverter/AdvancedNameConverterInterface.php +++ b/NameConverter/AdvancedNameConverterInterface.php @@ -15,6 +15,8 @@ * Gives access to the class, the format and the context in the property name converters. * * @author Kévin Dunglas + * + * @deprecated since Symfony 7.2, use NameConverterInterface instead */ interface AdvancedNameConverterInterface extends NameConverterInterface { From 460c5df9fb6c39d10d5b7f386e4feae4b6370221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Sep 2024 11:24:18 +0200 Subject: [PATCH 076/104] Add PR template and auto-close PR on subtree split repositories --- .gitattributes | 3 +-- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ .github/workflows/close-pull-request.yml | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/close-pull-request.yml diff --git a/.gitattributes b/.gitattributes index 84c7add05..14c3c3594 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..4689c4dad --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 000000000..e55b47817 --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! From e4c7264a86225ccf7e71cda8070667f30e968d06 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 26 Sep 2024 10:09:09 +0200 Subject: [PATCH 077/104] Remove unused imports --- Tests/Fixtures/DummyObjectWithEnumProperty.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/Fixtures/DummyObjectWithEnumProperty.php b/Tests/Fixtures/DummyObjectWithEnumProperty.php index f2677195f..70c6cff7b 100644 --- a/Tests/Fixtures/DummyObjectWithEnumProperty.php +++ b/Tests/Fixtures/DummyObjectWithEnumProperty.php @@ -2,8 +2,6 @@ namespace Symfony\Component\Serializer\Tests\Fixtures; -use Symfony\Component\Serializer\Tests\Fixtures\StringBackedEnumDummy; - class DummyObjectWithEnumProperty { public StringBackedEnumDummy $get; From 8be421505938b11a0ca4f656e4322232236386f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Laugks?= Date: Fri, 13 Sep 2024 13:10:52 +0200 Subject: [PATCH 078/104] [Serializer] Fix `ObjectNormalizer` gives warnings on normalizing with public static property --- Normalizer/ObjectNormalizer.php | 2 +- Tests/Normalizer/ObjectNormalizerTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Normalizer/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index a8c887e50..c8473a62c 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -197,7 +197,7 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string if ($context['_read_attributes'] ?? true) { if (!isset(self::$isReadableCache[$class.$attribute])) { - self::$isReadableCache[$class.$attribute] = (\is_object($classOrObject) && $this->propertyAccessor->isReadable($classOrObject, $attribute)) || $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute); + self::$isReadableCache[$class.$attribute] = $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute) || (\is_object($classOrObject) && $this->propertyAccessor->isReadable($classOrObject, $attribute)); } return self::$isReadableCache[$class.$attribute]; diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index e314ac74f..5b028e8c0 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -927,6 +927,16 @@ public function testDenormalizeWithPropertyPath() $this->assertEquals($expected, $obj); } + + public function testObjectNormalizerWithAttributeLoaderAndObjectHasStaticProperty() + { + $class = new class { + public static string $foo; + }; + + $normalizer = new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader())); + $this->assertSame([], $normalizer->normalize($class)); + } } class ProxyObjectDummy extends ObjectDummy From e71f70329e19a18607ea0a53391a4837e10218c8 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 3 Oct 2024 14:15:19 +0200 Subject: [PATCH 079/104] Various CS fix for consistency --- Tests/Normalizer/AbstractObjectNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 28324c48b..499fa8bff 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1230,7 +1230,7 @@ public static function provideDenormalizeWithFilterBoolData(): array public function testDenormalizeArrayObject() { - $normalizer = new class() extends AbstractObjectNormalizerDummy { + $normalizer = new class extends AbstractObjectNormalizerDummy { public function __construct() { parent::__construct(null, null, new PhpDocExtractor()); From 7c0c971e7cd929e6b34bf991cc16f245979885e6 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 14 Oct 2024 20:03:05 +0200 Subject: [PATCH 080/104] Reduce common control flows --- Command/DebugCommand.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index 3e70c93a5..7df4d6bc8 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -75,15 +75,14 @@ private function dumpSerializerDataForClass(InputInterface $input, OutputInterfa ]; } + $io->section($title); + if (!$rows) { - $io->section($title); $io->text('No Serializer data were found for this class.'); return; } - $io->section($title); - $table = new Table($output); $table->setHeaders(['Property', 'Options']); $table->setRows($rows); From b8f4851e37b94914bf269f32a720a203e7501a52 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 14 Aug 2024 15:51:51 +0200 Subject: [PATCH 081/104] [TypeInfo] Redesign Type methods and nullability --- Normalizer/AbstractObjectNormalizer.php | 41 +++++++++++++++---------- Normalizer/ArrayDenormalizer.php | 7 ++++- composer.json | 4 +-- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 9db241298..82aaa290d 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -32,12 +32,14 @@ use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -use Symfony\Component\TypeInfo\Exception\LogicException as TypeInfoLogicException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\NullableType; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; use Symfony\Component\TypeInfo\TypeIdentifier; /** @@ -644,11 +646,9 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; - $isUnionType = $type->asNonNullable() instanceof UnionType; $e = null; $extraAttributesException = null; $missingConstructorArgumentsException = null; - $isNullable = false; $types = match (true) { $type instanceof IntersectionType => throw new LogicException('Unable to handle intersection type.'), @@ -667,11 +667,13 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $collectionValueType = $t->getCollectionValueType(); } - $t = $t->getBaseType(); + while ($t instanceof WrappingTypeInterface) { + $t = $t->getWrappedType(); + } // Fix a collection that contains the only one element // This is special to xml format only - if ('xml' === $format && $collectionValueType && !$collectionValueType->isA(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) { + if ('xml' === $format && $collectionValueType && !$collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) { $data = [$data]; } @@ -694,8 +696,6 @@ private function validateAndDenormalize(Type $type, string $currentClass, string if (TypeIdentifier::STRING === $typeIdentifier) { return ''; } - - $isNullable = $isNullable ?: $type->isNullable(); } switch ($typeIdentifier) { @@ -731,10 +731,9 @@ private function validateAndDenormalize(Type $type, string $currentClass, string } if ($collectionValueType) { - try { - $collectionValueBaseType = $collectionValueType->getBaseType(); - } catch (TypeInfoLogicException) { - $collectionValueBaseType = Type::mixed(); + $collectionValueBaseType = $collectionValueType; + while ($collectionValueBaseType instanceof WrappingTypeInterface) { + $collectionValueBaseType = $collectionValueBaseType->getWrappedType(); } if ($collectionValueBaseType instanceof ObjectType) { @@ -742,15 +741,25 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $class = $collectionValueBaseType->getClassName().'[]'; $context['key_type'] = $collectionKeyType; $context['value_type'] = $collectionValueType; - } elseif (TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) { + } elseif ($collectionValueBaseType instanceof BuiltinType && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) { // get inner type for any nested array $innerType = $collectionValueType; + if ($innerType instanceof NullableType) { + $innerType = $innerType->getWrappedType(); + } // note that it will break for any other builtinType $dimensions = '[]'; while ($innerType instanceof CollectionType) { $dimensions .= '[]'; $innerType = $innerType->getCollectionValueType(); + if ($innerType instanceof NullableType) { + $innerType = $innerType->getWrappedType(); + } + } + + while ($innerType instanceof WrappingTypeInterface) { + $innerType = $innerType->getWrappedType(); } if ($innerType instanceof ObjectType) { @@ -832,17 +841,17 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return $data; } } catch (NotNormalizableValueException|InvalidArgumentException $e) { - if (!$isUnionType && !$isNullable) { + if (!$type instanceof UnionType) { throw $e; } } catch (ExtraAttributesException $e) { - if (!$isUnionType && !$isNullable) { + if (!$type instanceof UnionType) { throw $e; } $extraAttributesException ??= $e; } catch (MissingConstructorArgumentsException $e) { - if (!$isUnionType && !$isNullable) { + if (!$type instanceof UnionType) { throw $e; } @@ -862,7 +871,7 @@ private function validateAndDenormalize(Type $type, string $currentClass, string throw $missingConstructorArgumentsException; } - if (!$isUnionType && $e) { + if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) { throw $e; } diff --git a/Normalizer/ArrayDenormalizer.php b/Normalizer/ArrayDenormalizer.php index 94de2de34..08fae04df 100644 --- a/Normalizer/ArrayDenormalizer.php +++ b/Normalizer/ArrayDenormalizer.php @@ -16,7 +16,9 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Denormalizes arrays of objects. @@ -54,7 +56,10 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $typeIdentifiers = []; if (null !== $keyType = ($context['key_type'] ?? null)) { if ($keyType instanceof Type) { - $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + /** @var list|BuiltinType> */ + $keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; + + $typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes); } else { $typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]); } diff --git a/composer.json b/composer.json index 8691e2240..bb325dfef 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.1.5", + "symfony/type-info": "^7.2", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", @@ -51,7 +51,7 @@ "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", - "symfony/type-info": "<7.1.5", + "symfony/type-info": "<7.2", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4" From 56bcd41f8e8792b15ca363b730e26c7c536a7873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 10 Oct 2024 12:40:40 +0200 Subject: [PATCH 082/104] [Serializer] Improve `AttributeLoader` --- Mapping/Loader/AttributeLoader.php | 69 +++++++++++++----------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index 272e236b6..6bcbed36c 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -58,23 +58,12 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $attributesMetadata = $classMetadata->getAttributesMetadata(); foreach ($this->loadAttributes($reflectionClass) as $annotation) { - if ($annotation instanceof DiscriminatorMap) { - $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( - $annotation->getTypeProperty(), - $annotation->getMapping() - )); - continue; - } - - if ($annotation instanceof Groups) { - $classGroups = $annotation->getGroups(); - - continue; - } - - if ($annotation instanceof Context) { - $classContextAnnotation = $annotation; - } + match (true) { + $annotation instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($annotation->getTypeProperty(), $annotation->getMapping())), + $annotation instanceof Groups => $classGroups = $annotation->getGroups(), + $annotation instanceof Context => $classContextAnnotation = $annotation, + default => null, + }; } foreach ($reflectionClass->getProperties() as $property) { @@ -83,33 +72,35 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $classMetadata->addAttributeMetadata($attributesMetadata[$property->name]); } + $attributeMetadata = $attributesMetadata[$property->name]; if ($property->getDeclaringClass()->name === $className) { if ($classContextAnnotation) { - $this->setAttributeContextsForGroups($classContextAnnotation, $attributesMetadata[$property->name]); + $this->setAttributeContextsForGroups($classContextAnnotation, $attributeMetadata); } foreach ($classGroups as $group) { - $attributesMetadata[$property->name]->addGroup($group); + $attributeMetadata->addGroup($group); } foreach ($this->loadAttributes($property) as $annotation) { + $loaded = true; + if ($annotation instanceof Groups) { foreach ($annotation->getGroups() as $group) { - $attributesMetadata[$property->name]->addGroup($group); + $attributeMetadata->addGroup($group); } - } elseif ($annotation instanceof MaxDepth) { - $attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth()); - } elseif ($annotation instanceof SerializedName) { - $attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName()); - } elseif ($annotation instanceof SerializedPath) { - $attributesMetadata[$property->name]->setSerializedPath($annotation->getSerializedPath()); - } elseif ($annotation instanceof Ignore) { - $attributesMetadata[$property->name]->setIgnore(true); - } elseif ($annotation instanceof Context) { - $this->setAttributeContextsForGroups($annotation, $attributesMetadata[$property->name]); + + continue; } - $loaded = true; + match (true) { + $annotation instanceof MaxDepth => $attributeMetadata->setMaxDepth($annotation->getMaxDepth()), + $annotation instanceof SerializedName => $attributeMetadata->setSerializedName($annotation->getSerializedName()), + $annotation instanceof SerializedPath => $attributeMetadata->setSerializedPath($annotation->getSerializedPath()), + $annotation instanceof Ignore => $attributeMetadata->setIgnore(true), + $annotation instanceof Context => $this->setAttributeContextsForGroups($annotation, $attributeMetadata), + default => null, + }; } } } @@ -206,17 +197,17 @@ private function loadAttributes(\ReflectionMethod|\ReflectionClass|\ReflectionPr private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void { - if ($annotation->getContext()) { - $attributeMetadata->setNormalizationContextForGroups($annotation->getContext(), $annotation->getGroups()); - $attributeMetadata->setDenormalizationContextForGroups($annotation->getContext(), $annotation->getGroups()); - } + $context = $annotation->getContext(); + $groups = $annotation->getGroups(); + $normalizationContext = $annotation->getNormalizationContext(); + $denormalizationContext = $annotation->getDenormalizationContext(); - if ($annotation->getNormalizationContext()) { - $attributeMetadata->setNormalizationContextForGroups($annotation->getNormalizationContext(), $annotation->getGroups()); + if ($normalizationContext || $context) { + $attributeMetadata->setNormalizationContextForGroups($normalizationContext ?: $context, $groups); } - if ($annotation->getDenormalizationContext()) { - $attributeMetadata->setDenormalizationContextForGroups($annotation->getDenormalizationContext(), $annotation->getGroups()); + if ($denormalizationContext || $context) { + $attributeMetadata->setDenormalizationContextForGroups($denormalizationContext ?: $context, $groups); } } From 9d862d66198f3c2e30404228629ef4c18d5d608e Mon Sep 17 00:00:00 2001 From: eRIZ Date: Tue, 21 May 2024 20:55:10 +0200 Subject: [PATCH 083/104] [Serializer] fixed object normalizer for a class with `cancel` method --- Mapping/Loader/AttributeLoader.php | 2 +- Normalizer/GetSetMethodNormalizer.php | 8 +- Normalizer/ObjectNormalizer.php | 5 +- .../Attributes/AccessorishGetters.php | 39 ++++++++++ .../Loader/AttributeLoaderTestCase.php | 17 ++++ .../Normalizer/GetSetMethodNormalizerTest.php | 51 ++++++++++++ Tests/Normalizer/ObjectNormalizerTest.php | 78 +++++++++++++++++++ 7 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 Tests/Fixtures/Attributes/AccessorishGetters.php diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index 8acd5a8c5..6bd967b5f 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -129,7 +129,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 50dd48628..951005545 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -105,8 +105,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])) ); } @@ -118,7 +118,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 c8473a62c..e93d7b4cc 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -100,7 +100,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'), @@ -112,7 +113,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 000000000..f434e84f3 --- /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/AttributeLoaderTestCase.php b/Tests/Mapping/Loader/AttributeLoaderTestCase.php index 99615d382..73cb674c2 100644 --- a/Tests/Mapping/Loader/AttributeLoaderTestCase.php +++ b/Tests/Mapping/Loader/AttributeLoaderTestCase.php @@ -21,6 +21,7 @@ use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AccessorishGetters; use Symfony\Component\Serializer\Tests\Mapping\Loader\Features\ContextMappingTestTrait; use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory; @@ -212,6 +213,22 @@ public function testLoadGroupsOnClass() self::assertSame(['a'], $attributesMetadata['baz']->getGroups()); } + 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); + } + /** * @group legacy */ diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index ca5d25910..4398fbdab 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -515,6 +515,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()); @@ -902,3 +910,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 5b028e8c0..822c0016e 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -937,6 +937,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 @@ -1219,3 +1237,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; + } +} From 96421e664c2c3077a1e0858a02e62a7a4d66ce79 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Wed, 16 Oct 2024 22:04:16 +0200 Subject: [PATCH 084/104] [Serializer] Rename annotations to attributes in AttributeLoader --- Mapping/Loader/AttributeLoader.php | 64 +++++++++---------- .../{Annotation => Attribute}/ContextTest.php | 2 +- .../DiscriminatorMapTest.php | 8 +-- .../{Annotation => Attribute}/GroupsTest.php | 2 +- .../MaxDepthTest.php | 2 +- .../SerializedNameTest.php | 2 +- .../SerializedPathTest.php | 2 +- Tests/Fixtures/Attributes/GroupDummy.php | 4 +- ...ditionalGetterWithoutIgnoreAttributes.php} | 2 +- ...my.php => ChildOfGroupsAttributeDummy.php} | 2 +- Tests/Mapping/Loader/AttributeLoaderTest.php | 8 +-- 11 files changed, 49 insertions(+), 49 deletions(-) rename Tests/{Annotation => Attribute}/ContextTest.php (98%) rename Tests/{Annotation => Attribute}/DiscriminatorMapTest.php (82%) rename Tests/{Annotation => Attribute}/GroupsTest.php (95%) rename Tests/{Annotation => Attribute}/MaxDepthTest.php (94%) rename Tests/{Annotation => Attribute}/SerializedNameTest.php (94%) rename Tests/{Annotation => Attribute}/SerializedPathTest.php (95%) rename Tests/Fixtures/Attributes/{IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php => IgnoreDummyAdditionalGetterWithoutIgnoreAttributes.php} (85%) rename Tests/Fixtures/{ChildOfGroupsAnnotationDummy.php => ChildOfGroupsAttributeDummy.php} (82%) diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index 6bcbed36c..ca92aa88b 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -53,15 +53,15 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $className = $reflectionClass->name; $loaded = false; $classGroups = []; - $classContextAnnotation = null; + $classContextAttribute = null; $attributesMetadata = $classMetadata->getAttributesMetadata(); - foreach ($this->loadAttributes($reflectionClass) as $annotation) { + foreach ($this->loadAttributes($reflectionClass) as $attribute) { match (true) { - $annotation instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($annotation->getTypeProperty(), $annotation->getMapping())), - $annotation instanceof Groups => $classGroups = $annotation->getGroups(), - $annotation instanceof Context => $classContextAnnotation = $annotation, + $attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping())), + $attribute instanceof Groups => $classGroups = $attribute->getGroups(), + $attribute instanceof Context => $classContextAttribute = $attribute, default => null, }; } @@ -74,19 +74,19 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $attributeMetadata = $attributesMetadata[$property->name]; if ($property->getDeclaringClass()->name === $className) { - if ($classContextAnnotation) { - $this->setAttributeContextsForGroups($classContextAnnotation, $attributeMetadata); + if ($classContextAttribute) { + $this->setAttributeContextsForGroups($classContextAttribute, $attributeMetadata); } foreach ($classGroups as $group) { $attributeMetadata->addGroup($group); } - foreach ($this->loadAttributes($property) as $annotation) { + foreach ($this->loadAttributes($property) as $attribute) { $loaded = true; - if ($annotation instanceof Groups) { - foreach ($annotation->getGroups() as $group) { + if ($attribute instanceof Groups) { + foreach ($attribute->getGroups() as $group) { $attributeMetadata->addGroup($group); } @@ -94,11 +94,11 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool } match (true) { - $annotation instanceof MaxDepth => $attributeMetadata->setMaxDepth($annotation->getMaxDepth()), - $annotation instanceof SerializedName => $attributeMetadata->setSerializedName($annotation->getSerializedName()), - $annotation instanceof SerializedPath => $attributeMetadata->setSerializedPath($annotation->getSerializedPath()), - $annotation instanceof Ignore => $attributeMetadata->setIgnore(true), - $annotation instanceof Context => $this->setAttributeContextsForGroups($annotation, $attributeMetadata), + $attribute instanceof MaxDepth => $attributeMetadata->setMaxDepth($attribute->getMaxDepth()), + $attribute instanceof SerializedName => $attributeMetadata->setSerializedName($attribute->getSerializedName()), + $attribute instanceof SerializedPath => $attributeMetadata->setSerializedPath($attribute->getSerializedPath()), + $attribute instanceof Ignore => $attributeMetadata->setIgnore(true), + $attribute instanceof Context => $this->setAttributeContextsForGroups($attribute, $attributeMetadata), default => null, }; } @@ -126,43 +126,43 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool } } - foreach ($this->loadAttributes($method) as $annotation) { - if ($annotation instanceof Groups) { + foreach ($this->loadAttributes($method) as $attribute) { + if ($attribute instanceof Groups) { if (!$accessorOrMutator) { throw new MappingException(\sprintf('Groups on "%s::%s()" cannot be added. Groups can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } - foreach ($annotation->getGroups() as $group) { + foreach ($attribute->getGroups() as $group) { $attributeMetadata->addGroup($group); } - } elseif ($annotation instanceof MaxDepth) { + } elseif ($attribute instanceof MaxDepth) { if (!$accessorOrMutator) { throw new MappingException(\sprintf('MaxDepth on "%s::%s()" cannot be added. MaxDepth can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } - $attributeMetadata->setMaxDepth($annotation->getMaxDepth()); - } elseif ($annotation instanceof SerializedName) { + $attributeMetadata->setMaxDepth($attribute->getMaxDepth()); + } elseif ($attribute instanceof SerializedName) { if (!$accessorOrMutator) { throw new MappingException(\sprintf('SerializedName on "%s::%s()" cannot be added. SerializedName can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } - $attributeMetadata->setSerializedName($annotation->getSerializedName()); - } elseif ($annotation instanceof SerializedPath) { + $attributeMetadata->setSerializedName($attribute->getSerializedName()); + } elseif ($attribute instanceof SerializedPath) { if (!$accessorOrMutator) { throw new MappingException(\sprintf('SerializedPath on "%s::%s()" cannot be added. SerializedPath can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } - $attributeMetadata->setSerializedPath($annotation->getSerializedPath()); - } elseif ($annotation instanceof Ignore) { + $attributeMetadata->setSerializedPath($attribute->getSerializedPath()); + } elseif ($attribute instanceof Ignore) { if ($accessorOrMutator) { $attributeMetadata->setIgnore(true); } - } elseif ($annotation instanceof Context) { + } elseif ($attribute instanceof Context) { if (!$accessorOrMutator) { throw new MappingException(\sprintf('Context on "%s::%s()" cannot be added. Context can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } - $this->setAttributeContextsForGroups($annotation, $attributeMetadata); + $this->setAttributeContextsForGroups($attribute, $attributeMetadata); } $loaded = true; @@ -195,12 +195,12 @@ private function loadAttributes(\ReflectionMethod|\ReflectionClass|\ReflectionPr } } - private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void + private function setAttributeContextsForGroups(Context $attribute, AttributeMetadataInterface $attributeMetadata): void { - $context = $annotation->getContext(); - $groups = $annotation->getGroups(); - $normalizationContext = $annotation->getNormalizationContext(); - $denormalizationContext = $annotation->getDenormalizationContext(); + $context = $attribute->getContext(); + $groups = $attribute->getGroups(); + $normalizationContext = $attribute->getNormalizationContext(); + $denormalizationContext = $attribute->getDenormalizationContext(); if ($normalizationContext || $context) { $attributeMetadata->setNormalizationContextForGroups($normalizationContext ?: $context, $groups); diff --git a/Tests/Annotation/ContextTest.php b/Tests/Attribute/ContextTest.php similarity index 98% rename from Tests/Annotation/ContextTest.php rename to Tests/Attribute/ContextTest.php index 84ff41b8a..cfe175050 100644 --- a/Tests/Annotation/ContextTest.php +++ b/Tests/Attribute/ContextTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Serializer\Tests\Annotation; +namespace Symfony\Component\Serializer\Tests\Attribute; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Attribute\Context; diff --git a/Tests/Annotation/DiscriminatorMapTest.php b/Tests/Attribute/DiscriminatorMapTest.php similarity index 82% rename from Tests/Annotation/DiscriminatorMapTest.php rename to Tests/Attribute/DiscriminatorMapTest.php index bbd112e26..497bc6201 100644 --- a/Tests/Annotation/DiscriminatorMapTest.php +++ b/Tests/Attribute/DiscriminatorMapTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Serializer\Tests\Annotation; +namespace Symfony\Component\Serializer\Tests\Attribute; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Attribute\DiscriminatorMap; @@ -22,16 +22,16 @@ class DiscriminatorMapTest extends TestCase { public function testGetTypePropertyAndMapping() { - $annotation = new DiscriminatorMap(typeProperty: 'type', mapping: [ + $attribute = new DiscriminatorMap(typeProperty: 'type', mapping: [ 'foo' => 'FooClass', 'bar' => 'BarClass', ]); - $this->assertEquals('type', $annotation->getTypeProperty()); + $this->assertEquals('type', $attribute->getTypeProperty()); $this->assertEquals([ 'foo' => 'FooClass', 'bar' => 'BarClass', - ], $annotation->getMapping()); + ], $attribute->getMapping()); } public function testExceptionWithEmptyTypeProperty() diff --git a/Tests/Annotation/GroupsTest.php b/Tests/Attribute/GroupsTest.php similarity index 95% rename from Tests/Annotation/GroupsTest.php rename to Tests/Attribute/GroupsTest.php index 38ec518bd..266cbc4f4 100644 --- a/Tests/Annotation/GroupsTest.php +++ b/Tests/Attribute/GroupsTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Serializer\Tests\Annotation; +namespace Symfony\Component\Serializer\Tests\Attribute; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Attribute\Groups; diff --git a/Tests/Annotation/MaxDepthTest.php b/Tests/Attribute/MaxDepthTest.php similarity index 94% rename from Tests/Annotation/MaxDepthTest.php rename to Tests/Attribute/MaxDepthTest.php index f2ff35c87..e611bfbc4 100644 --- a/Tests/Annotation/MaxDepthTest.php +++ b/Tests/Attribute/MaxDepthTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Serializer\Tests\Annotation; +namespace Symfony\Component\Serializer\Tests\Attribute; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Attribute\MaxDepth; diff --git a/Tests/Annotation/SerializedNameTest.php b/Tests/Attribute/SerializedNameTest.php similarity index 94% rename from Tests/Annotation/SerializedNameTest.php rename to Tests/Attribute/SerializedNameTest.php index 3a829aecf..c645e7e85 100644 --- a/Tests/Annotation/SerializedNameTest.php +++ b/Tests/Attribute/SerializedNameTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Serializer\Tests\Annotation; +namespace Symfony\Component\Serializer\Tests\Attribute; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Attribute\SerializedName; diff --git a/Tests/Annotation/SerializedPathTest.php b/Tests/Attribute/SerializedPathTest.php similarity index 95% rename from Tests/Annotation/SerializedPathTest.php rename to Tests/Attribute/SerializedPathTest.php index f5bbfa62b..7ba31fc22 100644 --- a/Tests/Annotation/SerializedPathTest.php +++ b/Tests/Attribute/SerializedPathTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Serializer\Tests\Annotation; +namespace Symfony\Component\Serializer\Tests\Attribute; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyAccess\PropertyPath; diff --git a/Tests/Fixtures/Attributes/GroupDummy.php b/Tests/Fixtures/Attributes/GroupDummy.php index 5c34c95a4..dd88507be 100644 --- a/Tests/Fixtures/Attributes/GroupDummy.php +++ b/Tests/Fixtures/Attributes/GroupDummy.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Fixtures\Attributes; use Symfony\Component\Serializer\Attribute\Groups; -use Symfony\Component\Serializer\Tests\Fixtures\ChildOfGroupsAnnotationDummy; +use Symfony\Component\Serializer\Tests\Fixtures\ChildOfGroupsAttributeDummy; /** * @author Kévin Dunglas @@ -23,7 +23,7 @@ class GroupDummy extends GroupDummyParent implements GroupDummyInterface private $foo; #[Groups(['b', 'c', 'name_converter'])] protected $bar; - #[ChildOfGroupsAnnotationDummy] + #[ChildOfGroupsAttributeDummy] protected $quux; private $fooBar; private $symfony; diff --git a/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php b/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAttributes.php similarity index 85% rename from Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php rename to Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAttributes.php index 21abb870b..5d3e8f47f 100644 --- a/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php +++ b/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAttributes.php @@ -2,7 +2,7 @@ namespace Symfony\Component\Serializer\Tests\Fixtures\Attributes; -class IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations +class IgnoreDummyAdditionalGetterWithoutIgnoreAttributes { private $myValue; diff --git a/Tests/Fixtures/ChildOfGroupsAnnotationDummy.php b/Tests/Fixtures/ChildOfGroupsAttributeDummy.php similarity index 82% rename from Tests/Fixtures/ChildOfGroupsAnnotationDummy.php rename to Tests/Fixtures/ChildOfGroupsAttributeDummy.php index 9a163012f..4fc81fa9d 100644 --- a/Tests/Fixtures/ChildOfGroupsAnnotationDummy.php +++ b/Tests/Fixtures/ChildOfGroupsAttributeDummy.php @@ -5,7 +5,7 @@ use Symfony\Component\Serializer\Attribute\Groups; #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] -final class ChildOfGroupsAnnotationDummy extends Groups +final class ChildOfGroupsAttributeDummy extends Groups { public function __construct() { diff --git a/Tests/Mapping/Loader/AttributeLoaderTest.php b/Tests/Mapping/Loader/AttributeLoaderTest.php index 5b6ef3de7..fd7f2b729 100644 --- a/Tests/Mapping/Loader/AttributeLoaderTest.php +++ b/Tests/Mapping/Loader/AttributeLoaderTest.php @@ -33,7 +33,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Attributes\GroupDummyParent; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\IgnoreDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\IgnoreDummyAdditionalGetter; -use Symfony\Component\Serializer\Tests\Fixtures\Attributes\IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\IgnoreDummyAdditionalGetterWithoutIgnoreAttributes; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedPathDummy; @@ -181,7 +181,7 @@ public function testCanHandleUnrelatedIgnoredMethods() $this->assertSame(['id'], array_keys($metadata->getAttributesMetadata())); } - public function testIgnoreGetterWithRequiredParameterIfIgnoreAnnotationIsUsed() + public function testIgnoreGetterWithRequiredParameterIfIgnoreAttributeIsUsed() { $classMetadata = new ClassMetadata(IgnoreDummyAdditionalGetter::class); $this->getLoaderForContextMapping()->loadClassMetadata($classMetadata); @@ -191,9 +191,9 @@ public function testIgnoreGetterWithRequiredParameterIfIgnoreAnnotationIsUsed() self::assertArrayHasKey('extraValue2', $attributes); } - public function testIgnoreGetterWithRequiredParameterIfIgnoreAnnotationIsNotUsed() + public function testIgnoreGetterWithRequiredParameterIfIgnoreAttributeIsNotUsed() { - $classMetadata = new ClassMetadata(IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations::class); + $classMetadata = new ClassMetadata(IgnoreDummyAdditionalGetterWithoutIgnoreAttributes::class); $this->getLoaderForContextMapping()->loadClassMetadata($classMetadata); $attributes = $classMetadata->getAttributesMetadata(); From 6e7d9f9378bee4352de15bc7883a28294f4b82d6 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Tue, 29 Oct 2024 10:06:08 +0100 Subject: [PATCH 085/104] [Serializer] Revert default groups --- CHANGELOG.md | 1 - NameConverter/MetadataAwareNameConverter.php | 7 +-- Normalizer/AbstractNormalizer.php | 11 +--- Tests/Fixtures/Attributes/GroupDummy.php | 24 -------- Tests/Mapping/TestClassMetadataFactory.php | 8 --- Tests/Normalizer/Features/GroupsTestTrait.php | 56 +------------------ .../Normalizer/GetSetMethodNormalizerTest.php | 2 - Tests/Normalizer/ObjectNormalizerTest.php | 2 - Tests/Normalizer/PropertyNormalizerTest.php | 4 -- 9 files changed, 8 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3118834d8..463572046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ CHANGELOG * Add arguments `$class`, `$format` and `$context` to `NameConverterInterface::normalize()` and `NameConverterInterface::denormalize()` * Add `DateTimeNormalizer::CAST_KEY` context option - * Add `Default` and "class name" default groups * Add `AbstractNormalizer::FILTER_BOOL` context option * Add `CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES` context option * Deprecate `AbstractNormalizerContextBuilder::withDefaultContructorArguments(?array $defaultContructorArguments)`, use `withDefaultConstructorArguments(?array $defaultConstructorArguments)` instead (note the missing `s` character in Contructor word in deprecated method) diff --git a/NameConverter/MetadataAwareNameConverter.php b/NameConverter/MetadataAwareNameConverter.php index 327d92dc1..445ad7422 100644 --- a/NameConverter/MetadataAwareNameConverter.php +++ b/NameConverter/MetadataAwareNameConverter.php @@ -128,16 +128,13 @@ private function getCacheValueForAttributesMetadata(string $class, array $contex } $metadataGroups = $metadata->getGroups(); - $contextGroups = (array) ($context[AbstractNormalizer::GROUPS] ?? []); - $contextGroupsHasBeenDefined = [] !== $contextGroups; - $contextGroups = array_merge($contextGroups, ['Default', (false !== $nsSep = strrpos($class, '\\')) ? substr($class, $nsSep + 1) : $class]); - if ($contextGroupsHasBeenDefined && !$metadataGroups) { + if ($contextGroups && !$metadataGroups) { continue; } - if ($metadataGroups && !array_intersect(array_merge($metadataGroups, ['*']), $contextGroups)) { + if ($metadataGroups && !array_intersect($metadataGroups, $contextGroups) && !\in_array('*', $contextGroups, true)) { continue; } diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index 6f065984c..d8f796b4c 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -223,17 +223,12 @@ protected function getAllowedAttributes(string|object $classOrObject, array $con return false; } - $classMetadata = $this->classMetadataFactory->getMetadataFor($classOrObject); - $class = $classMetadata->getName(); - $groups = $this->getGroups($context); - $groupsHasBeenDefined = [] !== $groups; - $groups = array_merge($groups, ['Default', (false !== $nsSep = strrpos($class, '\\')) ? substr($class, $nsSep + 1) : $class]); $allowedAttributes = []; $ignoreUsed = false; - foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) { + foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) { if ($ignore = $attributeMetadata->isIgnored()) { $ignoreUsed = true; } @@ -241,14 +236,14 @@ protected function getAllowedAttributes(string|object $classOrObject, array $con // If you update this check, update accordingly the one in Symfony\Component\PropertyInfo\Extractor\SerializerExtractor::getProperties() if ( !$ignore - && (!$groupsHasBeenDefined || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups)) + && ([] === $groups || \in_array('*', $groups, true) || array_intersect($attributeMetadata->getGroups(), $groups)) && $this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context) ) { $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata; } } - if (!$ignoreUsed && !$groupsHasBeenDefined && $allowExtraAttributes) { + if (!$ignoreUsed && [] === $groups && $allowExtraAttributes) { // Backward Compatibility with the code using this method written before the introduction of @Ignore return false; } diff --git a/Tests/Fixtures/Attributes/GroupDummy.php b/Tests/Fixtures/Attributes/GroupDummy.php index 5c34c95a4..749e841a5 100644 --- a/Tests/Fixtures/Attributes/GroupDummy.php +++ b/Tests/Fixtures/Attributes/GroupDummy.php @@ -27,10 +27,6 @@ class GroupDummy extends GroupDummyParent implements GroupDummyInterface protected $quux; private $fooBar; private $symfony; - #[Groups(['Default'])] - private $default; - #[Groups(['GroupDummy'])] - private $className; #[Groups(['b'])] public function setBar($bar) @@ -84,24 +80,4 @@ public function setQuux($quux): void { $this->quux = $quux; } - - public function setDefault($default) - { - $this->default = $default; - } - - public function getDefault() - { - return $this->default; - } - - public function setClassName($className) - { - $this->className = $className; - } - - public function getClassName() - { - return $this->className; - } } diff --git a/Tests/Mapping/TestClassMetadataFactory.php b/Tests/Mapping/TestClassMetadataFactory.php index d617ffaeb..61147316a 100644 --- a/Tests/Mapping/TestClassMetadataFactory.php +++ b/Tests/Mapping/TestClassMetadataFactory.php @@ -63,14 +63,6 @@ public static function createClassMetadata(string $namespace, bool $withParent = $symfony->addGroup('name_converter'); } - $default = new AttributeMetadata('default'); - $default->addGroup('Default'); - $expected->addAttributeMetadata($default); - - $className = new AttributeMetadata('className'); - $className->addGroup('GroupDummy'); - $expected->addAttributeMetadata($className); - // load reflection class so that the comparison passes $expected->getReflectionClass(); diff --git a/Tests/Normalizer/Features/GroupsTestTrait.php b/Tests/Normalizer/Features/GroupsTestTrait.php index ba4d76323..621ceec41 100644 --- a/Tests/Normalizer/Features/GroupsTestTrait.php +++ b/Tests/Normalizer/Features/GroupsTestTrait.php @@ -36,13 +36,9 @@ public function testGroupsNormalize() $obj->setSymfony('symfony'); $obj->setKevin('kevin'); $obj->setCoopTilleuls('coopTilleuls'); - $obj->setDefault('default'); - $obj->setClassName('className'); $this->assertEquals([ 'bar' => 'bar', - 'default' => 'default', - 'className' => 'className', ], $normalizer->normalize($obj, null, ['groups' => ['c']])); $this->assertEquals([ @@ -52,26 +48,7 @@ public function testGroupsNormalize() 'bar' => 'bar', 'kevin' => 'kevin', 'coopTilleuls' => 'coopTilleuls', - 'default' => 'default', - 'className' => 'className', ], $normalizer->normalize($obj, null, ['groups' => ['a', 'c']])); - - $this->assertEquals([ - 'default' => 'default', - 'className' => 'className', - ], $normalizer->normalize($obj, null, ['groups' => ['unknown']])); - - $this->assertEquals([ - 'quux' => 'quux', - 'symfony' => 'symfony', - 'foo' => 'foo', - 'fooBar' => 'fooBar', - 'bar' => 'bar', - 'kevin' => 'kevin', - 'coopTilleuls' => 'coopTilleuls', - 'default' => 'default', - 'className' => 'className', - ], $normalizer->normalize($obj)); } public function testGroupsDenormalize() @@ -79,27 +56,10 @@ public function testGroupsDenormalize() $normalizer = $this->getDenormalizerForGroups(); $obj = new GroupDummy(); - $obj->setDefault('default'); - $obj->setClassName('className'); - - $data = [ - 'foo' => 'foo', - 'bar' => 'bar', - 'quux' => 'quux', - 'default' => 'default', - 'className' => 'className', - ]; - - $denormalized = $normalizer->denormalize( - $data, - GroupDummy::class, - null, - ['groups' => ['unknown']] - ); - $this->assertEquals($obj, $denormalized); - $obj->setFoo('foo'); + $data = ['foo' => 'foo', 'bar' => 'bar']; + $denormalized = $normalizer->denormalize( $data, GroupDummy::class, @@ -117,11 +77,6 @@ public function testGroupsDenormalize() ['groups' => ['a', 'b']] ); $this->assertEquals($obj, $denormalized); - - $obj->setQuux('quux'); - - $denormalized = $normalizer->denormalize($data, GroupDummy::class); - $this->assertEquals($obj, $denormalized); } public function testNormalizeNoPropertyInGroup() @@ -130,12 +85,7 @@ public function testNormalizeNoPropertyInGroup() $obj = new GroupDummy(); $obj->setFoo('foo'); - $obj->setDefault('default'); - $obj->setClassName('className'); - $this->assertEquals([ - 'default' => 'default', - 'className' => 'className', - ], $normalizer->normalize($obj, null, ['groups' => ['notExist']])); + $this->assertEquals([], $normalizer->normalize($obj, null, ['groups' => ['notExist']])); } } diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index f846a0d97..2c30a57bc 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -302,8 +302,6 @@ public function testGroupsNormalizeWithNameConverter() 'bar' => null, 'foo_bar' => '@dunglas', 'symfony' => '@coopTilleuls', - 'default' => null, - 'class_name' => null, ], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['name_converter']]) ); diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 50866a1b2..ed6a9d3be 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -507,8 +507,6 @@ public function testGroupsNormalizeWithNameConverter() 'bar' => null, 'foo_bar' => '@dunglas', 'symfony' => '@coopTilleuls', - 'default' => null, - 'class_name' => null, ], $this->normalizer->normalize($obj, null, [ObjectNormalizer::GROUPS => ['name_converter']]) ); diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index b93a7bb9f..9ac85920e 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -195,8 +195,6 @@ public function testNormalizeWithParentClass() 'fooBar' => null, 'symfony' => null, 'baz' => 'baz', - 'default' => null, - 'className' => null, ], $this->normalizer->normalize($group, 'any') ); @@ -321,8 +319,6 @@ public function testGroupsNormalizeWithNameConverter() 'bar' => null, 'foo_bar' => '@dunglas', 'symfony' => '@coopTilleuls', - 'default' => null, - 'class_name' => null, ], $this->normalizer->normalize($obj, null, [PropertyNormalizer::GROUPS => ['name_converter']]) ); From 2154d9a0520fa3d223ac36aad773376fb1d0a36a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 7 Nov 2024 16:24:12 +0100 Subject: [PATCH 086/104] fix support for phpstan/phpdoc-parser 2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 948fa36fa..d7bef296b 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ }, "require-dev": { "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", - "phpstan/phpdoc-parser": "^1.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", "seld/jsonlint": "^1.10", "symfony/cache": "^6.4|^7.0", "symfony/config": "^6.4|^7.0", From c210d2bf4d81e47e1444eaf246c5d0c60ad8672d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 14 Nov 2024 10:51:05 +0100 Subject: [PATCH 087/104] prevent failures around not existing TypeInfo classes Having a getType() method on an extractor is not enough. Such a method may exist to be forward-compatible with the TypeInfo component. We still must not call it if the TypeInfo component is not installed to prevent running into errors for not-defined classes when the TypeInfo component is not installed. --- Normalizer/AbstractObjectNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 63068420b..3e3ef426a 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -938,7 +938,7 @@ private function getType(string $currentClass, string $attribute): Type|array|nu */ private function getPropertyType(string $className, string $property): Type|array|null { - if (method_exists($this->propertyTypeExtractor, 'getType')) { + if (class_exists(Type::class) && method_exists($this->propertyTypeExtractor, 'getType')) { return $this->propertyTypeExtractor->getType($className, $property); } From 7afcfbb87f161666091f1c4d0382ffdd5a07ac26 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Thu, 14 Nov 2024 13:01:20 +0100 Subject: [PATCH 088/104] [Serializer][PropertyInfo][Validator] TypeInfo 7.2 compatibility --- Normalizer/AbstractObjectNormalizer.php | 72 +++++++++++++++++++++---- Normalizer/ArrayDenormalizer.php | 12 ++++- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 63068420b..f8a3a41b5 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -34,10 +34,13 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\TypeInfo\Exception\LogicException as TypeInfoLogicException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\NullableType; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; use Symfony\Component\TypeInfo\TypeIdentifier; /** @@ -644,7 +647,14 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; - $isUnionType = $type->asNonNullable() instanceof UnionType; + + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'asNonNullable')) { + $isUnionType = $type->asNonNullable() instanceof UnionType; + } else { + $isUnionType = $type instanceof UnionType; + } + $e = null; $extraAttributesException = null; $missingConstructorArgumentsException = null; @@ -667,12 +677,23 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $collectionValueType = $t->getCollectionValueType(); } - $t = $t->getBaseType(); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'getBaseType')) { + $t = $t->getBaseType(); + } else { + while ($t instanceof WrappingTypeInterface) { + $t = $t->getWrappedType(); + } + } // Fix a collection that contains the only one element // This is special to xml format only - if ('xml' === $format && $collectionValueType && !$collectionValueType->isA(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) { - $data = [$data]; + if ('xml' === $format && $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) { + // BC layer for type-info < 7.2 + $isMixedType = method_exists(Type::class, 'isA') ? $collectionValueType->isA(TypeIdentifier::MIXED) : $collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED); + if (!$isMixedType) { + $data = [$data]; + } } // This try-catch should cover all NotNormalizableValueException (and all return branches after the first @@ -695,7 +716,10 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return ''; } - $isNullable = $isNullable ?: $type->isNullable(); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'isNullable')) { + $isNullable = $isNullable ?: $type->isNullable(); + } } switch ($typeIdentifier) { @@ -732,7 +756,16 @@ private function validateAndDenormalize(Type $type, string $currentClass, string if ($collectionValueType) { try { - $collectionValueBaseType = $collectionValueType->getBaseType(); + $collectionValueBaseType = $collectionValueType; + + // BC layer for type-info < 7.2 + if (!interface_exists(WrappingTypeInterface::class)) { + $collectionValueBaseType = $collectionValueType->getBaseType(); + } else { + while ($collectionValueBaseType instanceof WrappingTypeInterface) { + $collectionValueBaseType = $collectionValueBaseType->getWrappedType(); + } + } } catch (TypeInfoLogicException) { $collectionValueBaseType = Type::mixed(); } @@ -742,15 +775,29 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $class = $collectionValueBaseType->getClassName().'[]'; $context['key_type'] = $collectionKeyType; $context['value_type'] = $collectionValueType; - } elseif (TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) { + } elseif ( + // BC layer for type-info < 7.2 + !class_exists(NullableType::class) && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier() + || $collectionValueBaseType instanceof BuiltinType && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier() + ) { // get inner type for any nested array $innerType = $collectionValueType; + if ($innerType instanceof NullableType) { + $innerType = $innerType->getWrappedType(); + } // note that it will break for any other builtinType $dimensions = '[]'; while ($innerType instanceof CollectionType) { $dimensions .= '[]'; $innerType = $innerType->getCollectionValueType(); + if ($innerType instanceof NullableType) { + $innerType = $innerType->getWrappedType(); + } + } + + while ($innerType instanceof WrappingTypeInterface) { + $innerType = $innerType->getWrappedType(); } if ($innerType instanceof ObjectType) { @@ -862,8 +909,15 @@ private function validateAndDenormalize(Type $type, string $currentClass, string throw $missingConstructorArgumentsException; } - if (!$isUnionType && $e) { - throw $e; + // BC layer for type-info < 7.2 + if (!class_exists(NullableType::class)) { + if (!$isUnionType && $e) { + throw $e; + } + } else { + if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) { + throw $e; + } } if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { diff --git a/Normalizer/ArrayDenormalizer.php b/Normalizer/ArrayDenormalizer.php index 347030c24..964d74b61 100644 --- a/Normalizer/ArrayDenormalizer.php +++ b/Normalizer/ArrayDenormalizer.php @@ -16,7 +16,9 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Denormalizes arrays of objects. @@ -59,7 +61,15 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $typeIdentifiers = []; if (null !== $keyType = ($context['key_type'] ?? null)) { if ($keyType instanceof Type) { - $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'getBaseType')) { + $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + } else { + /** @var list|BuiltinType> */ + $keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; + + $typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes); + } } else { $typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]); } From 5fff3abe545e26b2e024d18ad6e3f797e649f513 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Thu, 14 Nov 2024 13:01:20 +0100 Subject: [PATCH 089/104] [TypeInfo][Serializer][PropertyInfo][Validator] TypeInfo 7.1 compatibility --- Normalizer/AbstractObjectNormalizer.php | 59 ++++++++++++++++++++----- Normalizer/ArrayDenormalizer.php | 13 ++++-- composer.json | 3 +- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 3dbb750f1..fb45a924b 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -32,6 +32,7 @@ use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Exception\LogicException as TypeInfoLogicException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; @@ -646,6 +647,14 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; + + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'asNonNullable')) { + $isUnionType = $type->asNonNullable() instanceof UnionType; + } else { + $isUnionType = $type instanceof UnionType; + } + $e = null; $extraAttributesException = null; $missingConstructorArgumentsException = null; @@ -667,14 +676,23 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $collectionValueType = $t->getCollectionValueType(); } - while ($t instanceof WrappingTypeInterface) { - $t = $t->getWrappedType(); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'getBaseType')) { + $t = $t->getBaseType(); + } else { + while ($t instanceof WrappingTypeInterface) { + $t = $t->getWrappedType(); + } } // Fix a collection that contains the only one element // This is special to xml format only - if ('xml' === $format && $collectionValueType && !$collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) { - $data = [$data]; + if ('xml' === $format && $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) { + // BC layer for type-info < 7.2 + $isMixedType = method_exists(Type::class, 'isA') ? $collectionValueType->isA(TypeIdentifier::MIXED) : $collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED); + if (!$isMixedType) { + $data = [$data]; + } } // This try-catch should cover all NotNormalizableValueException (and all return branches after the first @@ -731,9 +749,19 @@ private function validateAndDenormalize(Type $type, string $currentClass, string } if ($collectionValueType) { - $collectionValueBaseType = $collectionValueType; - while ($collectionValueBaseType instanceof WrappingTypeInterface) { - $collectionValueBaseType = $collectionValueBaseType->getWrappedType(); + try { + $collectionValueBaseType = $collectionValueType; + + // BC layer for type-info < 7.2 + if (!interface_exists(WrappingTypeInterface::class)) { + $collectionValueBaseType = $collectionValueType->getBaseType(); + } else { + while ($collectionValueBaseType instanceof WrappingTypeInterface) { + $collectionValueBaseType = $collectionValueBaseType->getWrappedType(); + } + } + } catch (TypeInfoLogicException) { + $collectionValueBaseType = Type::mixed(); } if ($collectionValueBaseType instanceof ObjectType) { @@ -741,7 +769,11 @@ private function validateAndDenormalize(Type $type, string $currentClass, string $class = $collectionValueBaseType->getClassName().'[]'; $context['key_type'] = $collectionKeyType; $context['value_type'] = $collectionValueType; - } elseif ($collectionValueBaseType instanceof BuiltinType && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) { + } elseif ( + // BC layer for type-info < 7.2 + !class_exists(NullableType::class) && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier() + || $collectionValueBaseType instanceof BuiltinType && TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier() + ) { // get inner type for any nested array $innerType = $collectionValueType; if ($innerType instanceof NullableType) { @@ -871,8 +903,15 @@ private function validateAndDenormalize(Type $type, string $currentClass, string throw $missingConstructorArgumentsException; } - if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) { - throw $e; + // BC layer for type-info < 7.2 + if (!class_exists(NullableType::class)) { + if (!$isUnionType && $e) { + throw $e; + } + } else { + if ($e && !($type instanceof UnionType && !$type instanceof NullableType)) { + throw $e; + } } if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { diff --git a/Normalizer/ArrayDenormalizer.php b/Normalizer/ArrayDenormalizer.php index 08fae04df..96c4d259c 100644 --- a/Normalizer/ArrayDenormalizer.php +++ b/Normalizer/ArrayDenormalizer.php @@ -56,10 +56,15 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $typeIdentifiers = []; if (null !== $keyType = ($context['key_type'] ?? null)) { if ($keyType instanceof Type) { - /** @var list|BuiltinType> */ - $keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; - - $typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes); + // BC layer for type-info < 7.2 + if (method_exists(Type::class, 'getBaseType')) { + $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + } else { + /** @var list|BuiltinType> */ + $keyTypes = $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]; + + $typeIdentifiers = array_map(fn (BuiltinType $t): string => $t->getTypeIdentifier()->value, $keyTypes); + } } else { $typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]); } diff --git a/composer.json b/composer.json index 4e6865523..d8809fa07 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/type-info": "^7.2", + "symfony/type-info": "^7.1", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", @@ -51,7 +51,6 @@ "symfony/dependency-injection": "<6.4", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", - "symfony/type-info": "<7.2", "symfony/uid": "<6.4", "symfony/validator": "<6.4", "symfony/yaml": "<6.4" From 81f032d2ee6a3cd8b75990941a1e3f87c8ad086e Mon Sep 17 00:00:00 2001 From: wanxiangchwng Date: Sat, 23 Nov 2024 10:47:03 +0800 Subject: [PATCH 090/104] chore: fix some typos Signed-off-by: wanxiangchwng --- Tests/Encoder/XmlEncoderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index 31d2ddfc6..0eb332e80 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -149,7 +149,7 @@ public static function validEncodeProvider(): iterable ], ]; - yield 'encode remvoing empty tags' => [ + yield 'encode removing empty tags' => [ ''."\n". 'Peter'."\n", ['person' => ['firstname' => 'Peter', 'lastname' => null]], From c9a49af37c46114a884a9c0945f7f75a3e8b1ec0 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Mon, 25 Nov 2024 01:26:19 +0100 Subject: [PATCH 091/104] CS: re-apply trailing_comma_in_multiline --- Tests/Normalizer/ObjectNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 93ed5e468..d45586b44 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -976,7 +976,7 @@ public function testNormalizeWithMethodNamesSimilarToAccessors() 'tell' => true, 'class' => true, 'responsibility' => true, - 123 => 321 + 123 => 321, ], $normalized); } } From 3f5ed9f5e6c02e3853109190ba38408f5e1d2dd0 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 16 Nov 2024 15:49:06 +0100 Subject: [PATCH 092/104] Proofread UPGRADE guide --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b7a1fac3..4c36d5885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ CHANGELOG 7.2 --- - * Deprecate the `csv_escape_char` context option of `CsvEncoder` and the `CsvEncoder::ESCAPE_CHAR_KEY` constant - * Deprecate `CsvEncoderContextBuilder::withEscapeChar()` method + * Deprecate the `csv_escape_char` context option of `CsvEncoder`, the `CsvEncoder::ESCAPE_CHAR_KEY` constant + and the `CsvEncoderContextBuilder::withEscapeChar()` method, following its deprecation in PHP 8.4 * Add `SnakeCaseToCamelCaseNameConverter` * Support subclasses of `\DateTime` and `\DateTimeImmutable` for denormalization * Add the `UidNormalizer::NORMALIZATION_FORMAT_RFC9562` constant From ab913397b9aeee6d89277dded14fa8259dc1e46d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 29 Dec 2024 22:22:56 +0100 Subject: [PATCH 093/104] Fix exception thrown by YamlEncoder --- Encoder/YamlEncoder.php | 8 +++++++- Tests/Encoder/YamlEncoderTest.php | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Encoder/YamlEncoder.php b/Encoder/YamlEncoder.php index 223cd7933..1013129db 100644 --- a/Encoder/YamlEncoder.php +++ b/Encoder/YamlEncoder.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Serializer\Encoder; +use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Yaml\Dumper; +use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Yaml; @@ -85,7 +87,11 @@ public function decode(string $data, string $format, array $context = []): mixed { $context = array_merge($this->defaultContext, $context); - return $this->parser->parse($data, $context[self::YAML_FLAGS]); + try { + return $this->parser->parse($data, $context[self::YAML_FLAGS]); + } catch (ParseException $e) { + throw new NotEncodableValueException($e->getMessage(), $e->getCode(), $e); + } } public function supportsDecoding(string $format): bool diff --git a/Tests/Encoder/YamlEncoderTest.php b/Tests/Encoder/YamlEncoderTest.php index 33ee49f5d..f647fe423 100644 --- a/Tests/Encoder/YamlEncoderTest.php +++ b/Tests/Encoder/YamlEncoderTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Encoder\YamlEncoder; +use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Yaml\Yaml; /** @@ -81,4 +82,12 @@ public function testContext() $this->assertEquals(['foo' => $obj], $encoder->decode("foo: !php/object 'O:8:\"stdClass\":1:{s:3:\"bar\";i:2;}'", 'yaml')); $this->assertEquals(['foo' => null], $encoder->decode("foo: !php/object 'O:8:\"stdClass\":1:{s:3:\"bar\";i:2;}'", 'yaml', [YamlEncoder::YAML_FLAGS => 0])); } + + public function testInvalidYaml() + { + $encoder = new YamlEncoder(); + + $this->expectException(NotEncodableValueException::class); + $encoder->decode("\t", 'yaml'); + } } From ff34e9f1d46a8e918a8ddc2a4ff384eaca45ce7f Mon Sep 17 00:00:00 2001 From: djordy Date: Tue, 14 Jan 2025 14:22:11 +0100 Subject: [PATCH 094/104] [Serializer] [ObjectNormalizer] Filter int when using FILTER_BOOL --- Normalizer/AbstractObjectNormalizer.php | 4 +-- .../AbstractObjectNormalizerTest.php | 31 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index aad68f7ba..1860425f9 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -569,7 +569,7 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass return (float) $data; } - if (LegacyType::BUILTIN_TYPE_BOOL === $builtinType && \is_string($data) && ($context[self::FILTER_BOOL] ?? false)) { + if (LegacyType::BUILTIN_TYPE_BOOL === $builtinType && (\is_string($data) || \is_int($data)) && ($context[self::FILTER_BOOL] ?? false)) { return filter_var($data, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE); } @@ -854,7 +854,7 @@ private function validateAndDenormalize(Type $type, string $currentClass, string return (float) $data; } - if (TypeIdentifier::BOOL === $typeIdentifier && \is_string($data) && ($context[self::FILTER_BOOL] ?? false)) { + if (TypeIdentifier::BOOL === $typeIdentifier && (\is_string($data) || \is_int($data)) && ($context[self::FILTER_BOOL] ?? false)) { return filter_var($data, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE); } diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index b4f5c103c..27f3c2084 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1216,15 +1216,34 @@ public static function provideDenormalizeWithFilterBoolData(): array { return [ [['foo' => 'true'], true], + [['foo' => 'True'], true], + [['foo' => 'TRUE'], true], [['foo' => '1'], true], + [['foo' => 1], true], [['foo' => 'yes'], true], + [['foo' => 'Yes'], true], + [['foo' => 'YES'], true], + [['foo' => 'on'], true], + [['foo' => 'On'], true], + [['foo' => 'ON'], true], [['foo' => 'false'], false], + [['foo' => 'False'], false], + [['foo' => 'FALSE'], false], [['foo' => '0'], false], + [['foo' => 0], false], [['foo' => 'no'], false], + [['foo' => 'No'], false], + [['foo' => 'NO'], false], + [['foo' => 'off'], false], + [['foo' => 'Off'], false], + [['foo' => 'OFF'], false], [['foo' => ''], false], [['foo' => null], null], [['foo' => 'null'], null], [['foo' => 'something'], null], + [['foo' => 'foo'], null], + [['foo' => 1234567890], null], + [['foo' => -1234567890], null], ]; } @@ -1253,10 +1272,7 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string public function testTemplateTypeWhenAnObjectIsPassedToDenormalize() { - $normalizer = new class ( - classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()), - propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [new PhpStanExtractor(), new ReflectionExtractor()]) - ) extends AbstractObjectNormalizerDummy { + $normalizer = new class(classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()), propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [new PhpStanExtractor(), new ReflectionExtractor()])) extends AbstractObjectNormalizerDummy { protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool { return true; @@ -1279,10 +1295,7 @@ public function testDenormalizeTemplateType() $this->markTestSkipped('The PropertyInfo component before Symfony 7.1 does not support template types.'); } - $normalizer = new class ( - classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()), - propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [new PhpStanExtractor(), new ReflectionExtractor()]) - ) extends AbstractObjectNormalizerDummy { + $normalizer = new class(classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()), propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [new PhpStanExtractor(), new ReflectionExtractor()])) extends AbstractObjectNormalizerDummy { protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []): bool { return true; @@ -1587,7 +1600,7 @@ class TruePropertyDummy class BoolPropertyDummy { - /** @var null|bool */ + /** @var bool|null */ public $foo; } From 6ad986f62276da4c8c69754decfaa445a89cb6e3 Mon Sep 17 00:00:00 2001 From: Valmonzo Date: Fri, 15 Nov 2024 16:13:35 +0100 Subject: [PATCH 095/104] [Serializer] fix default context in Serializer --- DependencyInjection/SerializerPass.php | 1 + Serializer.php | 8 +++--- .../SerializerPassTest.php | 7 +++-- Tests/SerializerTest.php | 26 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/DependencyInjection/SerializerPass.php b/DependencyInjection/SerializerPass.php index d0b0deb48..c2959ecda 100644 --- a/DependencyInjection/SerializerPass.php +++ b/DependencyInjection/SerializerPass.php @@ -56,6 +56,7 @@ public function process(ContainerBuilder $container) } $container->getParameterBag()->remove('serializer.default_context'); + $container->getDefinition('serializer')->setArgument('$defaultContext', $defaultContext); } if ($container->getParameter('kernel.debug') && $container->hasDefinition('serializer.data_collector')) { diff --git a/Serializer.php b/Serializer.php index 7044c2f20..e17042097 100644 --- a/Serializer.php +++ b/Serializer.php @@ -84,10 +84,12 @@ class Serializer implements SerializerInterface, ContextAwareNormalizerInterface /** * @param array $normalizers * @param array $encoders + * @param array $defaultContext */ public function __construct( private array $normalizers = [], array $encoders = [], + private array $defaultContext = [], ) { foreach ($normalizers as $normalizer) { if ($normalizer instanceof SerializerAwareInterface) { @@ -163,12 +165,12 @@ public function normalize(mixed $data, ?string $format = null, array $context = return $data; } - if (\is_array($data) && !$data && ($context[self::EMPTY_ARRAY_AS_OBJECT] ?? false)) { + if (\is_array($data) && !$data && ($context[self::EMPTY_ARRAY_AS_OBJECT] ?? $this->defaultContext[self::EMPTY_ARRAY_AS_OBJECT] ?? false)) { return new \ArrayObject(); } if (is_iterable($data)) { - if ($data instanceof \Countable && ($context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? false) && !\count($data)) { + if ($data instanceof \Countable && ($context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? false) && !\count($data)) { return new \ArrayObject(); } @@ -220,7 +222,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type)); } - if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) { + if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) { unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]); $context['not_normalizable_value_exceptions'] = []; $errors = &$context['not_normalizable_value_exceptions']; diff --git a/Tests/DependencyInjection/SerializerPassTest.php b/Tests/DependencyInjection/SerializerPassTest.php index eb77263f4..b2f4fa7ad 100644 --- a/Tests/DependencyInjection/SerializerPassTest.php +++ b/Tests/DependencyInjection/SerializerPassTest.php @@ -77,9 +77,11 @@ public function testServicesAreOrderedAccordingToPriority() public function testBindSerializerDefaultContext() { + $context = ['enable_max_depth' => true]; + $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); - $container->register('serializer')->setArguments([null, null]); + $container->register('serializer')->setArguments([null, null, []]); $container->setParameter('serializer.default_context', ['enable_max_depth' => true]); $definition = $container->register('n1')->addTag('serializer.normalizer')->addTag('serializer.encoder'); @@ -87,7 +89,8 @@ public function testBindSerializerDefaultContext() $serializerPass->process($container); $bindings = $definition->getBindings(); - $this->assertEquals($bindings['array $defaultContext'], new BoundArgument(['enable_max_depth' => true], false)); + $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($context, false)); + $this->assertEquals($context, $container->getDefinition('serializer')->getArgument('$defaultContext')); } public function testNormalizersAndEncodersAreDecoredAndOrderedWhenCollectingData() diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 8f60ae1d4..8a8a54e98 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -1652,6 +1652,32 @@ public function testPartialDenormalizationWithInvalidVariadicParameter() DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, ]); } + + public function testEmptyArrayAsObjectDefaultContext() + { + $serializer = new Serializer( + defaultContext: [Serializer::EMPTY_ARRAY_AS_OBJECT => true], + ); + $this->assertEquals(new \ArrayObject(), $serializer->normalize([])); + } + + public function testPreserveEmptyObjectsAsDefaultContext() + { + $serializer = new Serializer( + defaultContext: [AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true], + ); + $this->assertEquals(new \ArrayObject(), $serializer->normalize(new \ArrayIterator())); + } + + public function testCollectDenormalizationErrorsDefaultContext() + { + $data = ['variadic' => ['a random string']]; + $serializer = new Serializer([new UidNormalizer(), new ObjectNormalizer()], [], [DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true]); + + $this->expectException(PartialDenormalizationException::class); + + $serializer->denormalize($data, DummyWithVariadicParameter::class); + } } class Model From ed5e8b563e10496a4f62557fbcb52b4191f2157d Mon Sep 17 00:00:00 2001 From: HypeMC Date: Wed, 5 Feb 2025 08:18:49 +0100 Subject: [PATCH 096/104] [Serializer] Handle default context in named Serializer --- DependencyInjection/SerializerPass.php | 2 +- Tests/DependencyInjection/SerializerPassTest.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/DependencyInjection/SerializerPass.php b/DependencyInjection/SerializerPass.php index bf1296e93..7b7f6f1c2 100644 --- a/DependencyInjection/SerializerPass.php +++ b/DependencyInjection/SerializerPass.php @@ -151,7 +151,7 @@ private function configureNamedSerializers(ContainerBuilder $container): void $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $config['default_context']); - $container->registerChild($serializerId, 'serializer'); + $container->registerChild($serializerId, 'serializer')->setArgument('$defaultContext', $config['default_context']); $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer'); $this->configureSerializer($container, $serializerId, $normalizers, $encoders, $serializerName); diff --git a/Tests/DependencyInjection/SerializerPassTest.php b/Tests/DependencyInjection/SerializerPassTest.php index ca54460a7..769243be2 100644 --- a/Tests/DependencyInjection/SerializerPassTest.php +++ b/Tests/DependencyInjection/SerializerPassTest.php @@ -556,7 +556,7 @@ public function testBindSerializerDefaultContextToNamedSerializers() 'api' => ['default_context' => $defaultContext = ['enable_max_depth' => true]], ]); - $container->register('serializer')->setArguments([null, null]); + $container->register('serializer')->setArguments([null, null, []]); $definition = $container->register('n1') ->addTag('serializer.normalizer', ['serializer' => '*']) ->addTag('serializer.encoder', ['serializer' => '*']) @@ -570,6 +570,8 @@ public function testBindSerializerDefaultContextToNamedSerializers() $bindings = $container->getDefinition('n1.api')->getBindings(); $this->assertArrayHasKey('array $defaultContext', $bindings); $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($defaultContext, false)); + $this->assertArrayNotHasKey('$defaultContext', $container->getDefinition('serializer')->getArguments()); + $this->assertEquals($defaultContext, $container->getDefinition('serializer.api')->getArgument('$defaultContext')); } public function testNamedSerializersAreRegistered() From a221b2f6066af304d760cff7a26f201b4fab4aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Hlavat=C3=BD?= <107676055+Pepperoni1337@users.noreply.github.com> Date: Sun, 23 Feb 2025 11:21:59 +0100 Subject: [PATCH 097/104] Update GetSetMethodNormalizer.php Fix: Add length check for setter method detection --- Normalizer/GetSetMethodNormalizer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index 951005545..3cb9b992b 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -118,6 +118,7 @@ private function isSetMethod(\ReflectionMethod $method): bool return !$method->isStatic() && !$method->getAttributes(Ignore::class) && 0 < $method->getNumberOfParameters() + && 3 < \strlen($method->name) && str_starts_with($method->name, 'set') && !ctype_lower($method->name[3]) ; From d8b75b2c8144c29ac43b235738411f7cca6d584d Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 24 Mar 2025 05:35:59 +0100 Subject: [PATCH 098/104] [Serializer] Fix ObjectNormalizer default context with named serializers --- DependencyInjection/SerializerPass.php | 38 +++++++++--- .../SerializerPassTest.php | 62 ++++++++++++++++++- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/DependencyInjection/SerializerPass.php b/DependencyInjection/SerializerPass.php index 7b7f6f1c2..179b7a3d9 100644 --- a/DependencyInjection/SerializerPass.php +++ b/DependencyInjection/SerializerPass.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Debug\TraceableNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; /** @@ -54,17 +55,27 @@ public function process(ContainerBuilder $container): void throw new RuntimeException('You must tag at least one service as "serializer.encoder" to use the "serializer" service.'); } + $defaultContext = []; if ($container->hasParameter('serializer.default_context')) { $defaultContext = $container->getParameter('serializer.default_context'); - $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $defaultContext); $container->getParameterBag()->remove('serializer.default_context'); $container->getDefinition('serializer')->setArgument('$defaultContext', $defaultContext); } + /** @var ?string $circularReferenceHandler */ + $circularReferenceHandler = $container->hasParameter('.serializer.circular_reference_handler') + ? $container->getParameter('.serializer.circular_reference_handler') : null; + + /** @var ?string $maxDepthHandler */ + $maxDepthHandler = $container->hasParameter('.serializer.max_depth_handler') + ? $container->getParameter('.serializer.max_depth_handler') : null; + + $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $defaultContext, $circularReferenceHandler, $maxDepthHandler); + $this->configureSerializer($container, 'serializer', $normalizers, $encoders, 'default'); if ($namedSerializers) { - $this->configureNamedSerializers($container); + $this->configureNamedSerializers($container, $circularReferenceHandler, $maxDepthHandler); } } @@ -98,11 +109,22 @@ private function createNamedSerializerTags(ContainerBuilder $container, string $ } } - private function bindDefaultContext(ContainerBuilder $container, array $services, array $defaultContext): void + private function bindDefaultContext(ContainerBuilder $container, array $services, array $defaultContext, ?string $circularReferenceHandler, ?string $maxDepthHandler): void { foreach ($services as $id) { $definition = $container->getDefinition((string) $id); - $definition->setBindings(['array $defaultContext' => new BoundArgument($defaultContext, false)] + $definition->getBindings()); + + $context = $defaultContext; + if (is_a($definition->getClass(), ObjectNormalizer::class, true)) { + if (null !== $circularReferenceHandler) { + $context += ['circular_reference_handler' => new Reference($circularReferenceHandler)]; + } + if (null !== $maxDepthHandler) { + $context += ['max_depth_handler' => new Reference($maxDepthHandler)]; + } + } + + $definition->setBindings(['array $defaultContext' => new BoundArgument($context, false)] + $definition->getBindings()); } } @@ -125,7 +147,7 @@ private function configureSerializer(ContainerBuilder $container, string $id, ar $serializerDefinition->replaceArgument(1, $encoders); } - private function configureNamedSerializers(ContainerBuilder $container): void + private function configureNamedSerializers(ContainerBuilder $container, ?string $circularReferenceHandler, ?string $maxDepthHandler): void { $defaultSerializerNameConverter = $container->hasParameter('.serializer.name_converter') ? $container->getParameter('.serializer.name_converter') : null; @@ -149,7 +171,7 @@ private function configureNamedSerializers(ContainerBuilder $container): void $normalizers = $this->buildChildDefinitions($container, $serializerName, $normalizers, $config); $encoders = $this->buildChildDefinitions($container, $serializerName, $encoders, $config); - $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $config['default_context']); + $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $config['default_context'], $circularReferenceHandler, $maxDepthHandler); $container->registerChild($serializerId, 'serializer')->setArgument('$defaultContext', $config['default_context']); $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer'); @@ -184,7 +206,9 @@ private function buildChildDefinitions(ContainerBuilder $container, string $seri foreach ($services as &$id) { $childId = $id.'.'.$serializerName; - $definition = $container->registerChild($childId, (string) $id); + $definition = $container->registerChild($childId, (string) $id) + ->setClass($container->getDefinition((string) $id)->getClass()) + ; if (null !== $nameConverterIndex = $this->findNameConverterIndex($container, (string) $id)) { $definition->replaceArgument($nameConverterIndex, new Reference($config['name_converter'])); diff --git a/Tests/DependencyInjection/SerializerPassTest.php b/Tests/DependencyInjection/SerializerPassTest.php index 769243be2..88ec02b87 100644 --- a/Tests/DependencyInjection/SerializerPassTest.php +++ b/Tests/DependencyInjection/SerializerPassTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Debug\TraceableNormalizer; use Symfony\Component\Serializer\Debug\TraceableSerializer; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\SerializerInterface; /** @@ -99,6 +100,32 @@ public function testBindSerializerDefaultContext() $this->assertEquals($context, $container->getDefinition('serializer')->getArgument('$defaultContext')); } + /** + * @testWith [{}, {}] + * [{"serializer.default_context": {"enable_max_depth": true}}, {"enable_max_depth": true}] + * [{".serializer.circular_reference_handler": "foo"}, {"circular_reference_handler": "foo"}] + * [{".serializer.max_depth_handler": "bar"}, {"max_depth_handler": "bar"}] + * [{"serializer.default_context": {"enable_max_depth": true}, ".serializer.circular_reference_handler": "foo", ".serializer.max_depth_handler": "bar"}, {"enable_max_depth": true, "circular_reference_handler": "foo", "max_depth_handler": "bar"}] + */ + public function testBindObjectNormalizerDefaultContext(array $parameters, array $context) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->register('serializer')->setArguments([null, null, []]); + $container->getParameterBag()->add($parameters); + $definition = $container->register('serializer.normalizer.object') + ->setClass(ObjectNormalizer::class) + ->addTag('serializer.normalizer') + ->addTag('serializer.encoder') + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $bindings = $definition->getBindings(); + $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($context, false)); + } + public function testNormalizersAndEncodersAreDecoratedAndOrderedWhenCollectingData() { $container = new ContainerBuilder(); @@ -565,7 +592,9 @@ public function testBindSerializerDefaultContextToNamedSerializers() $serializerPass = new SerializerPass(); $serializerPass->process($container); - $this->assertEmpty($definition->getBindings()); + $bindings = $definition->getBindings(); + $this->assertArrayHasKey('array $defaultContext', $bindings); + $this->assertEquals($bindings['array $defaultContext'], new BoundArgument([], false)); $bindings = $container->getDefinition('n1.api')->getBindings(); $this->assertArrayHasKey('array $defaultContext', $bindings); @@ -574,6 +603,37 @@ public function testBindSerializerDefaultContextToNamedSerializers() $this->assertEquals($defaultContext, $container->getDefinition('serializer.api')->getArgument('$defaultContext')); } + /** + * @testWith [{}, {}, {}] + * [{"enable_max_depth": true}, {}, {"enable_max_depth": true}] + * [{}, {".serializer.circular_reference_handler": "foo"}, {"circular_reference_handler": "foo"}] + * [{}, {".serializer.max_depth_handler": "bar"}, {"max_depth_handler": "bar"}] + * [{"enable_max_depth": true}, {".serializer.circular_reference_handler": "foo", ".serializer.max_depth_handler": "bar"}, {"enable_max_depth": true, "circular_reference_handler": "foo", "max_depth_handler": "bar"}] + */ + public function testBindNamedSerializerObjectNormalizerDefaultContext(array $defaultContext, array $parameters, array $context) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => ['default_context' => $defaultContext], + ]); + + $container->register('serializer')->setArguments([null, null, []]); + $container->getParameterBag()->add($parameters); + $container->register('serializer.normalizer.object') + ->setClass(ObjectNormalizer::class) + ->addTag('serializer.normalizer', ['serializer' => '*']) + ->addTag('serializer.encoder', ['serializer' => '*']) + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $bindings = $container->getDefinition('serializer.normalizer.object.api')->getBindings(); + $this->assertArrayHasKey('array $defaultContext', $bindings); + $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($context, false)); + } + public function testNamedSerializersAreRegistered() { $container = new ContainerBuilder(); From c45f8f7763afb11e85772c0c1debb8f272c17f51 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 27 Apr 2025 15:26:02 +0200 Subject: [PATCH 099/104] Remove unneeded use statements --- Tests/SerializerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 8a8a54e98..da5ccc15e 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -62,7 +62,6 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithVariadicParameter; -use Symfony\Component\Serializer\Tests\Fixtures\DummyWithVariadicProperty; use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\FooImplementationDummy; use Symfony\Component\Serializer\Tests\Fixtures\FooInterfaceDummyDenormalizer; From 72bf83d4cbab76f1c492c9c41ac104bf3671ea58 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Wed, 23 Apr 2025 18:56:44 -0300 Subject: [PATCH 100/104] Fix: prevent "Cannot traverse an already closed generator" error by materializing Traversable input --- Encoder/CsvEncoder.php | 4 ++++ Tests/Encoder/CsvEncoderTest.php | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/Encoder/CsvEncoder.php b/Encoder/CsvEncoder.php index 8f9c3cff0..07043d946 100644 --- a/Encoder/CsvEncoder.php +++ b/Encoder/CsvEncoder.php @@ -65,6 +65,10 @@ public function encode(mixed $data, string $format, array $context = []): string } elseif (empty($data)) { $data = [[]]; } else { + if ($data instanceof \Traversable) { + // Generators can only be iterated once — convert to array to allow multiple traversals + $data = iterator_to_array($data); + } // Sequential arrays of arrays are considered as collections $i = 0; foreach ($data as $key => $value) { diff --git a/Tests/Encoder/CsvEncoderTest.php b/Tests/Encoder/CsvEncoderTest.php index c0be73a8b..cde6d333e 100644 --- a/Tests/Encoder/CsvEncoderTest.php +++ b/Tests/Encoder/CsvEncoderTest.php @@ -710,4 +710,28 @@ public function testEndOfLinePassedInConstructor() $encoder = new CsvEncoder([CsvEncoder::END_OF_LINE => "\r\n"]); $this->assertSame("foo,bar\r\nhello,test\r\n", $encoder->encode($value, 'csv')); } + + /** @dataProvider provideIterable */ + public function testIterable(mixed $data) + { + $this->assertEquals(<<<'CSV' + foo,bar + hello,"hey ho" + hi,"let's go" + + CSV, $this->encoder->encode($data, 'csv')); + } + + public static function provideIterable() + { + $data = [ + ['foo' => 'hello', 'bar' => 'hey ho'], + ['foo' => 'hi', 'bar' => 'let\'s go'], + ]; + + yield 'array' => [$data]; + yield 'array iterator' => [new \ArrayIterator($data)]; + yield 'iterator aggregate' => [new \IteratorIterator(new \ArrayIterator($data))]; + yield 'generator' => [(fn (): \Generator => yield from $data)()]; + } } From 987915bb7557c282a72f566f3c0470b713e448a8 Mon Sep 17 00:00:00 2001 From: Orestis Date: Tue, 17 Jun 2025 11:50:15 +0200 Subject: [PATCH 101/104] Fix TraceableSerializer when collected caller from array map If the TraceableSerializer runs from a callable in an array_map, then the caller looses the file and the line because it was invoked. This causes a hard fail since the required items are not defined. The error is the following: TraceableSerializer.php line 174: Warning: Undefined array key "file". The fix is to get the file and the line from the following array item which always is the caller class. --- Debug/TraceableSerializer.php | 4 +-- Tests/Debug/TraceableSerializerTest.php | 34 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Debug/TraceableSerializer.php b/Debug/TraceableSerializer.php index dd22e8678..964fd7503 100644 --- a/Debug/TraceableSerializer.php +++ b/Debug/TraceableSerializer.php @@ -179,8 +179,8 @@ private function getCaller(string $method, string $interface): array && $method === $trace[$i]['function'] && is_a($trace[$i]['class'], $interface, true) ) { - $file = $trace[$i]['file']; - $line = $trace[$i]['line']; + $file = $trace[$i]['file'] ?? $trace[$i + 1]['file']; + $line = $trace[$i]['line'] ?? $trace[$i + 1]['line']; break; } diff --git a/Tests/Debug/TraceableSerializerTest.php b/Tests/Debug/TraceableSerializerTest.php index ea3c851c6..dc6e4a6b7 100644 --- a/Tests/Debug/TraceableSerializerTest.php +++ b/Tests/Debug/TraceableSerializerTest.php @@ -126,6 +126,40 @@ public function testAddDebugTraceIdInContext() $traceableSerializer->encode('data', 'format'); $traceableSerializer->decode('data', 'format'); } + + public function testCollectedCaller() + { + $serializer = new \Symfony\Component\Serializer\Serializer(); + + $collector = new SerializerDataCollector(); + $traceableSerializer = new TraceableSerializer($serializer, $collector); + + $traceableSerializer->normalize('data'); + $collector->lateCollect(); + + $this->assertSame([ + 'name' => 'TraceableSerializerTest.php', + 'file' => __FILE__, + 'line' => __LINE__ - 6, + ], $collector->getData()['normalize'][0]['caller']); + } + + public function testCollectedCallerFromArrayMap() + { + $serializer = new \Symfony\Component\Serializer\Serializer(); + + $collector = new SerializerDataCollector(); + $traceableSerializer = new TraceableSerializer($serializer, $collector); + + array_map([$traceableSerializer, 'normalize'], ['data']); + $collector->lateCollect(); + + $this->assertSame([ + 'name' => 'TraceableSerializerTest.php', + 'file' => __FILE__, + 'line' => __LINE__ - 6, + ], $collector->getData()['normalize'][0]['caller']); + } } class Serializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface From b828e2a67595d7435c5f3700454370c858fdc6f7 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 19 Jun 2025 14:18:57 +0200 Subject: [PATCH 102/104] [Serializer] Add support for discriminator map in property normalizer Fixes #60214 Currently it's not possible to serialize an object using the PropertyNormalizer when a DiscriminatorMap attribute is used. It produces the following error: > Symfony\Component\Serializer\Exception\NotNormalizableValueException: Type property "type" not found > for the abstract object "Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface". The ObjectNormalizer overrides the `getAllowedAttributes` from AbstractNormalizer and adds support for discriminators. But the PropertyNormalizer does not do this. Therefore it doesn't work. For now, we copy the logic from ObjectNormalizer to PropertyNormalizer and the problem goes away. --- Normalizer/AbstractObjectNormalizer.php | 25 ++++++++++++++++ Normalizer/ObjectNormalizer.php | 24 --------------- Tests/Fixtures/DummyMessageInterface.php | 1 + Tests/Fixtures/DummyMessageNumberFour.php | 29 +++++++++++++++++++ .../AbstractObjectNormalizerTest.php | 22 ++++++++++++++ 5 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 Tests/Fixtures/DummyMessageNumberFour.php diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index b27b1985e..7422c849d 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -25,6 +25,7 @@ use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; @@ -765,6 +766,30 @@ protected function createChildContext(array $parentContext, string $attribute, ? return $context; } + protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool + { + if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) { + return false; + } + + if (null !== $this->classDiscriminatorResolver) { + $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject; + if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) { + $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty()); + } + + if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { + $attributes = []; + foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) { + $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString); + } + $allowedAttributes = array_merge($allowedAttributes, ...$attributes); + } + } + + return $allowedAttributes; + } + /** * Builds the cache key for the attributes cache. * diff --git a/Normalizer/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index e93d7b4cc..c06c19e24 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -164,30 +164,6 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v } } - protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool - { - if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) { - return false; - } - - if (null !== $this->classDiscriminatorResolver) { - $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject; - if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) { - $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty()); - } - - if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { - $attributes = []; - foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) { - $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString); - } - $allowedAttributes = array_merge($allowedAttributes, ...$attributes); - } - } - - return $allowedAttributes; - } - protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []) { if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) { diff --git a/Tests/Fixtures/DummyMessageInterface.php b/Tests/Fixtures/DummyMessageInterface.php index 31206ea67..ea26589a2 100644 --- a/Tests/Fixtures/DummyMessageInterface.php +++ b/Tests/Fixtures/DummyMessageInterface.php @@ -20,6 +20,7 @@ 'one' => DummyMessageNumberOne::class, 'two' => DummyMessageNumberTwo::class, 'three' => DummyMessageNumberThree::class, + 'four' => DummyMessageNumberFour::class, ])] interface DummyMessageInterface { diff --git a/Tests/Fixtures/DummyMessageNumberFour.php b/Tests/Fixtures/DummyMessageNumberFour.php new file mode 100644 index 000000000..eaf87d48a --- /dev/null +++ b/Tests/Fixtures/DummyMessageNumberFour.php @@ -0,0 +1,29 @@ + + * + * 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; + +use Symfony\Component\Serializer\Attribute\Ignore; + +abstract class SomeAbstract { + #[Ignore] + public function getDescription() + { + return 'Hello, World!'; + } +} + +class DummyMessageNumberFour extends SomeAbstract implements DummyMessageInterface +{ + public function __construct(public $one) + { + } +} diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 0cca05db3..c2349901f 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -42,6 +42,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -49,6 +50,8 @@ use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild; use Symfony\Component\Serializer\Tests\Fixtures\DummyFirstChildQuux; +use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface; +use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberFour; use Symfony\Component\Serializer\Tests\Fixtures\DummySecondChildQuux; use Symfony\Component\Serializer\Tests\Fixtures\DummyString; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable; @@ -1087,6 +1090,25 @@ public static function provideBooleanTypesData() [['foo' => false], TruePropertyDummy::class], ]; } + + public function testDeserializeAndSerializeConstructorAndIgnoreAndInterfacedObjectsWithTheClassMetadataDiscriminator() + { + $example = new DummyMessageNumberFour('Hello'); + + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + + $normalizer = new PropertyNormalizer( + $classMetadataFactory, + null, + new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]), + new ClassDiscriminatorFromClassMetadata($classMetadataFactory), + ); + + $serialized = $normalizer->normalize($example, 'json'); + $deserialized = $normalizer->denormalize($serialized, DummyMessageInterface::class, 'json'); + + $this->assertEquals($example, $deserialized); + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer From 0aa40bf7fa213d8b4ab254928f5e08722a81236b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Nork=C5=ABnas?= Date: Wed, 18 Jun 2025 14:20:51 +0300 Subject: [PATCH 103/104] [TypeInfo] Fix handling `ConstFetchNode` --- Tests/Normalizer/ObjectNormalizerTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index d45586b44..83b0bbcdb 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -748,6 +748,8 @@ public function testDoesntHaveIssuesWithUnionConstTypes() $serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]); $this->assertSame('bar', $serializer->denormalize(['foo' => 'bar'], (new class { + public const TEST = 'me'; + /** @var self::*|null */ public $foo; })::class)->foo); From b91a31cfb535eeb0edf93047b383dfba487cf4f0 Mon Sep 17 00:00:00 2001 From: Danil Date: Tue, 13 May 2025 23:21:56 +1000 Subject: [PATCH 104/104] [Serializer] Fix collect_denormalization_errors flag in defaultContext --- Serializer.php | 2 +- Tests/SerializerTest.php | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Serializer.php b/Serializer.php index e17042097..9d0c45a6b 100644 --- a/Serializer.php +++ b/Serializer.php @@ -222,7 +222,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type)); } - if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) { + if ((isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) || isset($this->defaultContext[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) && !isset($context['not_normalizable_value_exceptions'])) { unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]); $context['not_normalizable_value_exceptions'] = []; $errors = &$context['not_normalizable_value_exceptions']; diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index da5ccc15e..b0dc887ce 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -1677,6 +1677,54 @@ public function testCollectDenormalizationErrorsDefaultContext() $serializer->denormalize($data, DummyWithVariadicParameter::class); } + + public function testDenormalizationFailsWithMultipleErrorsInDefaultContext() + { + $serializer = new Serializer( + [new DateTimeNormalizer(), new ObjectNormalizer()], + [], + [DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true] + ); + + $data = ['date' => '', 'unknown' => null]; + + try { + $serializer->denormalize($data, DummyEntityWithStringAndDateTime::class); + $this->fail('Expected PartialDenormalizationException was not thrown'); + } catch (PartialDenormalizationException $e) { + $this->assertIsArray($e->getErrors()); + $this->assertCount(2, $e->getErrors(), 'Expected two denormalization errors'); + + $exceptionsAsArray = array_map(function (NotNormalizableValueException $ex): array { + return [ + 'currentType' => $ex->getCurrentType(), + 'expectedTypes' => $ex->getExpectedTypes(), + 'path' => $ex->getPath(), + 'useMessageForUser' => $ex->canUseMessageForUser(), + 'message' => $ex->getMessage(), + ]; + }, $e->getErrors()); + + $expected = [ + [ + 'currentType' => 'null', + 'expectedTypes' => ['string'], + 'path' => 'bar', + 'useMessageForUser' => true, + 'message' => 'Failed to create object because the class misses the "bar" property.', + ], + [ + 'currentType' => 'string', + 'expectedTypes' => ['string'], + 'path' => 'date', + 'useMessageForUser' => true, + 'message' => 'The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.', + ], + ]; + + $this->assertSame($expected, $exceptionsAsArray); + } + } } class Model @@ -1743,6 +1791,15 @@ public function __construct($value) } } +class DummyEntityWithStringAndDateTime +{ + public function __construct( + public string $bar, + public \DateTimeInterface $date, + ) { + } +} + class DummyUnionType { /**