vendor/symfony/serializer/Encoder/XmlEncoder.php line 74

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Serializer\Encoder;
  11. use Symfony\Component\Serializer\Exception\BadMethodCallException;
  12. use Symfony\Component\Serializer\Exception\NotEncodableValueException;
  13. use Symfony\Component\Serializer\SerializerAwareInterface;
  14. use Symfony\Component\Serializer\SerializerAwareTrait;
  15. /**
  16.  * @author Jordi Boggiano <j.boggiano@seld.be>
  17.  * @author John Wards <jwards@whiteoctober.co.uk>
  18.  * @author Fabian Vogler <fabian@equivalence.ch>
  19.  * @author Kévin Dunglas <dunglas@gmail.com>
  20.  * @author Dany Maillard <danymaillard93b@gmail.com>
  21.  */
  22. class XmlEncoder implements EncoderInterfaceDecoderInterfaceNormalizationAwareInterfaceSerializerAwareInterface
  23. {
  24.     use SerializerAwareTrait;
  25.     public const FORMAT 'xml';
  26.     public const AS_COLLECTION 'as_collection';
  27.     /**
  28.      * An array of ignored XML node types while decoding, each one of the DOM Predefined XML_* constants.
  29.      */
  30.     public const DECODER_IGNORED_NODE_TYPES 'decoder_ignored_node_types';
  31.     /**
  32.      * An array of ignored XML node types while encoding, each one of the DOM Predefined XML_* constants.
  33.      */
  34.     public const ENCODER_IGNORED_NODE_TYPES 'encoder_ignored_node_types';
  35.     public const ENCODING 'xml_encoding';
  36.     public const FORMAT_OUTPUT 'xml_format_output';
  37.     /**
  38.      * A bit field of LIBXML_* constants.
  39.      */
  40.     public const LOAD_OPTIONS 'load_options';
  41.     public const REMOVE_EMPTY_TAGS 'remove_empty_tags';
  42.     public const ROOT_NODE_NAME 'xml_root_node_name';
  43.     public const STANDALONE 'xml_standalone';
  44.     public const TYPE_CAST_ATTRIBUTES 'xml_type_cast_attributes';
  45.     public const VERSION 'xml_version';
  46.     private $defaultContext = [
  47.         self::AS_COLLECTION => false,
  48.         self::DECODER_IGNORED_NODE_TYPES => [\XML_PI_NODE\XML_COMMENT_NODE],
  49.         self::ENCODER_IGNORED_NODE_TYPES => [],
  50.         self::LOAD_OPTIONS => \LIBXML_NONET \LIBXML_NOBLANKS,
  51.         self::REMOVE_EMPTY_TAGS => false,
  52.         self::ROOT_NODE_NAME => 'response',
  53.         self::TYPE_CAST_ATTRIBUTES => true,
  54.     ];
  55.     public function __construct(array $defaultContext = [])
  56.     {
  57.         $this->defaultContext array_merge($this->defaultContext$defaultContext);
  58.     }
  59.     /**
  60.      * {@inheritdoc}
  61.      */
  62.     public function encode(mixed $datastring $format, array $context = []): string
  63.     {
  64.         $encoderIgnoredNodeTypes $context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES];
  65.         $ignorePiNode \in_array(\XML_PI_NODE$encoderIgnoredNodeTypestrue);
  66.         if ($data instanceof \DOMDocument) {
  67.             return $data->saveXML($ignorePiNode $data->documentElement null);
  68.         }
  69.         $xmlRootNodeName $context[self::ROOT_NODE_NAME] ?? $this->defaultContext[self::ROOT_NODE_NAME];
  70.         $dom $this->createDomDocument($context);
  71.         if (null !== $data && !is_scalar($data)) {
  72.             $root $dom->createElement($xmlRootNodeName);
  73.             $dom->appendChild($root);
  74.             $this->buildXml($root$data$format$context$xmlRootNodeName);
  75.         } else {
  76.             $this->appendNode($dom$data$format$context$xmlRootNodeName);
  77.         }
  78.         return $dom->saveXML($ignorePiNode $dom->documentElement null);
  79.     }
  80.     /**
  81.      * {@inheritdoc}
  82.      */
  83.     public function decode(string $datastring $format, array $context = []): mixed
  84.     {
  85.         if ('' === trim($data)) {
  86.             throw new NotEncodableValueException('Invalid XML data, it cannot be empty.');
  87.         }
  88.         $internalErrors libxml_use_internal_errors(true);
  89.         libxml_clear_errors();
  90.         $dom = new \DOMDocument();
  91.         $dom->loadXML($data$context[self::LOAD_OPTIONS] ?? $this->defaultContext[self::LOAD_OPTIONS]);
  92.         libxml_use_internal_errors($internalErrors);
  93.         if ($error libxml_get_last_error()) {
  94.             libxml_clear_errors();
  95.             throw new NotEncodableValueException($error->message);
  96.         }
  97.         $rootNode null;
  98.         $decoderIgnoredNodeTypes $context[self::DECODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::DECODER_IGNORED_NODE_TYPES];
  99.         foreach ($dom->childNodes as $child) {
  100.             if (\in_array($child->nodeType$decoderIgnoredNodeTypestrue)) {
  101.                 continue;
  102.             }
  103.             if (\XML_DOCUMENT_TYPE_NODE === $child->nodeType) {
  104.                 throw new NotEncodableValueException('Document types are not allowed.');
  105.             }
  106.             if (!$rootNode) {
  107.                 $rootNode $child;
  108.             }
  109.         }
  110.         // todo: throw an exception if the root node name is not correctly configured (bc)
  111.         if ($rootNode->hasChildNodes()) {
  112.             $xpath = new \DOMXPath($dom);
  113.             $data = [];
  114.             foreach ($xpath->query('namespace::*'$dom->documentElement) as $nsNode) {
  115.                 $data['@'.$nsNode->nodeName] = $nsNode->nodeValue;
  116.             }
  117.             unset($data['@xmlns:xml']);
  118.             if (empty($data)) {
  119.                 return $this->parseXml($rootNode$context);
  120.             }
  121.             return array_merge($data, (array) $this->parseXml($rootNode$context));
  122.         }
  123.         if (!$rootNode->hasAttributes()) {
  124.             return $rootNode->nodeValue;
  125.         }
  126.         $data = [];
  127.         foreach ($rootNode->attributes as $attrKey => $attr) {
  128.             $data['@'.$attrKey] = $attr->nodeValue;
  129.         }
  130.         $data['#'] = $rootNode->nodeValue;
  131.         return $data;
  132.     }
  133.     /**
  134.      * {@inheritdoc}
  135.      *
  136.      * @param array $context
  137.      */
  138.     public function supportsEncoding(string $format /*, array $context = [] */): bool
  139.     {
  140.         return self::FORMAT === $format;
  141.     }
  142.     /**
  143.      * {@inheritdoc}
  144.      *
  145.      * @param array $context
  146.      */
  147.     public function supportsDecoding(string $format /*, array $context = [] */): bool
  148.     {
  149.         return self::FORMAT === $format;
  150.     }
  151.     final protected function appendXMLString(\DOMNode $nodestring $val): bool
  152.     {
  153.         if ('' !== $val) {
  154.             $frag $node->ownerDocument->createDocumentFragment();
  155.             $frag->appendXML($val);
  156.             $node->appendChild($frag);
  157.             return true;
  158.         }
  159.         return false;
  160.     }
  161.     final protected function appendText(\DOMNode $nodestring $val): bool
  162.     {
  163.         $nodeText $node->ownerDocument->createTextNode($val);
  164.         $node->appendChild($nodeText);
  165.         return true;
  166.     }
  167.     final protected function appendCData(\DOMNode $nodestring $val): bool
  168.     {
  169.         $nodeText $node->ownerDocument->createCDATASection($val);
  170.         $node->appendChild($nodeText);
  171.         return true;
  172.     }
  173.     final protected function appendDocumentFragment(\DOMNode $node\DOMDocumentFragment $fragment): bool
  174.     {
  175.         if ($fragment instanceof \DOMDocumentFragment) {
  176.             $node->appendChild($fragment);
  177.             return true;
  178.         }
  179.         return false;
  180.     }
  181.     final protected function appendComment(\DOMNode $nodestring $data): bool
  182.     {
  183.         $node->appendChild($node->ownerDocument->createComment($data));
  184.         return true;
  185.     }
  186.     /**
  187.      * Checks the name is a valid xml element name.
  188.      */
  189.     final protected function isElementNameValid(string $name): bool
  190.     {
  191.         return $name &&
  192.             !str_contains($name' ') &&
  193.             preg_match('#^[\pL_][\pL0-9._:-]*$#ui'$name);
  194.     }
  195.     /**
  196.      * Parse the input DOMNode into an array or a string.
  197.      */
  198.     private function parseXml(\DOMNode $node, array $context = []): array|string
  199.     {
  200.         $data $this->parseXmlAttributes($node$context);
  201.         $value $this->parseXmlValue($node$context);
  202.         if (!\count($data)) {
  203.             return $value;
  204.         }
  205.         if (!\is_array($value)) {
  206.             $data['#'] = $value;
  207.             return $data;
  208.         }
  209.         if (=== \count($value) && key($value)) {
  210.             $data[key($value)] = current($value);
  211.             return $data;
  212.         }
  213.         foreach ($value as $key => $val) {
  214.             $data[$key] = $val;
  215.         }
  216.         return $data;
  217.     }
  218.     /**
  219.      * Parse the input DOMNode attributes into an array.
  220.      */
  221.     private function parseXmlAttributes(\DOMNode $node, array $context = []): array
  222.     {
  223.         if (!$node->hasAttributes()) {
  224.             return [];
  225.         }
  226.         $data = [];
  227.         $typeCastAttributes = (bool) ($context[self::TYPE_CAST_ATTRIBUTES] ?? $this->defaultContext[self::TYPE_CAST_ATTRIBUTES]);
  228.         foreach ($node->attributes as $attr) {
  229.             if (!is_numeric($attr->nodeValue) || !$typeCastAttributes || (isset($attr->nodeValue[1]) && '0' === $attr->nodeValue[0] && '.' !== $attr->nodeValue[1])) {
  230.                 $data['@'.$attr->nodeName] = $attr->nodeValue;
  231.                 continue;
  232.             }
  233.             if (false !== $val filter_var($attr->nodeValue\FILTER_VALIDATE_INT)) {
  234.                 $data['@'.$attr->nodeName] = $val;
  235.                 continue;
  236.             }
  237.             $data['@'.$attr->nodeName] = (float) $attr->nodeValue;
  238.         }
  239.         return $data;
  240.     }
  241.     /**
  242.      * Parse the input DOMNode value (content and children) into an array or a string.
  243.      */
  244.     private function parseXmlValue(\DOMNode $node, array $context = []): array|string
  245.     {
  246.         if (!$node->hasChildNodes()) {
  247.             return $node->nodeValue;
  248.         }
  249.         if (=== $node->childNodes->length && \in_array($node->firstChild->nodeType, [\XML_TEXT_NODE\XML_CDATA_SECTION_NODE])) {
  250.             return $node->firstChild->nodeValue;
  251.         }
  252.         $value = [];
  253.         $decoderIgnoredNodeTypes $context[self::DECODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::DECODER_IGNORED_NODE_TYPES];
  254.         foreach ($node->childNodes as $subnode) {
  255.             if (\in_array($subnode->nodeType$decoderIgnoredNodeTypestrue)) {
  256.                 continue;
  257.             }
  258.             $val $this->parseXml($subnode$context);
  259.             if ('item' === $subnode->nodeName && isset($val['@key'])) {
  260.                 $value[$val['@key']] = $val['#'] ?? $val;
  261.             } else {
  262.                 $value[$subnode->nodeName][] = $val;
  263.             }
  264.         }
  265.         $asCollection $context[self::AS_COLLECTION] ?? $this->defaultContext[self::AS_COLLECTION];
  266.         foreach ($value as $key => $val) {
  267.             if (!$asCollection && \is_array($val) && === \count($val)) {
  268.                 $value[$key] = current($val);
  269.             }
  270.         }
  271.         return $value;
  272.     }
  273.     /**
  274.      * Parse the data and convert it to DOMElements.
  275.      *
  276.      * @throws NotEncodableValueException
  277.      */
  278.     private function buildXml(\DOMNode $parentNodemixed $datastring $format, array $contextstring $xmlRootNodeName null): bool
  279.     {
  280.         $append true;
  281.         $removeEmptyTags $context[self::REMOVE_EMPTY_TAGS] ?? $this->defaultContext[self::REMOVE_EMPTY_TAGS] ?? false;
  282.         $encoderIgnoredNodeTypes $context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES];
  283.         if (\is_array($data) || ($data instanceof \Traversable && (null === $this->serializer || !$this->serializer->supportsNormalization($data$format)))) {
  284.             foreach ($data as $key => $data) {
  285.                 //Ah this is the magic @ attribute types.
  286.                 if (str_starts_with($key'@') && $this->isElementNameValid($attributeName substr($key1))) {
  287.                     if (!is_scalar($data)) {
  288.                         $data $this->serializer->normalize($data$format$context);
  289.                     }
  290.                     $parentNode->setAttribute($attributeName$data);
  291.                 } elseif ('#' === $key) {
  292.                     $append $this->selectNodeType($parentNode$data$format$context);
  293.                 } elseif ('#comment' === $key) {
  294.                     if (!\in_array(\XML_COMMENT_NODE$encoderIgnoredNodeTypestrue)) {
  295.                         $append $this->appendComment($parentNode$data);
  296.                     }
  297.                 } elseif (\is_array($data) && false === is_numeric($key)) {
  298.                     // Is this array fully numeric keys?
  299.                     if (ctype_digit(implode(''array_keys($data)))) {
  300.                         /*
  301.                          * Create nodes to append to $parentNode based on the $key of this array
  302.                          * Produces <xml><item>0</item><item>1</item></xml>
  303.                          * From ["item" => [0,1]];.
  304.                          */
  305.                         foreach ($data as $subData) {
  306.                             $append $this->appendNode($parentNode$subData$format$context$key);
  307.                         }
  308.                     } else {
  309.                         $append $this->appendNode($parentNode$data$format$context$key);
  310.                     }
  311.                 } elseif (is_numeric($key) || !$this->isElementNameValid($key)) {
  312.                     $append $this->appendNode($parentNode$data$format$context'item'$key);
  313.                 } elseif (null !== $data || !$removeEmptyTags) {
  314.                     $append $this->appendNode($parentNode$data$format$context$key);
  315.                 }
  316.             }
  317.             return $append;
  318.         }
  319.         if (\is_object($data)) {
  320.             if (null === $this->serializer) {
  321.                 throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.'__METHOD__));
  322.             }
  323.             $data $this->serializer->normalize($data$format$context);
  324.             if (null !== $data && !is_scalar($data)) {
  325.                 return $this->buildXml($parentNode$data$format$context$xmlRootNodeName);
  326.             }
  327.             // top level data object was normalized into a scalar
  328.             if (!$parentNode->parentNode->parentNode) {
  329.                 $root $parentNode->parentNode;
  330.                 $root->removeChild($parentNode);
  331.                 return $this->appendNode($root$data$format$context$xmlRootNodeName);
  332.             }
  333.             return $this->appendNode($parentNode$data$format$context'data');
  334.         }
  335.         throw new NotEncodableValueException('An unexpected value could not be serialized: '.(!\is_resource($data) ? var_export($datatrue) : sprintf('%s resource'get_resource_type($data))));
  336.     }
  337.     /**
  338.      * Selects the type of node to create and appends it to the parent.
  339.      */
  340.     private function appendNode(\DOMNode $parentNodemixed $datastring $format, array $contextstring $nodeNamestring $key null): bool
  341.     {
  342.         $dom $parentNode instanceof \DomDocument $parentNode $parentNode->ownerDocument;
  343.         $node $dom->createElement($nodeName);
  344.         if (null !== $key) {
  345.             $node->setAttribute('key'$key);
  346.         }
  347.         $appendNode $this->selectNodeType($node$data$format$context);
  348.         // we may have decided not to append this node, either in error or if its $nodeName is not valid
  349.         if ($appendNode) {
  350.             $parentNode->appendChild($node);
  351.         }
  352.         return $appendNode;
  353.     }
  354.     /**
  355.      * Checks if a value contains any characters which would require CDATA wrapping.
  356.      */
  357.     private function needsCdataWrapping(string $val): bool
  358.     {
  359.         return preg_match('/[<>&]/'$val);
  360.     }
  361.     /**
  362.      * Tests the value being passed and decide what sort of element to create.
  363.      *
  364.      * @throws NotEncodableValueException
  365.      */
  366.     private function selectNodeType(\DOMNode $nodemixed $valstring $format, array $context): bool
  367.     {
  368.         if (\is_array($val)) {
  369.             return $this->buildXml($node$val$format$context);
  370.         } elseif ($val instanceof \SimpleXMLElement) {
  371.             $child $node->ownerDocument->importNode(dom_import_simplexml($val), true);
  372.             $node->appendChild($child);
  373.         } elseif ($val instanceof \Traversable) {
  374.             $this->buildXml($node$val$format$context);
  375.         } elseif ($val instanceof \DOMNode) {
  376.             $child $node->ownerDocument->importNode($valtrue);
  377.             $node->appendChild($child);
  378.         } elseif (\is_object($val)) {
  379.             if (null === $this->serializer) {
  380.                 throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.'__METHOD__));
  381.             }
  382.             return $this->selectNodeType($node$this->serializer->normalize($val$format$context), $format$context);
  383.         } elseif (is_numeric($val)) {
  384.             return $this->appendText($node, (string) $val);
  385.         } elseif (\is_string($val) && $this->needsCdataWrapping($val)) {
  386.             return $this->appendCData($node$val);
  387.         } elseif (\is_string($val)) {
  388.             return $this->appendText($node$val);
  389.         } elseif (\is_bool($val)) {
  390.             return $this->appendText($node, (int) $val);
  391.         }
  392.         return true;
  393.     }
  394.     /**
  395.      * Create a DOM document, taking serializer options into account.
  396.      */
  397.     private function createDomDocument(array $context): \DOMDocument
  398.     {
  399.         $document = new \DOMDocument();
  400.         // Set an attribute on the DOM document specifying, as part of the XML declaration,
  401.         $xmlOptions = [
  402.             // nicely formats output with indentation and extra space
  403.             self::FORMAT_OUTPUT => 'formatOutput',
  404.             // the version number of the document
  405.             self::VERSION => 'xmlVersion',
  406.             // the encoding of the document
  407.             self::ENCODING => 'encoding',
  408.             // whether the document is standalone
  409.             self::STANDALONE => 'xmlStandalone',
  410.         ];
  411.         foreach ($xmlOptions as $xmlOption => $documentProperty) {
  412.             if ($contextOption $context[$xmlOption] ?? $this->defaultContext[$xmlOption] ?? false) {
  413.                 $document->$documentProperty $contextOption;
  414.             }
  415.         }
  416.         return $document;
  417.     }
  418. }