From d89d08faee1fab665ff5138c1f57b5033dae030f Mon Sep 17 00:00:00 2001 From: Maximilian Ruta Date: Wed, 16 Apr 2025 15:41:39 +0200 Subject: [PATCH] [Serializer] Add `XmlEncoder::PRESERVE_NUMERIC_KEYS` context option --- src/Symfony/Component/Serializer/CHANGELOG.md | 1 + .../Encoder/XmlEncoderContextBuilder.php | 8 ++++ .../Serializer/Encoder/XmlEncoder.php | 7 ++- .../Loader/schema/serialization.schema.json | 4 ++ .../Tests/Encoder/XmlEncoderTest.php | 44 +++++++++++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 70d8f7c4528eb..a22f425c7dcb5 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add `CDATA_WRAPPING_NAME_PATTERN` support to `XmlEncoder` * Add support for `can*()` methods to `AttributeLoader` * Make `AttributeMetadata` and `ClassMetadata` final + * Add `XmlEncoder::PRESERVE_NUMERIC_KEYS` context option * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters in attribute classes in favor of public properties * Deprecate `ClassMetadataFactoryCompiler` diff --git a/src/Symfony/Component/Serializer/Context/Encoder/XmlEncoderContextBuilder.php b/src/Symfony/Component/Serializer/Context/Encoder/XmlEncoderContextBuilder.php index 9d0159c064b09..ece1c65144b1a 100644 --- a/src/Symfony/Component/Serializer/Context/Encoder/XmlEncoderContextBuilder.php +++ b/src/Symfony/Component/Serializer/Context/Encoder/XmlEncoderContextBuilder.php @@ -168,4 +168,12 @@ public function withIgnoreEmptyAttributes(?bool $ignoreEmptyAttributes): static { return $this->with(XmlEncoder::IGNORE_EMPTY_ATTRIBUTES, $ignoreEmptyAttributes); } + + /** + * Configures whether to preserve numeric keys in array. + */ + public function withPreserveNumericKeys(?bool $preserveNumericKeys): static + { + return $this->with(XmlEncoder::PRESERVE_NUMERIC_KEYS, $preserveNumericKeys); + } } diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index 4c03057aca85d..de6645a8bb998 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -62,6 +62,7 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa public const CDATA_WRAPPING_NAME_PATTERN = 'cdata_wrapping_name_pattern'; public const CDATA_WRAPPING_PATTERN = 'cdata_wrapping_pattern'; public const IGNORE_EMPTY_ATTRIBUTES = 'ignore_empty_attributes'; + public const PRESERVE_NUMERIC_KEYS = 'preserve_numeric_keys'; private array $defaultContext = [ self::AS_COLLECTION => false, @@ -76,6 +77,7 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa self::CDATA_WRAPPING_NAME_PATTERN => false, self::CDATA_WRAPPING_PATTERN => '/[<>&]/', self::IGNORE_EMPTY_ATTRIBUTES => false, + self::PRESERVE_NUMERIC_KEYS => false, ]; public function __construct(array $defaultContext = []) @@ -347,6 +349,7 @@ private function buildXml(\DOMNode $parentNode, mixed $data, string $format, arr { $append = true; $removeEmptyTags = $context[self::REMOVE_EMPTY_TAGS] ?? $this->defaultContext[self::REMOVE_EMPTY_TAGS] ?? false; + $preserveNumericKeys = $context[self::PRESERVE_NUMERIC_KEYS] ?? $this->defaultContext[self::PRESERVE_NUMERIC_KEYS] ?? false; $encoderIgnoredNodeTypes = $context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES]; if (\is_array($data) || ($data instanceof \Traversable && (null === $this->serializer || !$this->serializer->supportsNormalization($data, $format)))) { @@ -373,9 +376,9 @@ private function buildXml(\DOMNode $parentNode, mixed $data, string $format, arr if (!\in_array(\XML_COMMENT_NODE, $encoderIgnoredNodeTypes, true)) { $append = $this->appendComment($parentNode, $data); } - } elseif (\is_array($data) && false === is_numeric($key)) { + } elseif (\is_array($data) && !is_numeric($key)) { // Is this array fully numeric keys? - if (ctype_digit(implode('', array_keys($data)))) { + if (!$preserveNumericKeys && null === array_find_key($data, static fn ($v, $k) => is_string($k))) { /* * Create nodes to append to $parentNode based on the $key of this array * Produces 01 diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/schema/serialization.schema.json b/src/Symfony/Component/Serializer/Mapping/Loader/schema/serialization.schema.json index 077e84b36157d..246a238aadd96 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/schema/serialization.schema.json +++ b/src/Symfony/Component/Serializer/Mapping/Loader/schema/serialization.schema.json @@ -326,6 +326,10 @@ "type": "boolean", "description": "Whether to ignore empty attributes (XmlEncoder)" }, + "preserve_numeric_keys": { + "type": "boolean", + "description": "Whether to preserve numeric keys in array (XmlEncoder)" + }, "inline_threshold": { "type": "integer", "description": "Threshold to switch to inline YAML (YamlEncoder)" diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index d154461bc435d..3e15d5661d2a0 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -1033,4 +1033,48 @@ public function testEncodeIgnoringEmptyAttribute() $this->assertEquals($expected, $this->encoder->encode($data, 'xml', ['ignore_empty_attributes' => true])); } + + public function testEncodeArrayAsItem() + { + $expected = <<<'XML' + + BenjaminAlexandreDamienClay + + XML; + $source = ['person' => [ + ['@key' => 0, 'firstname' => 'Benjamin', 'lastname' => 'Alexandre'], + ['@key' => 1, 'firstname' => 'Damien', 'lastname' => 'Clay'], + ]]; + + $this->assertSame($expected, $this->encoder->encode($source, 'xml', [ + XmlEncoder::PRESERVE_NUMERIC_KEYS => true, + ])); + } + + public function testDecodeArrayAsItem() + { + $source = <<<'XML' + + + + + Benjamin + Alexandre + + + Damien + Clay + + + + XML; + $expected = ['person' => [ + ['@key' => 0, 'firstname' => 'Benjamin', 'lastname' => 'Alexandre', ], + ['@key' => 1, 'firstname' => 'Damien', 'lastname' => 'Clay', ], + ]]; + + $this->assertSame($expected, $this->encoder->decode($source, 'xml', [ + XmlEncoder::PRESERVE_NUMERIC_KEYS => true, + ])); + } }