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 <[email protected]>
  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 <[email protected]>
  17.  * @author John Wards <[email protected]>
  18.  * @author Fabian Vogler <[email protected]>
  19.  * @author Kévin Dunglas <[email protected]>
  20.  * @author Dany Maillard <[email protected]>
  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.     public function supportsEncoding(string $format): bool
  137.     {
  138.         return self::FORMAT === $format;
  139.     }
  140.     /**
  141.      * {@inheritdoc}
  142.      */
  143.     public function supportsDecoding(string $format): bool
  144.     {
  145.         return self::FORMAT === $format;
  146.     }
  147.     final protected function appendXMLString(\DOMNode $nodestring $val): bool
  148.     {
  149.         if ('' !== $val) {
  150.             $frag $node->ownerDocument->createDocumentFragment();
  151.             $frag->appendXML($val);
  152.             $node->appendChild($frag);
  153.             return true;
  154.         }
  155.         return false;
  156.     }
  157.     final protected function appendText(\DOMNode $nodestring $val): bool
  158.     {
  159.         $nodeText $node->ownerDocument->createTextNode($val);
  160.         $node->appendChild($nodeText);
  161.         return true;
  162.     }
  163.     final protected function appendCData(\DOMNode $nodestring $val): bool
  164.     {
  165.         $nodeText $node->ownerDocument->createCDATASection($val);
  166.         $node->appendChild($nodeText);
  167.         return true;
  168.     }
  169.     final protected function appendDocumentFragment(\DOMNode $node\DOMDocumentFragment $fragment): bool
  170.     {
  171.         if ($fragment instanceof \DOMDocumentFragment) {
  172.             $node->appendChild($fragment);
  173.             return true;
  174.         }
  175.         return false;
  176.     }
  177.     final protected function appendComment(\DOMNode $nodestring $data): bool
  178.     {
  179.         $node->appendChild($node->ownerDocument->createComment($data));
  180.         return true;
  181.     }
  182.     /**
  183.      * Checks the name is a valid xml element name.
  184.      */
  185.     final protected function isElementNameValid(string $name): bool
  186.     {
  187.         return $name &&
  188.             !str_contains($name' ') &&
  189.             preg_match('#^[\pL_][\pL0-9._:-]*$#ui'$name);
  190.     }
  191.     /**
  192.      * Parse the input DOMNode into an array or a string.
  193.      */
  194.     private function parseXml(\DOMNode $node, array $context = []): array|string
  195.     {
  196.         $data $this->parseXmlAttributes($node$context);
  197.         $value $this->parseXmlValue($node$context);
  198.         if (!\count($data)) {
  199.             return $value;
  200.         }
  201.         if (!\is_array($value)) {
  202.             $data['#'] = $value;
  203.             return $data;
  204.         }
  205.         if (=== \count($value) && key($value)) {
  206.             $data[key($value)] = current($value);
  207.             return $data;
  208.         }
  209.         foreach ($value as $key => $val) {
  210.             $data[$key] = $val;
  211.         }
  212.         return $data;
  213.     }
  214.     /**
  215.      * Parse the input DOMNode attributes into an array.
  216.      */
  217.     private function parseXmlAttributes(\DOMNode $node, array $context = []): array
  218.     {
  219.         if (!$node->hasAttributes()) {
  220.             return [];
  221.         }
  222.         $data = [];
  223.         $typeCastAttributes = (bool) ($context[self::TYPE_CAST_ATTRIBUTES] ?? $this->defaultContext[self::TYPE_CAST_ATTRIBUTES]);
  224.         foreach ($node->attributes as $attr) {
  225.             if (!is_numeric($attr->nodeValue) || !$typeCastAttributes || (isset($attr->nodeValue[1]) && '0' === $attr->nodeValue[0] && '.' !== $attr->nodeValue[1])) {
  226.                 $data['@'.$attr->nodeName] = $attr->nodeValue;
  227.                 continue;
  228.             }
  229.             if (false !== $val filter_var($attr->nodeValue\FILTER_VALIDATE_INT)) {
  230.                 $data['@'.$attr->nodeName] = $val;
  231.                 continue;
  232.             }
  233.             $data['@'.$attr->nodeName] = (float) $attr->nodeValue;
  234.         }
  235.         return $data;
  236.     }
  237.     /**
  238.      * Parse the input DOMNode value (content and children) into an array or a string.
  239.      */
  240.     private function parseXmlValue(\DOMNode $node, array $context = []): array|string
  241.     {
  242.         if (!$node->hasChildNodes()) {
  243.             return $node->nodeValue;
  244.         }
  245.         if (=== $node->childNodes->length && \in_array($node->firstChild->nodeType, [\XML_TEXT_NODE\XML_CDATA_SECTION_NODE])) {
  246.             return $node->firstChild->nodeValue;
  247.         }
  248.         $value = [];
  249.         $decoderIgnoredNodeTypes $context[self::DECODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::DECODER_IGNORED_NODE_TYPES];
  250.         foreach ($node->childNodes as $subnode) {
  251.             if (\in_array($subnode->nodeType$decoderIgnoredNodeTypestrue)) {
  252.                 continue;
  253.             }
  254.             $val $this->parseXml($subnode$context);
  255.             if ('item' === $subnode->nodeName && isset($val['@key'])) {
  256.                 $value[$val['@key']] = $val['#'] ?? $val;
  257.             } else {
  258.                 $value[$subnode->nodeName][] = $val;
  259.             }
  260.         }
  261.         $asCollection $context[self::AS_COLLECTION] ?? $this->defaultContext[self::AS_COLLECTION];
  262.         foreach ($value as $key => $val) {
  263.             if (!$asCollection && \is_array($val) && === \count($val)) {
  264.                 $value[$key] = current($val);
  265.             }
  266.         }
  267.         return $value;
  268.     }
  269.     /**
  270.      * Parse the data and convert it to DOMElements.
  271.      *
  272.      * @throws NotEncodableValueException
  273.      */
  274.     private function buildXml(\DOMNode $parentNodemixed $datastring $format, array $contextstring $xmlRootNodeName null): bool
  275.     {
  276.         $append true;
  277.         $removeEmptyTags $context[self::REMOVE_EMPTY_TAGS] ?? $this->defaultContext[self::REMOVE_EMPTY_TAGS] ?? false;
  278.         $encoderIgnoredNodeTypes $context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES];
  279.         if (\is_array($data) || ($data instanceof \Traversable && (null === $this->serializer || !$this->serializer->supportsNormalization($data$format)))) {
  280.             foreach ($data as $key => $data) {
  281.                 // Ah this is the magic @ attribute types.
  282.                 if (str_starts_with($key'@') && $this->isElementNameValid($attributeName substr($key1))) {
  283.                     if (!\is_scalar($data)) {
  284.                         $data $this->serializer->normalize($data$format$context);
  285.                     }
  286.                     if (\is_bool($data)) {
  287.                         $data = (int) $data;
  288.                     }
  289.                     $parentNode->setAttribute($attributeName$data);
  290.                 } elseif ('#' === $key) {
  291.                     $append $this->selectNodeType($parentNode$data$format$context);
  292.                 } elseif ('#comment' === $key) {
  293.                     if (!\in_array(\XML_COMMENT_NODE$encoderIgnoredNodeTypestrue)) {
  294.                         $append $this->appendComment($parentNode$data);
  295.                     }
  296.                 } elseif (\is_array($data) && false === is_numeric($key)) {
  297.                     // Is this array fully numeric keys?
  298.                     if (ctype_digit(implode(''array_keys($data)))) {
  299.                         /*
  300.                          * Create nodes to append to $parentNode based on the $key of this array
  301.                          * Produces <xml><item>0</item><item>1</item></xml>
  302.                          * From ["item" => [0,1]];.
  303.                          */
  304.                         foreach ($data as $subData) {
  305.                             $append $this->appendNode($parentNode$subData$format$context$key);
  306.                         }
  307.                     } else {
  308.                         $append $this->appendNode($parentNode$data$format$context$key);
  309.                     }
  310.                 } elseif (is_numeric($key) || !$this->isElementNameValid($key)) {
  311.                     $append $this->appendNode($parentNode$data$format$context'item'$key);
  312.                 } elseif (null !== $data || !$removeEmptyTags) {
  313.                     $append $this->appendNode($parentNode$data$format$context$key);
  314.                 }
  315.             }
  316.             return $append;
  317.         }
  318.         if (\is_object($data)) {
  319.             if (null === $this->serializer) {
  320.                 throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.'__METHOD__));
  321.             }
  322.             $data $this->serializer->normalize($data$format$context);
  323.             if (null !== $data && !\is_scalar($data)) {
  324.                 return $this->buildXml($parentNode$data$format$context$xmlRootNodeName);
  325.             }
  326.             // top level data object was normalized into a scalar
  327.             if (!$parentNode->parentNode->parentNode) {
  328.                 $root $parentNode->parentNode;
  329.                 $root->removeChild($parentNode);
  330.                 return $this->appendNode($root$data$format$context$xmlRootNodeName);
  331.             }
  332.             return $this->appendNode($parentNode$data$format$context'data');
  333.         }
  334.         throw new NotEncodableValueException('An unexpected value could not be serialized: '.(!\is_resource($data) ? var_export($datatrue) : sprintf('%s resource'get_resource_type($data))));
  335.     }
  336.     /**
  337.      * Selects the type of node to create and appends it to the parent.
  338.      */
  339.     private function appendNode(\DOMNode $parentNodemixed $datastring $format, array $contextstring $nodeNamestring $key null): bool
  340.     {
  341.         $dom $parentNode instanceof \DOMDocument $parentNode $parentNode->ownerDocument;
  342.         $node $dom->createElement($nodeName);
  343.         if (null !== $key) {
  344.             $node->setAttribute('key'$key);
  345.         }
  346.         $appendNode $this->selectNodeType($node$data$format$context);
  347.         // we may have decided not to append this node, either in error or if its $nodeName is not valid
  348.         if ($appendNode) {
  349.             $parentNode->appendChild($node);
  350.         }
  351.         return $appendNode;
  352.     }
  353.     /**
  354.      * Checks if a value contains any characters which would require CDATA wrapping.
  355.      */
  356.     private function needsCdataWrapping(string $val): bool
  357.     {
  358.         return preg_match('/[<>&]/'$val);
  359.     }
  360.     /**
  361.      * Tests the value being passed and decide what sort of element to create.
  362.      *
  363.      * @throws NotEncodableValueException
  364.      */
  365.     private function selectNodeType(\DOMNode $nodemixed $valstring $format, array $context): bool
  366.     {
  367.         if (\is_array($val)) {
  368.             return $this->buildXml($node$val$format$context);
  369.         } elseif ($val instanceof \SimpleXMLElement) {
  370.             $child $node->ownerDocument->importNode(dom_import_simplexml($val), true);
  371.             $node->appendChild($child);
  372.         } elseif ($val instanceof \Traversable) {
  373.             $this->buildXml($node$val$format$context);
  374.         } elseif ($val instanceof \DOMNode) {
  375.             $child $node->ownerDocument->importNode($valtrue);
  376.             $node->appendChild($child);
  377.         } elseif (\is_object($val)) {
  378.             if (null === $this->serializer) {
  379.                 throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.'__METHOD__));
  380.             }
  381.             return $this->selectNodeType($node$this->serializer->normalize($val$format$context), $format$context);
  382.         } elseif (is_numeric($val)) {
  383.             return $this->appendText($node, (string) $val);
  384.         } elseif (\is_string($val) && $this->needsCdataWrapping($val)) {
  385.             return $this->appendCData($node$val);
  386.         } elseif (\is_string($val)) {
  387.             return $this->appendText($node$val);
  388.         } elseif (\is_bool($val)) {
  389.             return $this->appendText($node, (int) $val);
  390.         }
  391.         return true;
  392.     }
  393.     /**
  394.      * Create a DOM document, taking serializer options into account.
  395.      */
  396.     private function createDomDocument(array $context): \DOMDocument
  397.     {
  398.         $document = new \DOMDocument();
  399.         // Set an attribute on the DOM document specifying, as part of the XML declaration,
  400.         $xmlOptions = [
  401.             // nicely formats output with indentation and extra space
  402.             self::FORMAT_OUTPUT => 'formatOutput',
  403.             // the version number of the document
  404.             self::VERSION => 'xmlVersion',
  405.             // the encoding of the document
  406.             self::ENCODING => 'encoding',
  407.             // whether the document is standalone
  408.             self::STANDALONE => 'xmlStandalone',
  409.         ];
  410.         foreach ($xmlOptions as $xmlOption => $documentProperty) {
  411.             if ($contextOption $context[$xmlOption] ?? $this->defaultContext[$xmlOption] ?? false) {
  412.                 $document->$documentProperty $contextOption;
  413.             }
  414.         }
  415.         return $document;
  416.     }
  417. }