From 61a08a6f6e24e45bcdabc4454a084f7d943682c7 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Wed, 11 Dec 2024 14:08:35 +0100 Subject: [PATCH 01/19] chore: PHP CS Fixer fixes --- Tests/SerializerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/SerializerTest.php b/Tests/SerializerTest.php index 50891e7e..e59e4402 100644 --- a/Tests/SerializerTest.php +++ b/Tests/SerializerTest.php @@ -65,7 +65,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 bb7b4c20560d5f0bea3a035eb3adb4f6ae08ea00 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 13 Dec 2024 22:36:21 +0100 Subject: [PATCH 02/19] chore: PHP CS Fixer fixes --- Normalizer/GetSetMethodNormalizer.php | 2 +- Tests/Attribute/ContextTest.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index f07adc2f..f3d0d924 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -102,7 +102,7 @@ private function isSetMethod(\ReflectionMethod $method): bool && 0 < $method->getNumberOfParameters() && 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/Tests/Attribute/ContextTest.php b/Tests/Attribute/ContextTest.php index cfe17505..ff149696 100644 --- a/Tests/Attribute/ContextTest.php +++ b/Tests/Attribute/ContextTest.php @@ -86,7 +86,7 @@ public static function provideValidInputs(): iterable -normalizationContext: [] -denormalizationContext: [] } -DUMP +DUMP, ]; yield 'named arguments: with normalization context option' => [ @@ -100,7 +100,7 @@ public static function provideValidInputs(): iterable ] -denormalizationContext: [] } -DUMP +DUMP, ]; yield 'named arguments: with denormalization context option' => [ @@ -114,7 +114,7 @@ public static function provideValidInputs(): iterable "foo" => "bar", ] } -DUMP +DUMP, ]; yield 'named arguments: with groups option as string' => [ @@ -130,7 +130,7 @@ public static function provideValidInputs(): iterable -normalizationContext: [] -denormalizationContext: [] } -DUMP +DUMP, ]; yield 'named arguments: with groups option as array' => [ @@ -147,7 +147,7 @@ public static function provideValidInputs(): iterable -normalizationContext: [] -denormalizationContext: [] } -DUMP +DUMP, ]; } } From 497a3c7c75ce83d74d71a1eb7809b5b2967e06ed Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Mon, 23 Dec 2024 10:07:10 +0100 Subject: [PATCH 03/19] [Serializer] Deprecate the `CompiledClassMetadataFactory` --- CHANGELOG.md | 5 +++++ CacheWarmer/CompiledClassMetadataCacheWarmer.php | 4 ++++ Mapping/Factory/CompiledClassMetadataFactory.php | 4 ++++ Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php | 3 +++ Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php | 2 ++ 5 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c36d588..525651fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes + 7.2 --- diff --git a/CacheWarmer/CompiledClassMetadataCacheWarmer.php b/CacheWarmer/CompiledClassMetadataCacheWarmer.php index 379a2a38..1bd08502 100644 --- a/CacheWarmer/CompiledClassMetadataCacheWarmer.php +++ b/CacheWarmer/CompiledClassMetadataCacheWarmer.php @@ -16,8 +16,12 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +trigger_deprecation('symfony/serializer', '7.3', 'The "%s" class is deprecated.', CompiledClassMetadataCacheWarmer::class); + /** * @author Fabien Bourigault + * + * @deprecated since Symfony 7.3 */ final class CompiledClassMetadataCacheWarmer implements CacheWarmerInterface { diff --git a/Mapping/Factory/CompiledClassMetadataFactory.php b/Mapping/Factory/CompiledClassMetadataFactory.php index ec25d744..759da166 100644 --- a/Mapping/Factory/CompiledClassMetadataFactory.php +++ b/Mapping/Factory/CompiledClassMetadataFactory.php @@ -16,8 +16,12 @@ use Symfony\Component\Serializer\Mapping\ClassMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; +trigger_deprecation('symfony/serializer', '7.3', 'The "%s" class is deprecated.', CompiledClassMetadataFactory::class); + /** * @author Fabien Bourigault + * + * @deprecated since Symfony 7.3 */ final class CompiledClassMetadataFactory implements ClassMetadataFactoryInterface { diff --git a/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php b/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php index 9d354270..c9f5081b 100644 --- a/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php +++ b/Tests/CacheWarmer/CompiledClassMetadataCacheWarmerTest.php @@ -18,6 +18,9 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +/** + * @group legacy + */ final class CompiledClassMetadataCacheWarmerTest extends TestCase { public function testItImplementsCacheWarmerInterface() diff --git a/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php b/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php index ff54fb96..e77a8bf3 100644 --- a/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php +++ b/Tests/Mapping/Factory/CompiledClassMetadataFactoryTest.php @@ -21,6 +21,8 @@ /** * @author Fabien Bourigault + * + * @group legacy */ final class CompiledClassMetadataFactoryTest extends TestCase { From 59100acfa7a72a8a3a8a2733bcf81d37a36c1ec1 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 27 Dec 2024 11:05:13 +0100 Subject: [PATCH 04/19] [Serializer] Document `SerializerInterface` exceptions --- SerializerInterface.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/SerializerInterface.php b/SerializerInterface.php index b883dbea..7ee63a77 100644 --- a/SerializerInterface.php +++ b/SerializerInterface.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Serializer; +use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; + /** * @author Jordi Boggiano */ @@ -20,6 +24,10 @@ interface SerializerInterface * Serializes data in the appropriate format. * * @param array $context Options normalizers/encoders have access to + * + * @throws NotNormalizableValueException Occurs when a value cannot be normalized + * @throws UnexpectedValueException Occurs when a value cannot be encoded + * @throws ExceptionInterface Occurs for all the other cases of serialization-related errors */ public function serialize(mixed $data, string $format, array $context = []): string; @@ -35,6 +43,10 @@ public function serialize(mixed $data, string $format, array $context = []): str * @psalm-return (TType is class-string ? TObject : mixed) * * @phpstan-return ($type is class-string ? TObject : mixed) + * + * @throws NotNormalizableValueException Occurs when a value cannot be denormalized + * @throws UnexpectedValueException Occurs when a value cannot be decoded + * @throws ExceptionInterface Occurs for all the other cases of serialization-related errors */ public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed; } From b5de46850df37dc14d2da13aac5c7d8f5fd50d57 Mon Sep 17 00:00:00 2001 From: Jan Rosier Date: Mon, 6 Jan 2025 15:35:18 +0100 Subject: [PATCH 05/19] Use spl_object_id() instead of spl_object_hash() --- Normalizer/AbstractNormalizer.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Normalizer/AbstractNormalizer.php b/Normalizer/AbstractNormalizer.php index 04f378c4..4aba4b0b 100644 --- a/Normalizer/AbstractNormalizer.php +++ b/Normalizer/AbstractNormalizer.php @@ -164,19 +164,19 @@ public function __construct( */ protected function isCircularReference(object $object, array &$context): bool { - $objectHash = spl_object_hash($object); + $objectId = spl_object_id($object); $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT]; - if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) { - if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) { - unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]); + if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId])) { + if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId] >= $circularReferenceLimit) { + unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId]); return true; } - ++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]; + ++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId]; } else { - $context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1; + $context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectId] = 1; } return false; From 83580c155b63bf439d462e2ee8dba27804f4dd20 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 10 Jan 2025 15:17:09 +0100 Subject: [PATCH 06/19] chore: PHP CS Fixer fixes --- 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 499fa8bf..72acb994 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -1587,7 +1587,7 @@ class TruePropertyDummy class BoolPropertyDummy { - /** @var null|bool */ + /** @var bool|null */ public $foo; } From 086586545b21bbb09cb48d01689767743f436800 Mon Sep 17 00:00:00 2001 From: Quentin Dequippe Date: Fri, 18 Oct 2024 17:17:23 +0400 Subject: [PATCH 07/19] [Serializer] Add xml context option to ignore empty attributes --- CHANGELOG.md | 10 +++++++--- Context/Encoder/XmlEncoderContextBuilder.php | 8 ++++++++ Encoder/XmlEncoder.php | 9 +++++++++ .../Encoder/XmlEncoderContextBuilderTest.php | 3 +++ Tests/Encoder/XmlEncoderTest.php | 13 +++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a04c323d..525651fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ CHANGELOG ========= +7.3 +--- + + * Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes + 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 @@ -19,7 +24,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/Context/Encoder/XmlEncoderContextBuilder.php b/Context/Encoder/XmlEncoderContextBuilder.php index 0fd1f2f4..7a5097e9 100644 --- a/Context/Encoder/XmlEncoderContextBuilder.php +++ b/Context/Encoder/XmlEncoderContextBuilder.php @@ -160,4 +160,12 @@ public function withCdataWrappingPattern(?string $cdataWrappingPattern): static { return $this->with(XmlEncoder::CDATA_WRAPPING_PATTERN, $cdataWrappingPattern); } + + /** + * Configures whether to ignore empty attributes. + */ + public function withIgnoreEmptyAttributes(?bool $ignoreEmptyAttributes): static + { + return $this->with(XmlEncoder::IGNORE_EMPTY_ATTRIBUTES, $ignoreEmptyAttributes); + } } diff --git a/Encoder/XmlEncoder.php b/Encoder/XmlEncoder.php index e1a81638..ed66fa30 100644 --- a/Encoder/XmlEncoder.php +++ b/Encoder/XmlEncoder.php @@ -60,6 +60,7 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa public const VERSION = 'xml_version'; public const CDATA_WRAPPING = 'cdata_wrapping'; public const CDATA_WRAPPING_PATTERN = 'cdata_wrapping_pattern'; + public const IGNORE_EMPTY_ATTRIBUTES = 'ignore_empty_attributes'; private array $defaultContext = [ self::AS_COLLECTION => false, @@ -72,6 +73,7 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa self::TYPE_CAST_ATTRIBUTES => true, self::CDATA_WRAPPING => true, self::CDATA_WRAPPING_PATTERN => '/[<>&]/', + self::IGNORE_EMPTY_ATTRIBUTES => false, ]; public function __construct(array $defaultContext = []) @@ -355,6 +357,13 @@ private function buildXml(\DOMNode $parentNode, mixed $data, string $format, arr if (\is_bool($data)) { $data = (int) $data; } + + if ($context[self::IGNORE_EMPTY_ATTRIBUTES] ?? $this->defaultContext[self::IGNORE_EMPTY_ATTRIBUTES]) { + if (null === $data || '' === $data) { + continue; + } + } + $parentNode->setAttribute($attributeName, $data); } elseif ('#' === $key) { $append = $this->selectNodeType($parentNode, $data, $format, $context); diff --git a/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php b/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php index 2f71c601..4175751b 100644 --- a/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php +++ b/Tests/Context/Encoder/XmlEncoderContextBuilderTest.php @@ -47,6 +47,7 @@ public function testWithers(array $values) ->withVersion($values[XmlEncoder::VERSION]) ->withCdataWrapping($values[XmlEncoder::CDATA_WRAPPING]) ->withCdataWrappingPattern($values[XmlEncoder::CDATA_WRAPPING_PATTERN]) + ->withIgnoreEmptyAttributes($values[XmlEncoder::IGNORE_EMPTY_ATTRIBUTES]) ->toArray(); $this->assertSame($values, $context); @@ -69,6 +70,7 @@ public static function withersDataProvider(): iterable XmlEncoder::VERSION => '1.0', XmlEncoder::CDATA_WRAPPING => false, XmlEncoder::CDATA_WRAPPING_PATTERN => '/[<>&"\']/', + XmlEncoder::IGNORE_EMPTY_ATTRIBUTES => true, ]]; yield 'With null values' => [[ @@ -86,6 +88,7 @@ public static function withersDataProvider(): iterable XmlEncoder::VERSION => null, XmlEncoder::CDATA_WRAPPING => null, XmlEncoder::CDATA_WRAPPING_PATTERN => null, + XmlEncoder::IGNORE_EMPTY_ATTRIBUTES => null, ]]; } } diff --git a/Tests/Encoder/XmlEncoderTest.php b/Tests/Encoder/XmlEncoderTest.php index 31d2ddfc..ca36554e 100644 --- a/Tests/Encoder/XmlEncoderTest.php +++ b/Tests/Encoder/XmlEncoderTest.php @@ -1004,4 +1004,17 @@ private function createXmlWithDateTimeField(): string ', $this->exampleDateTimeString); } + + public function testEncodeIgnoringEmptyAttribute() + { + $expected = <<<'XML' + +Test + +XML; + + $data = ['#' => 'Test', '@attribute' => '', '@attribute2' => null]; + + $this->assertEquals($expected, $this->encoder->encode($data, 'xml', ['ignore_empty_attributes' => true])); + } } From bd81b09500b9b1c7cbff8d019488d10c8ad873d3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 17 Jan 2025 08:30:45 +0100 Subject: [PATCH 08/19] Revert bad merge --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 525651fc..b5e302aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ CHANGELOG 7.2 --- - * 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 + * 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 * Add the `UidNormalizer::NORMALIZATION_FORMAT_RFC9562` constant @@ -24,6 +24,7 @@ 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) From 7c40203a0614a7ebedff028057fb752e2dfee70e Mon Sep 17 00:00:00 2001 From: Mathieu Rochette Date: Tue, 28 Jan 2025 23:20:12 +0100 Subject: [PATCH 09/19] [Serializer] register named normalizer & denormalizer aliases --- CHANGELOG.md | 1 + DependencyInjection/SerializerPass.php | 4 ++++ Tests/DependencyInjection/SerializerPassTest.php | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e302aa..a4fc951f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes + * Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers 7.2 --- diff --git a/DependencyInjection/SerializerPass.php b/DependencyInjection/SerializerPass.php index bc1c6e10..37cb3e33 100644 --- a/DependencyInjection/SerializerPass.php +++ b/DependencyInjection/SerializerPass.php @@ -19,6 +19,8 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Debug\TraceableNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerInterface; /** @@ -152,6 +154,8 @@ private function configureNamedSerializers(ContainerBuilder $container): void $container->registerChild($serializerId, 'serializer'); $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer'); + $container->registerAliasForArgument($serializerId, NormalizerInterface::class, $serializerName.'.normalizer'); + $container->registerAliasForArgument($serializerId, DenormalizerInterface::class, $serializerName.'.denormalizer'); $this->configureSerializer($container, $serializerId, $normalizers, $encoders, $serializerName); diff --git a/Tests/DependencyInjection/SerializerPassTest.php b/Tests/DependencyInjection/SerializerPassTest.php index b721b1ba..3325b1ef 100644 --- a/Tests/DependencyInjection/SerializerPassTest.php +++ b/Tests/DependencyInjection/SerializerPassTest.php @@ -19,6 +19,8 @@ use Symfony\Component\Serializer\Debug\TraceableNormalizer; use Symfony\Component\Serializer\Debug\TraceableSerializer; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerInterface; /** @@ -591,6 +593,8 @@ public function testNamedSerializersAreRegistered() $this->assertTrue($container->hasAlias(\sprintf('%s $apiSerializer', SerializerInterface::class))); $this->assertTrue($container->hasDefinition('serializer.api2')); $this->assertTrue($container->hasAlias(\sprintf('%s $api2Serializer', SerializerInterface::class))); + $this->assertTrue($container->hasAlias(\sprintf('%s $api2Normalizer', NormalizerInterface::class))); + $this->assertTrue($container->hasAlias(\sprintf('%s $api2Denormalizer', DenormalizerInterface::class))); } public function testNormalizersAndEncodersAreDecoratedAndOrderedWhenCollectingDataForNamedSerializers() From 7cb8ab43b491e9031a7f8644588e502aff171a98 Mon Sep 17 00:00:00 2001 From: valtzu Date: Sat, 1 Feb 2025 18:47:33 +0200 Subject: [PATCH 10/19] Add `NumberNormalizer` --- CHANGELOG.md | 1 + Normalizer/NumberNormalizer.php | 79 ++++++++++++ Tests/Normalizer/NumberNormalizerTest.php | 150 ++++++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 Normalizer/NumberNormalizer.php create mode 100644 Tests/Normalizer/NumberNormalizerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a4fc951f..2286d584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes * Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers + * Add `NumberNormalizer` to normalize `BcMath\Number` and `GMP` as `string` 7.2 --- diff --git a/Normalizer/NumberNormalizer.php b/Normalizer/NumberNormalizer.php new file mode 100644 index 00000000..de68a406 --- /dev/null +++ b/Normalizer/NumberNormalizer.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use BcMath\Number; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; + +/** + * Normalizes {@see Number} and {@see \GMP} to a string. + */ +final class NumberNormalizer implements NormalizerInterface, DenormalizerInterface +{ + public function getSupportedTypes(?string $format): array + { + return [ + Number::class => true, + \GMP::class => true, + ]; + } + + public function normalize(mixed $data, ?string $format = null, array $context = []): string + { + if (!$data instanceof Number && !$data instanceof \GMP) { + throw new InvalidArgumentException(\sprintf('The data must be an instance of "%s" or "%s".', Number::class, \GMP::class)); + } + + return (string) $data; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Number || $data instanceof \GMP; + } + + /** + * @throws NotNormalizableValueException + */ + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): Number|\GMP + { + if (!\is_string($data) && !\is_int($data)) { + throw $this->createNotNormalizableValueException($type, $data, $context); + } + + try { + return match ($type) { + Number::class => new Number($data), + \GMP::class => new \GMP($data), + default => throw new InvalidArgumentException(\sprintf('Only "%s" and "%s" types are supported.', Number::class, \GMP::class)), + }; + } catch (\ValueError $e) { + throw $this->createNotNormalizableValueException($type, $data, $context, $e); + } + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return \in_array($type, [Number::class, \GMP::class], true) && null !== $data; + } + + private function createNotNormalizableValueException(string $type, mixed $data, array $context, ?\Throwable $previous = null): NotNormalizableValueException + { + $message = match ($type) { + Number::class => 'The data must be a "string" representing a decimal number, or an "int".', + \GMP::class => 'The data must be a "string" representing an integer, or an "int".', + }; + + return NotNormalizableValueException::createForUnexpectedDataType($message, $data, ['string', 'int'], $context['deserialization_path'] ?? null, true, 0, $previous); + } +} diff --git a/Tests/Normalizer/NumberNormalizerTest.php b/Tests/Normalizer/NumberNormalizerTest.php new file mode 100644 index 00000000..be97588d --- /dev/null +++ b/Tests/Normalizer/NumberNormalizerTest.php @@ -0,0 +1,150 @@ + + * + * 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; + +use BcMath\Number; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Normalizer\NumberNormalizer; + +/** + * @requires PHP 8.4 + * @requires extension bcmath + * @requires extension gmp + */ +class NumberNormalizerTest extends TestCase +{ + private NumberNormalizer $normalizer; + + protected function setUp(): void + { + $this->normalizer = new NumberNormalizer(); + } + + /** + * @dataProvider supportsNormalizationProvider + */ + public function testSupportsNormalization(mixed $data, bool $expected) + { + $this->assertSame($expected, $this->normalizer->supportsNormalization($data)); + } + + public static function supportsNormalizationProvider(): iterable + { + yield 'GMP object' => [new \GMP('0b111'), true]; + yield 'Number object' => [new Number('1.23'), true]; + yield 'object with similar properties as Number' => [(object) ['value' => '1.23', 'scale' => 2], false]; + yield 'stdClass' => [new \stdClass(), false]; + yield 'string' => ['1.23', false]; + yield 'float' => [1.23, false]; + yield 'null' => [null, false]; + } + + /** + * @dataProvider normalizeGoodValueProvider + */ + public function testNormalize(mixed $data, mixed $expected) + { + $this->assertSame($expected, $this->normalizer->normalize($data)); + } + + public static function normalizeGoodValueProvider(): iterable + { + yield 'Number with scale=2' => [new Number('1.23'), '1.23']; + yield 'Number with scale=0' => [new Number('1'), '1']; + yield 'Number with integer' => [new Number(123), '123']; + yield 'GMP hex' => [new \GMP('0x10'), '16']; + yield 'GMP base=10' => [new \GMP('10'), '10']; + } + + /** + * @dataProvider normalizeBadValueProvider + */ + public function testNormalizeBadValueThrows(mixed $data) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The data must be an instance of "BcMath\Number" or "GMP".'); + + $this->normalizer->normalize($data); + } + + public static function normalizeBadValueProvider(): iterable + { + yield 'stdClass' => [new \stdClass()]; + yield 'string' => ['1.23']; + yield 'null' => [null]; + } + + /** + * @dataProvider supportsDenormalizationProvider + */ + public function testSupportsDenormalization(mixed $data, string $type, bool $expected) + { + $this->assertSame($expected, $this->normalizer->supportsDenormalization($data, $type)); + } + + public static function supportsDenormalizationProvider(): iterable + { + yield 'null value, Number' => [null, Number::class, false]; + yield 'null value, GMP' => [null, \GMP::class, false]; + yield 'null value, unmatching type' => [null, \stdClass::class, false]; + } + + /** + * @dataProvider denormalizeGoodValueProvider + */ + public function testDenormalize(mixed $data, string $type, mixed $expected) + { + $this->assertEquals($expected, $this->normalizer->denormalize($data, $type)); + } + + public static function denormalizeGoodValueProvider(): iterable + { + yield 'Number, string with decimal point' => ['1.23', Number::class, new Number('1.23')]; + yield 'Number, integer as string' => ['123', Number::class, new Number('123')]; + yield 'Number, integer' => [123, Number::class, new Number('123')]; + yield 'GMP, large number' => ['9223372036854775808', \GMP::class, new \GMP('9223372036854775808')]; + yield 'GMP, integer' => [123, \GMP::class, new \GMP('123')]; + } + + /** + * @dataProvider denormalizeBadValueProvider + */ + public function testDenormalizeBadValueThrows(mixed $data, string $type, string $expectedException, string $expectedExceptionMessage) + { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->normalizer->denormalize($data, $type); + } + + public static function denormalizeBadValueProvider(): iterable + { + $stringOrDecimalExpectedMessage = 'The data must be a "string" representing a decimal number, or an "int".'; + yield 'Number, null' => [null, Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + yield 'Number, boolean' => [true, Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + yield 'Number, object' => [new \stdClass(), Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + yield 'Number, non-numeric string' => ['foobar', Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + yield 'Number, float' => [1.23, Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + + $stringOrIntExpectedMessage = 'The data must be a "string" representing an integer, or an "int".'; + yield 'GMP, null' => [null, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, boolean' => [true, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, object' => [new \stdClass(), \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, non-numeric string' => ['foobar', \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, scale > 0' => ['1.23', \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + yield 'GMP, float' => [1.23, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + + yield 'unsupported type' => ['1.23', \stdClass::class, InvalidArgumentException::class, 'Only "BcMath\Number" and "GMP" types are supported.']; + } +} From 49c27cafe95787c0e61e21d3af8e3472ca6bb336 Mon Sep 17 00:00:00 2001 From: Kevin Jansen Date: Mon, 13 Jan 2025 19:11:38 +0100 Subject: [PATCH 11/19] Correct all implementations of the NormalizerInterface to have the correct method signature This commit fixes #59495 Rename test method name Remove void return type from test Remove redundant docblock --- Debug/TraceableNormalizer.php | 4 +-- Debug/TraceableSerializer.php | 6 ++-- Normalizer/AbstractObjectNormalizer.php | 34 +++++++++---------- Normalizer/BackedEnumNormalizer.php | 6 ++-- .../ConstraintViolationListNormalizer.php | 4 +-- Normalizer/CustomNormalizer.php | 4 +-- Normalizer/DataUriNormalizer.php | 16 ++++----- Normalizer/DateIntervalNormalizer.php | 6 ++-- Normalizer/DateTimeNormalizer.php | 14 ++++---- Normalizer/DateTimeZoneNormalizer.php | 6 ++-- Normalizer/FormErrorNormalizer.php | 12 +++---- Normalizer/JsonSerializableNormalizer.php | 10 +++--- Normalizer/MimeMessageNormalizer.php | 14 ++++---- Normalizer/ProblemNormalizer.php | 24 ++++++------- Normalizer/TranslatableNormalizer.php | 8 ++--- Normalizer/UidNormalizer.php | 13 +++---- Tests/Debug/TraceableSerializerTest.php | 2 +- Tests/Fixtures/AbstractNormalizerDummy.php | 2 +- Tests/Fixtures/EnvelopeNormalizer.php | 4 +-- Tests/Fixtures/EnvelopedMessageNormalizer.php | 4 +-- Tests/Normalizer/ObjectNormalizerTest.php | 3 ++ Tests/Normalizer/TestNormalizer.php | 2 +- 22 files changed, 99 insertions(+), 99 deletions(-) diff --git a/Debug/TraceableNormalizer.php b/Debug/TraceableNormalizer.php index 1b143e29..d737f5b3 100644 --- a/Debug/TraceableNormalizer.php +++ b/Debug/TraceableNormalizer.php @@ -40,14 +40,14 @@ public function getSupportedTypes(?string $format): array return $this->normalizer->getSupportedTypes($format); } - 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 { 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)); } $startTime = microtime(true); - $normalized = $this->normalizer->normalize($object, $format, $context); + $normalized = $this->normalizer->normalize($data, $format, $context); $time = microtime(true) - $startTime; if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) { diff --git a/Debug/TraceableSerializer.php b/Debug/TraceableSerializer.php index a05bf4bf..bd4f505f 100644 --- a/Debug/TraceableSerializer.php +++ b/Debug/TraceableSerializer.php @@ -66,17 +66,17 @@ public function deserialize(mixed $data, string $type, string $format, array $co return $result; } - 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 { $context[self::DEBUG_TRACE_ID] = $traceId = bin2hex(random_bytes(4)); $startTime = microtime(true); - $result = $this->serializer->normalize($object, $format, $context); + $result = $this->serializer->normalize($data, $format, $context); $time = microtime(true) - $startTime; $caller = $this->getCaller(__FUNCTION__, NormalizerInterface::class); - $this->dataCollector->collectNormalize($traceId, $object, $format, $context, $time, $caller, $this->serializerName); + $this->dataCollector->collectNormalize($traceId, $data, $format, $context, $time, $caller, $this->serializerName); return $result; } diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index fb45a924..32d1c906 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -154,7 +154,7 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return \is_object($data) && !$data instanceof \Traversable; } - 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 { $context['_read_attributes'] = true; @@ -164,14 +164,14 @@ public function normalize(mixed $object, ?string $format = null, array $context $this->validateCallbackContext($context); - if ($this->isCircularReference($object, $context)) { - return $this->handleCircularReference($object, $format, $context); + if ($this->isCircularReference($data, $context)) { + return $this->handleCircularReference($data, $format, $context); } - $data = []; + $normalizedData = []; $stack = []; - $attributes = $this->getAttributes($object, $format, $context); - $class = ($this->objectClassResolver)($object); + $attributes = $this->getAttributes($data, $format, $context); + $class = ($this->objectClassResolver)($data); $classMetadata = $this->classMetadataFactory?->getMetadataFor($class); $attributesMetadata = $this->classMetadataFactory?->getMetadataFor($class)->getAttributesMetadata(); if (isset($context[self::MAX_DEPTH_HANDLER])) { @@ -189,12 +189,12 @@ public function normalize(mixed $object, ?string $format = null, array $context continue; } - $attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context); + $attributeContext = $this->getAttributeNormalizationContext($data, $attribute, $context); try { - $attributeValue = $attribute === $this->classDiscriminatorResolver?->getMappingForMappedObject($object)?->getTypeProperty() - ? $this->classDiscriminatorResolver?->getTypeForMappedObject($object) - : $this->getAttributeValue($object, $attribute, $format, $attributeContext); + $attributeValue = $attribute === $this->classDiscriminatorResolver?->getMappingForMappedObject($data)?->getTypeProperty() + ? $this->classDiscriminatorResolver?->getTypeForMappedObject($data) + : $this->getAttributeValue($data, $attribute, $format, $attributeContext); } catch (UninitializedPropertyException|\Error $e) { if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e)) { continue; @@ -203,17 +203,17 @@ public function normalize(mixed $object, ?string $format = null, array $context } if ($maxDepthReached) { - $attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $attributeContext); + $attributeValue = $maxDepthHandler($attributeValue, $data, $attribute, $format, $attributeContext); } - $stack[$attribute] = $this->applyCallbacks($attributeValue, $object, $attribute, $format, $attributeContext); + $stack[$attribute] = $this->applyCallbacks($attributeValue, $data, $attribute, $format, $attributeContext); } foreach ($stack as $attribute => $attributeValue) { - $attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context); + $attributeContext = $this->getAttributeNormalizationContext($data, $attribute, $context); if (null === $attributeValue || \is_scalar($attributeValue)) { - $data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata); + $normalizedData = $this->updateData($normalizedData, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata); continue; } @@ -223,15 +223,15 @@ public function normalize(mixed $object, ?string $format = null, array $context $childContext = $this->createChildContext($attributeContext, $attribute, $format); - $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $childContext), $class, $format, $attributeContext, $attributesMetadata, $classMetadata); + $normalizedData = $this->updateData($normalizedData, $attribute, $this->serializer->normalize($attributeValue, $format, $childContext), $class, $format, $attributeContext, $attributesMetadata, $classMetadata); } $preserveEmptyObjects = $context[self::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[self::PRESERVE_EMPTY_OBJECTS] ?? false; - if ($preserveEmptyObjects && !$data) { + if ($preserveEmptyObjects && !$normalizedData) { return new \ArrayObject(); } - return $data; + return $normalizedData; } protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, ?string $format = null): object diff --git a/Normalizer/BackedEnumNormalizer.php b/Normalizer/BackedEnumNormalizer.php index 3d8e7e7c..7d3659c8 100644 --- a/Normalizer/BackedEnumNormalizer.php +++ b/Normalizer/BackedEnumNormalizer.php @@ -33,13 +33,13 @@ public function getSupportedTypes(?string $format): array ]; } - public function normalize(mixed $object, ?string $format = null, array $context = []): int|string + public function normalize(mixed $data, ?string $format = null, array $context = []): int|string { - if (!$object instanceof \BackedEnum) { + if (!$data instanceof \BackedEnum) { throw new InvalidArgumentException('The data must belong to a backed enumeration.'); } - return $object->value; + return $data->value; } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/ConstraintViolationListNormalizer.php b/Normalizer/ConstraintViolationListNormalizer.php index eda3b758..92e03638 100644 --- a/Normalizer/ConstraintViolationListNormalizer.php +++ b/Normalizer/ConstraintViolationListNormalizer.php @@ -43,7 +43,7 @@ public function getSupportedTypes(?string $format): array ]; } - public function normalize(mixed $object, ?string $format = null, array $context = []): array + public function normalize(mixed $data, ?string $format = null, array $context = []): array { if (\array_key_exists(self::PAYLOAD_FIELDS, $context)) { $payloadFieldsToSerialize = $context[self::PAYLOAD_FIELDS]; @@ -59,7 +59,7 @@ public function normalize(mixed $object, ?string $format = null, array $context $violations = []; $messages = []; - foreach ($object as $violation) { + foreach ($data as $violation) { $propertyPath = $this->nameConverter ? $this->nameConverter->normalize($violation->getPropertyPath(), null, $format, $context) : $violation->getPropertyPath(); $violationEntry = [ diff --git a/Normalizer/CustomNormalizer.php b/Normalizer/CustomNormalizer.php index d9710831..444c4135 100644 --- a/Normalizer/CustomNormalizer.php +++ b/Normalizer/CustomNormalizer.php @@ -30,9 +30,9 @@ public function getSupportedTypes(?string $format): array ]; } - 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 { - return $object->normalize($this->serializer, $format, $context); + return $data->normalize($this->serializer, $format, $context); } public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed diff --git a/Normalizer/DataUriNormalizer.php b/Normalizer/DataUriNormalizer.php index 5ee076be..57ad724c 100644 --- a/Normalizer/DataUriNormalizer.php +++ b/Normalizer/DataUriNormalizer.php @@ -47,27 +47,27 @@ public function getSupportedTypes(?string $format): array return self::SUPPORTED_TYPES; } - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $data, ?string $format = null, array $context = []): string { - if (!$object instanceof \SplFileInfo) { + if (!$data instanceof \SplFileInfo) { throw new InvalidArgumentException('The object must be an instance of "\SplFileInfo".'); } - $mimeType = $this->getMimeType($object); - $splFileObject = $this->extractSplFileObject($object); + $mimeType = $this->getMimeType($data); + $splFileObject = $this->extractSplFileObject($data); - $data = ''; + $splFileData = ''; $splFileObject->rewind(); while (!$splFileObject->eof()) { - $data .= $splFileObject->fgets(); + $splFileData .= $splFileObject->fgets(); } if ('text' === explode('/', $mimeType, 2)[0]) { - return \sprintf('data:%s,%s', $mimeType, rawurlencode($data)); + return \sprintf('data:%s,%s', $mimeType, rawurlencode($splFileData)); } - return \sprintf('data:%s;base64,%s', $mimeType, base64_encode($data)); + return \sprintf('data:%s;base64,%s', $mimeType, base64_encode($splFileData)); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/DateIntervalNormalizer.php b/Normalizer/DateIntervalNormalizer.php index 05d1a852..1ad81ec6 100644 --- a/Normalizer/DateIntervalNormalizer.php +++ b/Normalizer/DateIntervalNormalizer.php @@ -43,13 +43,13 @@ public function getSupportedTypes(?string $format): array /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $data, ?string $format = null, array $context = []): string { - if (!$object instanceof \DateInterval) { + if (!$data instanceof \DateInterval) { throw new InvalidArgumentException('The object must be an instance of "\DateInterval".'); } - return $object->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]); + return $data->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/DateTimeNormalizer.php b/Normalizer/DateTimeNormalizer.php index dfc498c1..a136ec22 100644 --- a/Normalizer/DateTimeNormalizer.php +++ b/Normalizer/DateTimeNormalizer.php @@ -56,9 +56,9 @@ public function getSupportedTypes(?string $format): array /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): int|float|string + public function normalize(mixed $data, ?string $format = null, array $context = []): int|float|string { - if (!$object instanceof \DateTimeInterface) { + if (!$data instanceof \DateTimeInterface) { throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".'); } @@ -66,14 +66,14 @@ public function normalize(mixed $object, ?string $format = null, array $context $timezone = $this->getTimezone($context); if (null !== $timezone) { - $object = clone $object; - $object = $object->setTimezone($timezone); + $data = clone $data; + $data = $data->setTimezone($timezone); } 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), + 'int' => (int) $data->format($dateTimeFormat), + 'float' => (float) $data->format($dateTimeFormat), + default => $data->format($dateTimeFormat), }; } diff --git a/Normalizer/DateTimeZoneNormalizer.php b/Normalizer/DateTimeZoneNormalizer.php index f4528a03..cdb6eb2f 100644 --- a/Normalizer/DateTimeZoneNormalizer.php +++ b/Normalizer/DateTimeZoneNormalizer.php @@ -31,13 +31,13 @@ public function getSupportedTypes(?string $format): array /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $data, ?string $format = null, array $context = []): string { - if (!$object instanceof \DateTimeZone) { + if (!$data instanceof \DateTimeZone) { throw new InvalidArgumentException('The object must be an instance of "\DateTimeZone".'); } - return $object->getName(); + return $data->getName(); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/FormErrorNormalizer.php b/Normalizer/FormErrorNormalizer.php index 9ef13a66..c91d7d52 100644 --- a/Normalizer/FormErrorNormalizer.php +++ b/Normalizer/FormErrorNormalizer.php @@ -22,20 +22,20 @@ final class FormErrorNormalizer implements NormalizerInterface public const TYPE = 'type'; public const CODE = 'status_code'; - public function normalize(mixed $object, ?string $format = null, array $context = []): array + public function normalize(mixed $data, ?string $format = null, array $context = []): array { - $data = [ + $error = [ 'title' => $context[self::TITLE] ?? 'Validation Failed', 'type' => $context[self::TYPE] ?? 'https://symfony.com/errors/form', 'code' => $context[self::CODE] ?? null, - 'errors' => $this->convertFormErrorsToArray($object), + 'errors' => $this->convertFormErrorsToArray($data), ]; - if (0 !== \count($object->all())) { - $data['children'] = $this->convertFormChildrenToArray($object); + if (0 !== \count($data->all())) { + $error['children'] = $this->convertFormChildrenToArray($data); } - return $data; + return $error; } public function getSupportedTypes(?string $format): array diff --git a/Normalizer/JsonSerializableNormalizer.php b/Normalizer/JsonSerializableNormalizer.php index 31c22417..3bc4f280 100644 --- a/Normalizer/JsonSerializableNormalizer.php +++ b/Normalizer/JsonSerializableNormalizer.php @@ -21,13 +21,13 @@ */ final class JsonSerializableNormalizer extends AbstractNormalizer { - 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 { - if ($this->isCircularReference($object, $context)) { - return $this->handleCircularReference($object, $format, $context); + if ($this->isCircularReference($data, $context)) { + return $this->handleCircularReference($data, $format, $context); } - if (!$object instanceof \JsonSerializable) { + if (!$data instanceof \JsonSerializable) { throw new InvalidArgumentException(\sprintf('The object must implement "%s".', \JsonSerializable::class)); } @@ -35,7 +35,7 @@ public function normalize(mixed $object, ?string $format = null, array $context throw new LogicException('Cannot normalize object because injected serializer is not a normalizer.'); } - return $this->serializer->normalize($object->jsonSerialize(), $format, $context); + return $this->serializer->normalize($data->jsonSerialize(), $format, $context); } public function getSupportedTypes(?string $format): array diff --git a/Normalizer/MimeMessageNormalizer.php b/Normalizer/MimeMessageNormalizer.php index 633edf36..5b121338 100644 --- a/Normalizer/MimeMessageNormalizer.php +++ b/Normalizer/MimeMessageNormalizer.php @@ -62,25 +62,25 @@ public function setSerializer(SerializerInterface $serializer): void $this->normalizer->setSerializer($serializer); } - 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 { - if ($object instanceof Headers) { + if ($data instanceof Headers) { $ret = []; - foreach ($this->headersProperty->getValue($object) as $name => $header) { + foreach ($this->headersProperty->getValue($data) as $name => $header) { $ret[$name] = $this->serializer->normalize($header, $format, $context); } return $ret; } - $ret = $this->normalizer->normalize($object, $format, $context); + $ret = $this->normalizer->normalize($data, $format, $context); - if ($object instanceof AbstractPart) { - $ret['class'] = $object::class; + if ($data instanceof AbstractPart) { + $ret['class'] = $data::class; unset($ret['seekable'], $ret['cid'], $ret['handle']); } - if ($object instanceof RawMessage && \array_key_exists('message', $ret) && null === $ret['message']) { + if ($data instanceof RawMessage && \array_key_exists('message', $ret) && null === $ret['message']) { unset($ret['message']); } diff --git a/Normalizer/ProblemNormalizer.php b/Normalizer/ProblemNormalizer.php index 08aca679..8f255e6c 100644 --- a/Normalizer/ProblemNormalizer.php +++ b/Normalizer/ProblemNormalizer.php @@ -57,7 +57,7 @@ public function normalize(mixed $object, ?string $format = null, array $context throw new InvalidArgumentException(\sprintf('The object must implement "%s".', FlattenException::class)); } - $data = []; + $error = []; $context += $this->defaultContext; $debug = $this->debug && ($context['debug'] ?? true); $exception = $context['exception'] ?? null; @@ -67,7 +67,7 @@ public function normalize(mixed $object, ?string $format = null, array $context if ($exception instanceof PartialDenormalizationException) { $trans = $this->translator ? $this->translator->trans(...) : fn ($m, $p) => strtr($m, $p); $template = 'This value should be of type {{ type }}.'; - $data = [ + $error = [ self::TYPE => 'https://symfony.com/errors/validation', self::TITLE => 'Validation Failed', 'violations' => array_map( @@ -84,27 +84,27 @@ public function normalize(mixed $object, ?string $format = null, array $context $exception->getErrors() ), ]; - $data['detail'] = implode("\n", array_map(fn ($e) => $e['propertyPath'].': '.$e['title'], $data['violations'])); + $error['detail'] = implode("\n", array_map(fn ($e) => $e['propertyPath'].': '.$e['title'], $error['violations'])); } elseif (($exception instanceof ValidationFailedException || $exception instanceof MessageValidationFailedException) && $this->serializer instanceof NormalizerInterface && $this->serializer->supportsNormalization($exception->getViolations(), $format, $context) ) { - $data = $this->serializer->normalize($exception->getViolations(), $format, $context); + $error = $this->serializer->normalize($exception->getViolations(), $format, $context); } } - $data = [ - self::TYPE => $data[self::TYPE] ?? $context[self::TYPE] ?? 'https://tools.ietf.org/html/rfc2616#section-10', - self::TITLE => $data[self::TITLE] ?? $context[self::TITLE] ?? 'An error occurred', + $error = [ + self::TYPE => $error[self::TYPE] ?? $context[self::TYPE] ?? 'https://tools.ietf.org/html/rfc2616#section-10', + self::TITLE => $error[self::TITLE] ?? $context[self::TITLE] ?? 'An error occurred', self::STATUS => $context[self::STATUS] ?? $object->getStatusCode(), - 'detail' => $data['detail'] ?? ($debug ? $object->getMessage() : $object->getStatusText()), - ] + $data; + 'detail' => $error['detail'] ?? ($debug ? $object->getMessage() : $object->getStatusText()), + ] + $error; if ($debug) { - $data['class'] = $object->getClass(); - $data['trace'] = $object->getTrace(); + $error['class'] = $object->getClass(); + $error['trace'] = $object->getTrace(); } - return $data; + return $error; } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/TranslatableNormalizer.php b/Normalizer/TranslatableNormalizer.php index 463616e7..d365598b 100644 --- a/Normalizer/TranslatableNormalizer.php +++ b/Normalizer/TranslatableNormalizer.php @@ -34,13 +34,13 @@ public function __construct( /** * @throws InvalidArgumentException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): string + public function normalize(mixed $data, ?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]); + if (!$data instanceof TranslatableInterface) { + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The object must implement the "%s".', TranslatableInterface::class), $data, [TranslatableInterface::class]); } - return $object->trans($this->translator, $context[self::NORMALIZATION_LOCALE_KEY] ?? $this->defaultContext[self::NORMALIZATION_LOCALE_KEY]); + return $data->trans($this->translator, $context[self::NORMALIZATION_LOCALE_KEY] ?? $this->defaultContext[self::NORMALIZATION_LOCALE_KEY]); } public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool diff --git a/Normalizer/UidNormalizer.php b/Normalizer/UidNormalizer.php index 0bee1f70..eb624eed 100644 --- a/Normalizer/UidNormalizer.php +++ b/Normalizer/UidNormalizer.php @@ -48,16 +48,13 @@ public function getSupportedTypes(?string $format): array ]; } - /** - * @param AbstractUid $object - */ - 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 { return match ($context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY]) { - self::NORMALIZATION_FORMAT_CANONICAL => (string) $object, - self::NORMALIZATION_FORMAT_BASE58 => $object->toBase58(), - self::NORMALIZATION_FORMAT_BASE32 => $object->toBase32(), - self::NORMALIZATION_FORMAT_RFC4122 => $object->toRfc4122(), + self::NORMALIZATION_FORMAT_CANONICAL => (string) $data, + self::NORMALIZATION_FORMAT_BASE58 => $data->toBase58(), + self::NORMALIZATION_FORMAT_BASE32 => $data->toBase32(), + self::NORMALIZATION_FORMAT_RFC4122 => $data->toRfc4122(), default => throw new LogicException(\sprintf('The "%s" format is not valid.', $context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY])), }; } diff --git a/Tests/Debug/TraceableSerializerTest.php b/Tests/Debug/TraceableSerializerTest.php index d697b270..90924332 100644 --- a/Tests/Debug/TraceableSerializerTest.php +++ b/Tests/Debug/TraceableSerializerTest.php @@ -142,7 +142,7 @@ public function deserialize(mixed $data, string $type, string $format, array $co return 'deserialized'; } - 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 { return 'normalized'; } diff --git a/Tests/Fixtures/AbstractNormalizerDummy.php b/Tests/Fixtures/AbstractNormalizerDummy.php index f5bf565a..3945a108 100644 --- a/Tests/Fixtures/AbstractNormalizerDummy.php +++ b/Tests/Fixtures/AbstractNormalizerDummy.php @@ -30,7 +30,7 @@ public function getAllowedAttributes(string|object $classOrObject, array $contex return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString); } - 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 { } diff --git a/Tests/Fixtures/EnvelopeNormalizer.php b/Tests/Fixtures/EnvelopeNormalizer.php index 4acfdf83..7d085ad9 100644 --- a/Tests/Fixtures/EnvelopeNormalizer.php +++ b/Tests/Fixtures/EnvelopeNormalizer.php @@ -20,9 +20,9 @@ class EnvelopeNormalizer implements NormalizerInterface { private $serializer; - public function normalize($envelope, ?string $format = null, array $context = []): array + public function normalize(mixed $data, ?string $format = null, array $context = []): array { - $xmlContent = $this->serializer->serialize($envelope->message, 'xml'); + $xmlContent = $this->serializer->serialize($data->message, 'xml'); $encodedContent = base64_encode($xmlContent); diff --git a/Tests/Fixtures/EnvelopedMessageNormalizer.php b/Tests/Fixtures/EnvelopedMessageNormalizer.php index 812dbf01..e907b00a 100644 --- a/Tests/Fixtures/EnvelopedMessageNormalizer.php +++ b/Tests/Fixtures/EnvelopedMessageNormalizer.php @@ -18,10 +18,10 @@ */ class EnvelopedMessageNormalizer implements NormalizerInterface { - public function normalize($message, ?string $format = null, array $context = []): array + public function normalize(mixed $data, ?string $format = null, array $context = []): array { return [ - 'text' => $message->text, + 'text' => $data->text, ]; } diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index d45586b4..231448d4 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -85,11 +85,14 @@ class ObjectNormalizerTest extends TestCase use TypeEnforcementTestTrait; private ObjectNormalizer $normalizer; + private NormalizerInterface $normalizerInterface; private SerializerInterface&NormalizerInterface&MockObject $serializer; protected function setUp(): void { $this->createNormalizer(); + + $this->normalizerInterface = $this->normalizer; } private function createNormalizer(array $defaultContext = [], ?ClassMetadataFactoryInterface $classMetadataFactory = null): void diff --git a/Tests/Normalizer/TestNormalizer.php b/Tests/Normalizer/TestNormalizer.php index 941fef42..6c6e9306 100644 --- a/Tests/Normalizer/TestNormalizer.php +++ b/Tests/Normalizer/TestNormalizer.php @@ -20,7 +20,7 @@ */ class TestNormalizer implements NormalizerInterface { - public function normalize($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 { return null; } From cc30c6ebfd297fc7ad43d2c0d4185c85e9d78c24 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 7 Feb 2025 14:08:25 +0100 Subject: [PATCH 12/19] clean up unused property --- Tests/Normalizer/ObjectNormalizerTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 231448d4..d45586b4 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -85,14 +85,11 @@ class ObjectNormalizerTest extends TestCase use TypeEnforcementTestTrait; private ObjectNormalizer $normalizer; - private NormalizerInterface $normalizerInterface; private SerializerInterface&NormalizerInterface&MockObject $serializer; protected function setUp(): void { $this->createNormalizer(); - - $this->normalizerInterface = $this->normalizer; } private function createNormalizer(array $defaultContext = [], ?ClassMetadataFactoryInterface $classMetadataFactory = null): void From 8833037a97c463436d43602d01b73edfdd67df94 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 7 Feb 2025 13:36:55 +0100 Subject: [PATCH 13/19] separate NumberNormalizer tests per PHP extension --- Tests/Normalizer/NumberNormalizerTest.php | 140 ++++++++++++++++------ 1 file changed, 106 insertions(+), 34 deletions(-) diff --git a/Tests/Normalizer/NumberNormalizerTest.php b/Tests/Normalizer/NumberNormalizerTest.php index be97588d..338f63ba 100644 --- a/Tests/Normalizer/NumberNormalizerTest.php +++ b/Tests/Normalizer/NumberNormalizerTest.php @@ -17,11 +17,6 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Normalizer\NumberNormalizer; -/** - * @requires PHP 8.4 - * @requires extension bcmath - * @requires extension gmp - */ class NumberNormalizerTest extends TestCase { private NumberNormalizer $normalizer; @@ -41,8 +36,14 @@ public function testSupportsNormalization(mixed $data, bool $expected) public static function supportsNormalizationProvider(): iterable { - yield 'GMP object' => [new \GMP('0b111'), true]; - yield 'Number object' => [new Number('1.23'), true]; + if (class_exists(\GMP::class)) { + yield 'GMP object' => [new \GMP('0b111'), true]; + } + + if (class_exists(Number::class)) { + yield 'Number object' => [new Number('1.23'), true]; + } + yield 'object with similar properties as Number' => [(object) ['value' => '1.23', 'scale' => 2], false]; yield 'stdClass' => [new \stdClass(), false]; yield 'string' => ['1.23', false]; @@ -51,20 +52,40 @@ public static function supportsNormalizationProvider(): iterable } /** - * @dataProvider normalizeGoodValueProvider + * @requires extension bcmath + * + * @dataProvider normalizeGoodBcMathNumberValueProvider */ - public function testNormalize(mixed $data, mixed $expected) + public function testNormalizeBcMathNumber(Number $data, string $expected) { $this->assertSame($expected, $this->normalizer->normalize($data)); } - public static function normalizeGoodValueProvider(): iterable + public static function normalizeGoodBcMathNumberValueProvider(): iterable { - yield 'Number with scale=2' => [new Number('1.23'), '1.23']; - yield 'Number with scale=0' => [new Number('1'), '1']; - yield 'Number with integer' => [new Number(123), '123']; - yield 'GMP hex' => [new \GMP('0x10'), '16']; - yield 'GMP base=10' => [new \GMP('10'), '10']; + if (class_exists(Number::class)) { + yield 'Number with scale=2' => [new Number('1.23'), '1.23']; + yield 'Number with scale=0' => [new Number('1'), '1']; + yield 'Number with integer' => [new Number(123), '123']; + } + } + + /** + * @requires extension gmp + * + * @dataProvider normalizeGoodGmpValueProvider + */ + public function testNormalizeGmp(\GMP $data, string $expected) + { + $this->assertSame($expected, $this->normalizer->normalize($data)); + } + + public static function normalizeGoodGmpValueProvider(): iterable + { + if (class_exists(\GMP::class)) { + yield 'GMP hex' => [new \GMP('0x10'), '16']; + yield 'GMP base=10' => [new \GMP('10'), '10']; + } } /** @@ -86,41 +107,70 @@ public static function normalizeBadValueProvider(): iterable } /** - * @dataProvider supportsDenormalizationProvider + * @requires PHP 8.4 + * @requires extension bcmath + */ + public function testSupportsBcMathNumberDenormalization() + { + $this->assertFalse($this->normalizer->supportsDenormalization(null, Number::class)); + } + + /** + * @requires extension gmp */ - public function testSupportsDenormalization(mixed $data, string $type, bool $expected) + public function testSupportsGmpDenormalization() { - $this->assertSame($expected, $this->normalizer->supportsDenormalization($data, $type)); + $this->assertFalse($this->normalizer->supportsDenormalization(null, \GMP::class)); } - public static function supportsDenormalizationProvider(): iterable + public function testDoesNotSupportOtherValuesDenormalization() { - yield 'null value, Number' => [null, Number::class, false]; - yield 'null value, GMP' => [null, \GMP::class, false]; - yield 'null value, unmatching type' => [null, \stdClass::class, false]; + $this->assertFalse($this->normalizer->supportsDenormalization(null, \stdClass::class)); } /** - * @dataProvider denormalizeGoodValueProvider + * @requires PHP 8.4 + * @requires extension bcmath + * + * @dataProvider denormalizeGoodBcMathNumberValueProvider */ - public function testDenormalize(mixed $data, string $type, mixed $expected) + public function testDenormalizeBcMathNumber(string|int $data, string $type, Number $expected) { $this->assertEquals($expected, $this->normalizer->denormalize($data, $type)); } - public static function denormalizeGoodValueProvider(): iterable + public static function denormalizeGoodBcMathNumberValueProvider(): iterable { - yield 'Number, string with decimal point' => ['1.23', Number::class, new Number('1.23')]; - yield 'Number, integer as string' => ['123', Number::class, new Number('123')]; - yield 'Number, integer' => [123, Number::class, new Number('123')]; - yield 'GMP, large number' => ['9223372036854775808', \GMP::class, new \GMP('9223372036854775808')]; - yield 'GMP, integer' => [123, \GMP::class, new \GMP('123')]; + if (class_exists(Number::class)) { + yield 'Number, string with decimal point' => ['1.23', Number::class, new Number('1.23')]; + yield 'Number, integer as string' => ['123', Number::class, new Number('123')]; + yield 'Number, integer' => [123, Number::class, new Number('123')]; + } } /** - * @dataProvider denormalizeBadValueProvider + * @dataProvider denormalizeGoodGmpValueProvider */ - public function testDenormalizeBadValueThrows(mixed $data, string $type, string $expectedException, string $expectedExceptionMessage) + public function testDenormalizeGmp(string|int $data, string $type, \GMP $expected) + { + $this->assertEquals($expected, $this->normalizer->denormalize($data, $type)); + } + + public static function denormalizeGoodGmpValueProvider(): iterable + { + if (class_exists(\GMP::class)) { + yield 'GMP, large number' => ['9223372036854775808', \GMP::class, new \GMP('9223372036854775808')]; + yield 'GMP, integer' => [123, \GMP::class, new \GMP('123')]; + } + } + + /** + * @requires PHP 8.4 + * @requires extension bcmath + * + * @dataProvider denormalizeBadBcMathNumberValueProvider + */ + public function testDenormalizeBadBcMathNumberValueThrows(mixed $data, string $type, string $expectedException, string $expectedExceptionMessage) { $this->expectException($expectedException); $this->expectExceptionMessage($expectedExceptionMessage); @@ -128,7 +178,7 @@ public function testDenormalizeBadValueThrows(mixed $data, string $type, string $this->normalizer->denormalize($data, $type); } - public static function denormalizeBadValueProvider(): iterable + public static function denormalizeBadBcMathNumberValueProvider(): iterable { $stringOrDecimalExpectedMessage = 'The data must be a "string" representing a decimal number, or an "int".'; yield 'Number, null' => [null, Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; @@ -136,7 +186,23 @@ public static function denormalizeBadValueProvider(): iterable yield 'Number, object' => [new \stdClass(), Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; yield 'Number, non-numeric string' => ['foobar', Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; yield 'Number, float' => [1.23, Number::class, NotNormalizableValueException::class, $stringOrDecimalExpectedMessage]; + } + + /** + * @requires extension gmp + * + * @dataProvider denormalizeBadGmpValueProvider + */ + public function testDenormalizeBadGmpValueThrows(mixed $data, string $type, string $expectedException, string $expectedExceptionMessage) + { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->normalizer->denormalize($data, $type); + } + public static function denormalizeBadGmpValueProvider(): iterable + { $stringOrIntExpectedMessage = 'The data must be a "string" representing an integer, or an "int".'; yield 'GMP, null' => [null, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; yield 'GMP, boolean' => [true, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; @@ -144,7 +210,13 @@ public static function denormalizeBadValueProvider(): iterable yield 'GMP, non-numeric string' => ['foobar', \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; yield 'GMP, scale > 0' => ['1.23', \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; yield 'GMP, float' => [1.23, \GMP::class, NotNormalizableValueException::class, $stringOrIntExpectedMessage]; + } + + public function testDenormalizeBadValueThrows() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only "BcMath\Number" and "GMP" types are supported.'); - yield 'unsupported type' => ['1.23', \stdClass::class, InvalidArgumentException::class, 'Only "BcMath\Number" and "GMP" types are supported.']; + $this->normalizer->denormalize('1.23', \stdClass::class); } } From 509a6e8da5ceb19843744ae857ad957bf3714300 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Tue, 8 Oct 2024 09:59:42 +0200 Subject: [PATCH 14/19] [Serializer] Fix deserializing XML Attributes into string properties --- Normalizer/AbstractObjectNormalizer.php | 24 +++++++++++++++++++ .../SerializedNameAttributeDummy.php | 11 +++++++++ Tests/SerializerTest.php | 14 +++++++++++ 3 files changed, 49 insertions(+) create mode 100644 Tests/Fixtures/Attributes/SerializedNameAttributeDummy.php diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 5f2a8302..3570493e 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -512,6 +512,18 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass } } + if (is_numeric($data) && XmlEncoder::FORMAT === $format) { + // encoder parsed them wrong, so they might need to be transformed back + switch ($builtinType) { + case LegacyType::BUILTIN_TYPE_STRING: + return (string) $data; + case LegacyType::BUILTIN_TYPE_FLOAT: + return (float) $data; + case LegacyType::BUILTIN_TYPE_INT: + return (int) $data; + } + } + if (null !== $collectionValueType && LegacyType::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { $builtinType = LegacyType::BUILTIN_TYPE_OBJECT; $class = $collectionValueType->getClassName().'[]'; @@ -748,6 +760,18 @@ private function validateAndDenormalize(Type $type, string $currentClass, string } } + if (is_numeric($data) && XmlEncoder::FORMAT === $format) { + // encoder parsed them wrong, so they might need to be transformed back + switch ($typeIdentifier) { + case TypeIdentifier::STRING: + return (string) $data; + case TypeIdentifier::FLOAT: + return (float) $data; + case TypeIdentifier::INT: + return (int) $data; + } + } + if ($collectionValueType) { try { $collectionValueBaseType = $collectionValueType; diff --git a/Tests/Fixtures/Attributes/SerializedNameAttributeDummy.php b/Tests/Fixtures/Attributes/SerializedNameAttributeDummy.php new file mode 100644 index 00000000..968ea2bb --- /dev/null +++ b/Tests/Fixtures/Attributes/SerializedNameAttributeDummy.php @@ -0,0 +1,11 @@ +assertNull($obj->value); } + public function testDeserializeIntAsStringPropertyInXML() + { + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $nameConverter = new MetadataAwareNameConverter($classMetadataFactory); + $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); + $serializer = new Serializer([new ObjectNormalizer($classMetadataFactory, $nameConverter, null, $extractor)], ['xml' => new XmlEncoder()]); + + $obj = $serializer->deserialize('', SerializedNameAttributeDummy::class, 'xml'); + + $this->assertSame('123', $obj->foo); + } + public function testUnionTypeDeserializable() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); From 8064a269c4b373754537e028424e294b7ac22e67 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Fri, 21 Feb 2025 12:25:01 +0100 Subject: [PATCH 15/19] [Serializer] Add defaultType to DiscriminatorMap --- Attribute/DiscriminatorMap.php | 11 ++++++ CHANGELOG.md | 1 + Mapping/ClassDiscriminatorMapping.php | 6 ++++ .../Factory/ClassMetadataFactoryCompiler.php | 1 + Mapping/Loader/AttributeLoader.php | 2 +- Mapping/Loader/XmlFileLoader.php | 3 +- Mapping/Loader/YamlFileLoader.php | 3 +- .../serializer-mapping-1.0.xsd | 1 + Normalizer/AbstractObjectNormalizer.php | 2 +- Tests/Attribute/DiscriminatorMapTest.php | 9 ++++- Tests/Fixtures/Attributes/AbstractDummy.php | 2 +- Tests/Fixtures/serialization.xml | 2 +- Tests/Fixtures/serialization.yml | 1 + .../ClassMetadataFactoryCompilerTest.php | 24 ++++++++++++- Tests/Mapping/Loader/AttributeLoaderTest.php | 2 +- Tests/Mapping/Loader/XmlFileLoaderTest.php | 2 +- Tests/Mapping/Loader/YamlFileLoaderTest.php | 2 +- .../AbstractObjectNormalizerTest.php | 35 +++++++++++++++++++ 18 files changed, 98 insertions(+), 11 deletions(-) diff --git a/Attribute/DiscriminatorMap.php b/Attribute/DiscriminatorMap.php index 48d0842a..a61f3267 100644 --- a/Attribute/DiscriminatorMap.php +++ b/Attribute/DiscriminatorMap.php @@ -22,12 +22,14 @@ 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]) + * @param ?string $defaultType The fallback value if nothing specified by $typeProperty * * @throws InvalidArgumentException */ public function __construct( private readonly string $typeProperty, private readonly array $mapping, + private readonly ?string $defaultType = null, ) { if (!$typeProperty) { throw new InvalidArgumentException(\sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class)); @@ -36,6 +38,10 @@ public function __construct( if (!$mapping) { throw new InvalidArgumentException(\sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class)); } + + if (null !== $this->defaultType && !\array_key_exists($this->defaultType, $this->mapping)) { + throw new InvalidArgumentException(\sprintf('Default type "%s" given to "%s" must be present in "mapping" types.', $this->defaultType, static::class)); + } } public function getTypeProperty(): string @@ -47,6 +53,11 @@ public function getMapping(): array { return $this->mapping; } + + public function getDefaultType(): ?string + { + return $this->defaultType; + } } if (!class_exists(\Symfony\Component\Serializer\Annotation\DiscriminatorMap::class, false)) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 2286d584..1b5c95cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes * Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers * Add `NumberNormalizer` to normalize `BcMath\Number` and `GMP` as `string` + * Add `defaultType` to `DiscriminatorMap` 7.2 --- diff --git a/Mapping/ClassDiscriminatorMapping.php b/Mapping/ClassDiscriminatorMapping.php index 260575a4..985ea1ce 100644 --- a/Mapping/ClassDiscriminatorMapping.php +++ b/Mapping/ClassDiscriminatorMapping.php @@ -22,6 +22,7 @@ class ClassDiscriminatorMapping public function __construct( private readonly string $typeProperty, private array $typesMapping = [], + private readonly ?string $defaultType = null, ) { uasort($this->typesMapping, static function (string $a, string $b): int { if (is_a($a, $b, true)) { @@ -61,4 +62,9 @@ public function getTypesMapping(): array { return $this->typesMapping; } + + public function getDefaultType(): ?string + { + return $this->defaultType; + } } diff --git a/Mapping/Factory/ClassMetadataFactoryCompiler.php b/Mapping/Factory/ClassMetadataFactoryCompiler.php index 1e9202b7..7ec3e0ac 100644 --- a/Mapping/Factory/ClassMetadataFactoryCompiler.php +++ b/Mapping/Factory/ClassMetadataFactoryCompiler.php @@ -55,6 +55,7 @@ private function generateDeclaredClassMetadata(array $classMetadatas): string $classDiscriminatorMapping = $classMetadata->getClassDiscriminatorMapping() ? [ $classMetadata->getClassDiscriminatorMapping()->getTypeProperty(), $classMetadata->getClassDiscriminatorMapping()->getTypesMapping(), + $classMetadata->getClassDiscriminatorMapping()->getDefaultType(), ] : null; $compiled .= \sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([ diff --git a/Mapping/Loader/AttributeLoader.php b/Mapping/Loader/AttributeLoader.php index 13c59d1c..bf8ab356 100644 --- a/Mapping/Loader/AttributeLoader.php +++ b/Mapping/Loader/AttributeLoader.php @@ -59,7 +59,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool foreach ($this->loadAttributes($reflectionClass) as $attribute) { match (true) { - $attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping())), + $attribute instanceof DiscriminatorMap => $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping($attribute->getTypeProperty(), $attribute->getMapping(), $attribute->getDefaultType())), $attribute instanceof Groups => $classGroups = $attribute->getGroups(), $attribute instanceof Context => $classContextAttribute = $attribute, default => null, diff --git a/Mapping/Loader/XmlFileLoader.php b/Mapping/Loader/XmlFileLoader.php index 44ba89df..ac6fee2d 100644 --- a/Mapping/Loader/XmlFileLoader.php +++ b/Mapping/Loader/XmlFileLoader.php @@ -107,7 +107,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( (string) $xml->{'discriminator-map'}->attributes()->{'type-property'}, - $mapping + $mapping, + $xml->{'discriminator-map'}->attributes()->{'default-type'} ?? null )); } diff --git a/Mapping/Loader/YamlFileLoader.php b/Mapping/Loader/YamlFileLoader.php index ca71cbcb..898ae9f1 100644 --- a/Mapping/Loader/YamlFileLoader.php +++ b/Mapping/Loader/YamlFileLoader.php @@ -133,7 +133,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( $yaml['discriminator_map']['type_property'], - $yaml['discriminator_map']['mapping'] + $yaml['discriminator_map']['mapping'], + $yaml['discriminator_map']['default_type'] ?? null )); } diff --git a/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd b/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd index f5f6cca9..06c6ddfb 100644 --- a/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd +++ b/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd @@ -47,6 +47,7 @@ + diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 3570493e..c346aafa 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -1179,7 +1179,7 @@ private function getMappedClass(array $data, string $class, array $context): str return $class; } - if (null === $type = $data[$mapping->getTypeProperty()] ?? null) { + if (null === $type = $data[$mapping->getTypeProperty()] ?? $mapping->getDefaultType()) { 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); } diff --git a/Tests/Attribute/DiscriminatorMapTest.php b/Tests/Attribute/DiscriminatorMapTest.php index 497bc620..39da4019 100644 --- a/Tests/Attribute/DiscriminatorMapTest.php +++ b/Tests/Attribute/DiscriminatorMapTest.php @@ -40,9 +40,16 @@ public function testExceptionWithEmptyTypeProperty() new DiscriminatorMap(typeProperty: '', mapping: ['foo' => 'FooClass']); } - public function testExceptionWitEmptyMappingProperty() + public function testExceptionWithEmptyMappingProperty() { $this->expectException(InvalidArgumentException::class); new DiscriminatorMap(typeProperty: 'type', mapping: []); } + + public function testExceptionWithMissingDefaultTypeInMapping() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Default type "bar" given to "%s" must be present in "mapping" types.', DiscriminatorMap::class)); + new DiscriminatorMap(typeProperty: 'type', mapping: ['foo' => 'FooClass'], defaultType: 'bar'); + } } diff --git a/Tests/Fixtures/Attributes/AbstractDummy.php b/Tests/Fixtures/Attributes/AbstractDummy.php index a8c15fcc..f85874c1 100644 --- a/Tests/Fixtures/Attributes/AbstractDummy.php +++ b/Tests/Fixtures/Attributes/AbstractDummy.php @@ -17,7 +17,7 @@ 'first' => AbstractDummyFirstChild::class, 'second' => AbstractDummySecondChild::class, 'third' => AbstractDummyThirdChild::class, -])] +], defaultType: 'third')] abstract class AbstractDummy { public $foo; diff --git a/Tests/Fixtures/serialization.xml b/Tests/Fixtures/serialization.xml index 512736db..a03c546e 100644 --- a/Tests/Fixtures/serialization.xml +++ b/Tests/Fixtures/serialization.xml @@ -35,7 +35,7 @@ - + diff --git a/Tests/Fixtures/serialization.yml b/Tests/Fixtures/serialization.yml index 4371016e..ef612924 100644 --- a/Tests/Fixtures/serialization.yml +++ b/Tests/Fixtures/serialization.yml @@ -32,6 +32,7 @@ mapping: first: 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild' second: 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild' + default_type: first attributes: foo: ~ 'Symfony\Component\Serializer\Tests\Fixtures\Attributes\IgnoreDummy': diff --git a/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php b/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php index 40dcb501..aec9bcc9 100644 --- a/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php +++ b/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php @@ -15,6 +15,10 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild; +use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyThirdChild; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedPathDummy; @@ -40,6 +44,7 @@ public function testItDumpMetadata() $classMetatadataFactory = new ClassMetadataFactory(new AttributeLoader()); $dummyMetadata = $classMetatadataFactory->getMetadataFor(Dummy::class); + $abstractDummyMetadata = $classMetatadataFactory->getMetadataFor(AbstractDummy::class); $maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class); $serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class); $serializedPathDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathDummy::class); @@ -47,6 +52,7 @@ public function testItDumpMetadata() $code = (new ClassMetadataFactoryCompiler())->compile([ $dummyMetadata, + $abstractDummyMetadata, $maxDepthDummyMetadata, $serializedNameDummyMetadata, $serializedPathDummyMetadata, @@ -56,7 +62,7 @@ public function testItDumpMetadata() file_put_contents($this->dumpPath, $code); $compiledMetadata = require $this->dumpPath; - $this->assertCount(5, $compiledMetadata); + $this->assertCount(6, $compiledMetadata); $this->assertArrayHasKey(Dummy::class, $compiledMetadata); $this->assertEquals([ @@ -69,6 +75,22 @@ public function testItDumpMetadata() null, ], $compiledMetadata[Dummy::class]); + $this->assertArrayHasKey(AbstractDummy::class, $compiledMetadata); + $this->assertEquals([ + [ + 'foo' => [[], null, null, null], + ], + [ + 'type', + [ + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + 'third' => AbstractDummyThirdChild::class, + ], + 'third', + ], + ], $compiledMetadata[AbstractDummy::class]); + $this->assertArrayHasKey(MaxDepthDummy::class, $compiledMetadata); $this->assertEquals([ [ diff --git a/Tests/Mapping/Loader/AttributeLoaderTest.php b/Tests/Mapping/Loader/AttributeLoaderTest.php index 2af244a6..16d64f25 100644 --- a/Tests/Mapping/Loader/AttributeLoaderTest.php +++ b/Tests/Mapping/Loader/AttributeLoaderTest.php @@ -85,7 +85,7 @@ public function testLoadDiscriminatorMap() 'first' => AbstractDummyFirstChild::class, 'second' => AbstractDummySecondChild::class, 'third' => AbstractDummyThirdChild::class, - ])); + ], 'third')); $expected->addAttributeMetadata(new AttributeMetadata('foo')); $expected->getReflectionClass(); diff --git a/Tests/Mapping/Loader/XmlFileLoaderTest.php b/Tests/Mapping/Loader/XmlFileLoaderTest.php index c0298129..45b5aeb1 100644 --- a/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -109,7 +109,7 @@ public function testLoadDiscriminatorMap() $expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', [ 'first' => AbstractDummyFirstChild::class, 'second' => AbstractDummySecondChild::class, - ])); + ], 'second')); $expected->addAttributeMetadata(new AttributeMetadata('foo')); diff --git a/Tests/Mapping/Loader/YamlFileLoaderTest.php b/Tests/Mapping/Loader/YamlFileLoaderTest.php index 48e95aec..4997f164 100644 --- a/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -126,7 +126,7 @@ public function testLoadDiscriminatorMap() $expected = new ClassMetadata(AbstractDummy::class, new ClassDiscriminatorMapping('type', [ 'first' => AbstractDummyFirstChild::class, 'second' => AbstractDummySecondChild::class, - ])); + ], 'first')); $expected->addAttributeMetadata(new AttributeMetadata('foo')); diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 270b65f3..8e70bcc3 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -560,6 +560,41 @@ public function hasMetadataFor($value): bool $this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux); } + public function testDenormalizeWithDiscriminatorMapUsesCorrectClassnameWithDefaultType() + { + $factory = new ClassMetadataFactory(new AttributeLoader()); + + $loaderMock = new class implements ClassMetadataFactoryInterface { + public function getMetadataFor($value): ClassMetadataInterface + { + if (AbstractDummy::class === $value) { + return new ClassMetadata( + AbstractDummy::class, + new ClassDiscriminatorMapping('type', [ + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + ], 'second') + ); + } + + throw new InvalidArgumentException(sprintf('"%s" is not handled.', $value)); + } + + public function hasMetadataFor($value): bool + { + return AbstractDummy::class === $value; + } + }; + + $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); + $normalizer = new AbstractObjectNormalizerDummy($factory, null, new PhpDocExtractor(), $discriminatorResolver); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); + $normalizedData = $normalizer->denormalize(['foo' => 'foo', 'baz' => 'baz', 'quux' => ['value' => 'quux']], AbstractDummy::class); + + $this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux); + } + public function testDenormalizeWithDiscriminatorMapAndObjectToPopulateUsesCorrectClassname() { $factory = new ClassMetadataFactory(new AttributeLoader()); From 757d63b4b27bbd0719f6bb22b9e80e766d3d6f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4dlich?= Date: Sun, 26 Nov 2023 23:11:50 +0100 Subject: [PATCH 16/19] [Serializer] Add discriminator map to debug commmand output --- Command/DebugCommand.php | 7 ++++ Tests/Command/DebugCommandTest.php | 36 +++++++++++++++++++ Tests/Dummy/DummyClassTwo.php | 16 +++++++++ .../Dummy/DummyClassWithDiscriminatorMap.php | 23 ++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 Tests/Dummy/DummyClassTwo.php create mode 100644 Tests/Dummy/DummyClassWithDiscriminatorMap.php diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index c85ee213..d2c40489 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -97,6 +97,9 @@ private function getAttributesData(ClassMetadataInterface $classMetadata): array { $data = []; + $mapping = $classMetadata->getClassDiscriminatorMapping(); + $typeProperty = $mapping?->getTypeProperty(); + foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) { $data[$attributeMetadata->getName()] = [ 'groups' => $attributeMetadata->getGroups(), @@ -107,6 +110,10 @@ private function getAttributesData(ClassMetadataInterface $classMetadata): array 'normalizationContexts' => $attributeMetadata->getNormalizationContexts(), 'denormalizationContexts' => $attributeMetadata->getDenormalizationContexts(), ]; + + if ($mapping && $typeProperty === $attributeMetadata->getName()) { + $data[$attributeMetadata->getName()]['discriminatorMap'] = $mapping->getTypesMapping(); + } } return $data; diff --git a/Tests/Command/DebugCommandTest.php b/Tests/Command/DebugCommandTest.php index 7bfdf93d..01caca0c 100644 --- a/Tests/Command/DebugCommandTest.php +++ b/Tests/Command/DebugCommandTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Tests\Dummy\DummyClassWithDiscriminatorMap; use Symfony\Component\Serializer\Tests\Dummy\DummyClassOne; /** @@ -79,6 +80,41 @@ public function testOutputWithClassArgument() ); } + public function testOutputWithDiscriminatorMapClass() + { + $command = new DebugCommand(new ClassMetadataFactory(new AttributeLoader())); + + $tester = new CommandTester($command); + $tester->execute(['class' => DummyClassWithDiscriminatorMap::class], ['decorated' => false]); + + $this->assertSame(<< [], | + | | "maxDepth" => null, | + | | "serializedName" => null, | + | | "serializedPath" => null, | + | | "ignore" => false, | + | | "normalizationContexts" => [], | + | | "denormalizationContexts" => [], | + | | "discriminatorMap" => [ | + | | "one" => "Symfony\Component\Serializer\Tests\Dummy\DummyClassOne", | + | | "two" => "Symfony\Component\Serializer\Tests\Dummy\DummyClassTwo" | + | | ] | + | | ] | + +----------+------------------------------------------------------------------------+ + + TXT, + $tester->getDisplay(true), + ); + } + public function testOutputWithInvalidClassArgument() { $serializer = $this->createMock(ClassMetadataFactoryInterface::class); diff --git a/Tests/Dummy/DummyClassTwo.php b/Tests/Dummy/DummyClassTwo.php new file mode 100644 index 00000000..8bb5311e --- /dev/null +++ b/Tests/Dummy/DummyClassTwo.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Dummy; + +class DummyClassTwo +{ +} diff --git a/Tests/Dummy/DummyClassWithDiscriminatorMap.php b/Tests/Dummy/DummyClassWithDiscriminatorMap.php new file mode 100644 index 00000000..50044bf2 --- /dev/null +++ b/Tests/Dummy/DummyClassWithDiscriminatorMap.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Dummy; + +use Symfony\Component\Serializer\Attribute\DiscriminatorMap; + +#[DiscriminatorMap(typeProperty: 'type', mapping: [ + 'one' => DummyClassOne::class, + 'two' => DummyClassTwo::class, +])] +class DummyClassWithDiscriminatorMap +{ + public string $type; +} From 0aa4b10f571ba82f1ac44d50588276a30850c942 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Mar 2025 16:03:52 +0100 Subject: [PATCH 17/19] replace assertEmpty() with stricter assertions --- Tests/Attribute/ContextTest.php | 12 ++++++------ Tests/DataCollector/SerializerDataCollectorTest.php | 12 ++++++------ Tests/DependencyInjection/SerializerPassTest.php | 2 +- Tests/Normalizer/ObjectNormalizerTest.php | 3 +-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Tests/Attribute/ContextTest.php b/Tests/Attribute/ContextTest.php index ff149696..e012f7bf 100644 --- a/Tests/Attribute/ContextTest.php +++ b/Tests/Attribute/ContextTest.php @@ -50,9 +50,9 @@ public function testAsFirstArg() $context = new Context(['foo' => 'bar']); self::assertSame(['foo' => 'bar'], $context->getContext()); - self::assertEmpty($context->getNormalizationContext()); - self::assertEmpty($context->getDenormalizationContext()); - self::assertEmpty($context->getGroups()); + self::assertSame([], $context->getNormalizationContext()); + self::assertSame([], $context->getDenormalizationContext()); + self::assertSame([], $context->getGroups()); } public function testAsContextArg() @@ -60,9 +60,9 @@ public function testAsContextArg() $context = new Context(context: ['foo' => 'bar']); self::assertSame(['foo' => 'bar'], $context->getContext()); - self::assertEmpty($context->getNormalizationContext()); - self::assertEmpty($context->getDenormalizationContext()); - self::assertEmpty($context->getGroups()); + self::assertSame([], $context->getNormalizationContext()); + self::assertSame([], $context->getDenormalizationContext()); + self::assertSame([], $context->getGroups()); } /** diff --git a/Tests/DataCollector/SerializerDataCollectorTest.php b/Tests/DataCollector/SerializerDataCollectorTest.php index 6a26565a..0ea4ad3a 100644 --- a/Tests/DataCollector/SerializerDataCollectorTest.php +++ b/Tests/DataCollector/SerializerDataCollectorTest.php @@ -373,17 +373,17 @@ public function testNamedSerializers() $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([], $collectedData['encode']); + $this->assertSame([], $collectedData['deserialize']); + $this->assertSame([], $collectedData['denormalize']); + $this->assertSame([], $collectedData['decode']); $this->assertSame(4, $dataCollector->getHandledCount('api')); $collectedData = $dataCollector->getData('api'); - $this->assertEmpty($collectedData['serialize']); - $this->assertEmpty($collectedData['normalize']); + $this->assertSame([], $collectedData['serialize']); + $this->assertSame([], $collectedData['normalize']); $this->assertSame('api', $collectedData['encode'][0]['name']); $this->assertSame('JsonEncoder', $collectedData['encode'][0]['encoder']['class']); diff --git a/Tests/DependencyInjection/SerializerPassTest.php b/Tests/DependencyInjection/SerializerPassTest.php index fe6cbb22..4b49acd0 100644 --- a/Tests/DependencyInjection/SerializerPassTest.php +++ b/Tests/DependencyInjection/SerializerPassTest.php @@ -567,7 +567,7 @@ public function testBindSerializerDefaultContextToNamedSerializers() $serializerPass = new SerializerPass(); $serializerPass->process($container); - $this->assertEmpty($definition->getBindings()); + $this->assertSame([], $definition->getBindings()); $bindings = $container->getDefinition('n1.api')->getBindings(); $this->assertArrayHasKey('array $defaultContext', $bindings); diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index d45586b4..439dce05 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -199,8 +199,7 @@ public function testDenormalizeEmptyXmlArray() 'xml' ); - $this->assertIsArray($obj->bar); - $this->assertEmpty($obj->bar); + $this->assertSame([], $obj->bar); } public function testDenormalizeWithObject() From 0d0bc59d07e77555bf367e67f559aa61356482ae Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sat, 8 Mar 2025 00:16:33 +0100 Subject: [PATCH 18/19] chore: PHP CS Fixer fixes --- Tests/Attribute/DiscriminatorMapTest.php | 2 +- Tests/Command/DebugCommandTest.php | 2 +- Tests/Normalizer/AbstractObjectNormalizerTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Attribute/DiscriminatorMapTest.php b/Tests/Attribute/DiscriminatorMapTest.php index 39da4019..e33fe7e0 100644 --- a/Tests/Attribute/DiscriminatorMapTest.php +++ b/Tests/Attribute/DiscriminatorMapTest.php @@ -49,7 +49,7 @@ public function testExceptionWithEmptyMappingProperty() public function testExceptionWithMissingDefaultTypeInMapping() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage(sprintf('Default type "bar" given to "%s" must be present in "mapping" types.', DiscriminatorMap::class)); + $this->expectExceptionMessage(\sprintf('Default type "bar" given to "%s" must be present in "mapping" types.', DiscriminatorMap::class)); new DiscriminatorMap(typeProperty: 'type', mapping: ['foo' => 'FooClass'], defaultType: 'bar'); } } diff --git a/Tests/Command/DebugCommandTest.php b/Tests/Command/DebugCommandTest.php index 01caca0c..ffba4f49 100644 --- a/Tests/Command/DebugCommandTest.php +++ b/Tests/Command/DebugCommandTest.php @@ -17,8 +17,8 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; -use Symfony\Component\Serializer\Tests\Dummy\DummyClassWithDiscriminatorMap; use Symfony\Component\Serializer\Tests\Dummy\DummyClassOne; +use Symfony\Component\Serializer\Tests\Dummy\DummyClassWithDiscriminatorMap; /** * @author Loïc Frémont diff --git a/Tests/Normalizer/AbstractObjectNormalizerTest.php b/Tests/Normalizer/AbstractObjectNormalizerTest.php index 8e70bcc3..7068b8c8 100644 --- a/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -577,7 +577,7 @@ public function getMetadataFor($value): ClassMetadataInterface ); } - throw new InvalidArgumentException(sprintf('"%s" is not handled.', $value)); + throw new InvalidArgumentException(\sprintf('"%s" is not handled.', $value)); } public function hasMetadataFor($value): bool From 74e0e5611da8be8df6d3a2fc29b4a89e6a0da730 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 8 Apr 2025 15:58:30 +0200 Subject: [PATCH 19/19] add PHP version and extension that are required to run tests --- Tests/Normalizer/NumberNormalizerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/Normalizer/NumberNormalizerTest.php b/Tests/Normalizer/NumberNormalizerTest.php index 338f63ba..56d4776b 100644 --- a/Tests/Normalizer/NumberNormalizerTest.php +++ b/Tests/Normalizer/NumberNormalizerTest.php @@ -52,6 +52,7 @@ public static function supportsNormalizationProvider(): iterable } /** + * @requires PHP 8.4 * @requires extension bcmath * * @dataProvider normalizeGoodBcMathNumberValueProvider @@ -149,6 +150,8 @@ public static function denormalizeGoodBcMathNumberValueProvider(): iterable } /** + * @requires extension gmp + * * @dataProvider denormalizeGoodGmpValueProvider */ public function testDenormalizeGmp(string|int $data, string $type, \GMP $expected)