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 - 0
- 1
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'
+
+ - BenjaminAlexandre
- DamienClay
+
+ 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,
+ ]));
+ }
}