diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php
index 4c03057aca85d..3bf4b4ff9f26e 100644
--- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php
@@ -62,6 +62,8 @@ 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 VALIDATE_ROOT_NODE_NAME = 'validate_root_node_name';
+ public const VALIDATE_ROOT_NODE_EXISTS = 'validate_root_node_exists';
private array $defaultContext = [
self::AS_COLLECTION => false,
@@ -76,6 +78,8 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa
self::CDATA_WRAPPING_NAME_PATTERN => false,
self::CDATA_WRAPPING_PATTERN => '/[<>&]/',
self::IGNORE_EMPTY_ATTRIBUTES => false,
+ self::VALIDATE_ROOT_NODE_NAME => false,
+ self::VALIDATE_ROOT_NODE_EXISTS => false,
];
public function __construct(array $defaultContext = [])
@@ -140,7 +144,30 @@ public function decode(string $data, string $format, array $context = []): mixed
}
}
- // todo: throw an exception if the root node name is not correctly configured (bc)
+ // Validate root node existence and, if configured by the user, its name
+ $shouldValidateRootExists = (bool) ($context[self::VALIDATE_ROOT_NODE_EXISTS] ?? $this->defaultContext[self::VALIDATE_ROOT_NODE_EXISTS] ?? false);
+ if (!$rootNode instanceof \DOMNode) {
+ if ($shouldValidateRootExists) {
+ throw new NotEncodableValueException('Invalid XML data: no root node found.');
+ }
+
+ trigger_deprecation('symfony/serializer', '7.4', 'Decoding XML without a root node is deprecated and will throw a NotEncodableValueException in 8.0.');
+
+ // Return empty result for backward compatibility
+ return [];
+ }
+
+ $shouldValidateRoot = (bool) ($context[self::VALIDATE_ROOT_NODE_NAME] ?? $this->defaultContext[self::VALIDATE_ROOT_NODE_NAME] ?? false);
+ if (\array_key_exists(self::ROOT_NODE_NAME, $context)) {
+ $expectedRootName = (string) $context[self::ROOT_NODE_NAME];
+ if ($expectedRootName !== $rootNode->nodeName) {
+ if ($shouldValidateRoot) {
+ throw new NotEncodableValueException(\sprintf('Expected root node "%s", but found "%s".', $expectedRootName, $rootNode->nodeName));
+ }
+
+ trigger_deprecation('symfony/serializer', '7.4', 'Decoding XML with a mismatching root node name is deprecated and will throw an exception in 8.0. Expected root node "%s", but found "%s". Set the "%s" context option to true to enable validation now.', $expectedRootName, $rootNode->nodeName, self::VALIDATE_ROOT_NODE_NAME);
+ }
+ }
if ($rootNode->hasChildNodes()) {
$data = $this->parseXml($rootNode, $context);
diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php
index d154461bc435d..4216a32f5ce5c 100644
--- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php
@@ -772,6 +772,38 @@ public function testDecodeEmptyXml()
$this->encoder->decode(' ', 'xml');
}
+ public function testDecodeThrowsOnRootNodeNameMismatch()
+ {
+ $this->expectException(NotEncodableValueException::class);
+ $this->expectExceptionMessage('Expected root node "expectedRoot", but found "wrongRoot".');
+
+ $xml = '- value
';
+ $this->encoder->decode($xml, 'xml', ['xml_root_node_name' => 'expectedRoot', XmlEncoder::VALIDATE_ROOT_NODE_NAME => true]);
+ }
+
+ public function testDecodeThrowsWhenNoRootNodeFound()
+ {
+ $this->expectException(NotEncodableValueException::class);
+ $this->expectExceptionMessage('Invalid XML data: no root node found.');
+
+ $xml = '1';
+ $this->encoder->decode($xml, 'xml', [
+ XmlEncoder::DECODER_IGNORED_NODE_TYPES => [\XML_PI_NODE, \XML_COMMENT_NODE, \XML_ELEMENT_NODE],
+ XmlEncoder::VALIDATE_ROOT_NODE_EXISTS => true
+ ]);
+ }
+
+ public function testDecodeReturnsEmptyArrayWhenNoRootNodeFoundAndValidationDisabled()
+ {
+ $xml = '1';
+ $result = $this->encoder->decode($xml, 'xml', [
+ XmlEncoder::DECODER_IGNORED_NODE_TYPES => [\XML_PI_NODE, \XML_COMMENT_NODE, \XML_ELEMENT_NODE],
+ XmlEncoder::VALIDATE_ROOT_NODE_EXISTS => false
+ ]);
+
+ $this->assertSame([], $result);
+ }
+
protected static function getXmlSource(): string
{
return ''."\n".