Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Serializer\Encoder; Chris@0: Chris@14: use Symfony\Component\Serializer\Exception\NotEncodableValueException; Chris@0: Chris@0: /** Chris@0: * Encodes XML data. Chris@0: * Chris@0: * @author Jordi Boggiano Chris@0: * @author John Wards Chris@0: * @author Fabian Vogler Chris@0: * @author Kévin Dunglas Chris@0: */ Chris@0: class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface Chris@0: { Chris@14: const FORMAT = 'xml'; Chris@14: Chris@0: /** Chris@0: * @var \DOMDocument Chris@0: */ Chris@0: private $dom; Chris@0: private $format; Chris@0: private $context; Chris@0: private $rootNodeName = 'response'; Chris@0: private $loadOptions; Chris@0: Chris@0: /** Chris@0: * Construct new XmlEncoder and allow to change the root node element name. Chris@0: * Chris@0: * @param string $rootNodeName Chris@0: * @param int|null $loadOptions A bit field of LIBXML_* constants Chris@0: */ Chris@0: public function __construct($rootNodeName = 'response', $loadOptions = null) Chris@0: { Chris@0: $this->rootNodeName = $rootNodeName; Chris@0: $this->loadOptions = null !== $loadOptions ? $loadOptions : LIBXML_NONET | LIBXML_NOBLANKS; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@17: public function encode($data, $format, array $context = []) Chris@0: { Chris@0: if ($data instanceof \DOMDocument) { Chris@0: return $data->saveXML(); Chris@0: } Chris@0: Chris@0: $xmlRootNodeName = $this->resolveXmlRootName($context); Chris@0: Chris@0: $this->dom = $this->createDomDocument($context); Chris@0: $this->format = $format; Chris@0: $this->context = $context; Chris@0: Chris@0: if (null !== $data && !is_scalar($data)) { Chris@0: $root = $this->dom->createElement($xmlRootNodeName); Chris@0: $this->dom->appendChild($root); Chris@0: $this->buildXml($root, $data, $xmlRootNodeName); Chris@0: } else { Chris@0: $this->appendNode($this->dom, $data, $xmlRootNodeName); Chris@0: } Chris@0: Chris@0: return $this->dom->saveXML(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@17: public function decode($data, $format, array $context = []) Chris@0: { Chris@0: if ('' === trim($data)) { Chris@14: throw new NotEncodableValueException('Invalid XML data, it can not be empty.'); Chris@0: } Chris@0: Chris@0: $internalErrors = libxml_use_internal_errors(true); Chris@0: $disableEntities = libxml_disable_entity_loader(true); Chris@0: libxml_clear_errors(); Chris@0: Chris@0: $dom = new \DOMDocument(); Chris@0: $dom->loadXML($data, $this->loadOptions); Chris@0: Chris@0: libxml_use_internal_errors($internalErrors); Chris@0: libxml_disable_entity_loader($disableEntities); Chris@0: Chris@0: if ($error = libxml_get_last_error()) { Chris@0: libxml_clear_errors(); Chris@0: Chris@14: throw new NotEncodableValueException($error->message); Chris@0: } Chris@0: Chris@0: $rootNode = null; Chris@0: foreach ($dom->childNodes as $child) { Chris@14: if (XML_DOCUMENT_TYPE_NODE === $child->nodeType) { Chris@14: throw new NotEncodableValueException('Document types are not allowed.'); Chris@0: } Chris@14: if (!$rootNode && XML_PI_NODE !== $child->nodeType) { Chris@0: $rootNode = $child; Chris@0: } Chris@0: } Chris@0: Chris@0: // todo: throw an exception if the root node name is not correctly configured (bc) Chris@0: Chris@0: if ($rootNode->hasChildNodes()) { Chris@0: $xpath = new \DOMXPath($dom); Chris@17: $data = []; Chris@0: foreach ($xpath->query('namespace::*', $dom->documentElement) as $nsNode) { Chris@0: $data['@'.$nsNode->nodeName] = $nsNode->nodeValue; Chris@0: } Chris@0: Chris@0: unset($data['@xmlns:xml']); Chris@0: Chris@0: if (empty($data)) { Chris@14: return $this->parseXml($rootNode, $context); Chris@0: } Chris@0: Chris@14: return array_merge($data, (array) $this->parseXml($rootNode, $context)); Chris@0: } Chris@0: Chris@0: if (!$rootNode->hasAttributes()) { Chris@0: return $rootNode->nodeValue; Chris@0: } Chris@0: Chris@17: $data = []; Chris@0: Chris@0: foreach ($rootNode->attributes as $attrKey => $attr) { Chris@0: $data['@'.$attrKey] = $attr->nodeValue; Chris@0: } Chris@0: Chris@0: $data['#'] = $rootNode->nodeValue; Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supportsEncoding($format) Chris@0: { Chris@14: return self::FORMAT === $format; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function supportsDecoding($format) Chris@0: { Chris@14: return self::FORMAT === $format; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the root node name. Chris@0: * Chris@14: * @param string $name Root node name Chris@0: */ Chris@0: public function setRootNodeName($name) Chris@0: { Chris@0: $this->rootNodeName = $name; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the root node name. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function getRootNodeName() Chris@0: { Chris@0: return $this->rootNodeName; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param \DOMNode $node Chris@0: * @param string $val Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: final protected function appendXMLString(\DOMNode $node, $val) Chris@0: { Chris@14: if (\strlen($val) > 0) { Chris@0: $frag = $this->dom->createDocumentFragment(); Chris@0: $frag->appendXML($val); Chris@0: $node->appendChild($frag); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param \DOMNode $node Chris@0: * @param string $val Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: final protected function appendText(\DOMNode $node, $val) Chris@0: { Chris@0: $nodeText = $this->dom->createTextNode($val); Chris@0: $node->appendChild($nodeText); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param \DOMNode $node Chris@0: * @param string $val Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: final protected function appendCData(\DOMNode $node, $val) Chris@0: { Chris@0: $nodeText = $this->dom->createCDATASection($val); Chris@0: $node->appendChild($nodeText); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param \DOMNode $node Chris@0: * @param \DOMDocumentFragment $fragment Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: final protected function appendDocumentFragment(\DOMNode $node, $fragment) Chris@0: { Chris@0: if ($fragment instanceof \DOMDocumentFragment) { Chris@0: $node->appendChild($fragment); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks the name is a valid xml element name. Chris@0: * Chris@0: * @param string $name Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: final protected function isElementNameValid($name) Chris@0: { Chris@0: return $name && Chris@0: false === strpos($name, ' ') && Chris@0: preg_match('#^[\pL_][\pL0-9._:-]*$#ui', $name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parse the input DOMNode into an array or a string. Chris@0: * Chris@0: * @return array|string Chris@0: */ Chris@17: private function parseXml(\DOMNode $node, array $context = []) Chris@0: { Chris@14: $data = $this->parseXmlAttributes($node, $context); Chris@0: Chris@14: $value = $this->parseXmlValue($node, $context); Chris@0: Chris@14: if (!\count($data)) { Chris@0: return $value; Chris@0: } Chris@0: Chris@14: if (!\is_array($value)) { Chris@0: $data['#'] = $value; Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@14: if (1 === \count($value) && key($value)) { Chris@0: $data[key($value)] = current($value); Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@0: foreach ($value as $key => $val) { Chris@0: $data[$key] = $val; Chris@0: } Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parse the input DOMNode attributes into an array. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@17: private function parseXmlAttributes(\DOMNode $node, array $context = []) Chris@0: { Chris@0: if (!$node->hasAttributes()) { Chris@17: return []; Chris@0: } Chris@0: Chris@17: $data = []; Chris@14: $typeCastAttributes = $this->resolveXmlTypeCastAttributes($context); Chris@0: Chris@0: foreach ($node->attributes as $attr) { Chris@14: if (!is_numeric($attr->nodeValue) || !$typeCastAttributes) { Chris@0: $data['@'.$attr->nodeName] = $attr->nodeValue; Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (false !== $val = filter_var($attr->nodeValue, FILTER_VALIDATE_INT)) { Chris@0: $data['@'.$attr->nodeName] = $val; Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@0: $data['@'.$attr->nodeName] = (float) $attr->nodeValue; Chris@0: } Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parse the input DOMNode value (content and children) into an array or a string. Chris@0: * Chris@0: * @return array|string Chris@0: */ Chris@17: private function parseXmlValue(\DOMNode $node, array $context = []) Chris@0: { Chris@0: if (!$node->hasChildNodes()) { Chris@0: return $node->nodeValue; Chris@0: } Chris@0: Chris@17: if (1 === $node->childNodes->length && \in_array($node->firstChild->nodeType, [XML_TEXT_NODE, XML_CDATA_SECTION_NODE])) { Chris@0: return $node->firstChild->nodeValue; Chris@0: } Chris@0: Chris@17: $value = []; Chris@0: Chris@0: foreach ($node->childNodes as $subnode) { Chris@14: if (XML_PI_NODE === $subnode->nodeType) { Chris@0: continue; Chris@0: } Chris@0: Chris@14: $val = $this->parseXml($subnode, $context); Chris@0: Chris@0: if ('item' === $subnode->nodeName && isset($val['@key'])) { Chris@0: if (isset($val['#'])) { Chris@0: $value[$val['@key']] = $val['#']; Chris@0: } else { Chris@0: $value[$val['@key']] = $val; Chris@0: } Chris@0: } else { Chris@0: $value[$subnode->nodeName][] = $val; Chris@0: } Chris@0: } Chris@0: Chris@0: foreach ($value as $key => $val) { Chris@14: if (\is_array($val) && 1 === \count($val)) { Chris@0: $value[$key] = current($val); Chris@0: } Chris@0: } Chris@0: Chris@0: return $value; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parse the data and convert it to DOMElements. Chris@0: * Chris@0: * @param \DOMNode $parentNode Chris@0: * @param array|object $data Chris@0: * @param string|null $xmlRootNodeName Chris@0: * Chris@0: * @return bool Chris@0: * Chris@14: * @throws NotEncodableValueException Chris@0: */ Chris@0: private function buildXml(\DOMNode $parentNode, $data, $xmlRootNodeName = null) Chris@0: { Chris@0: $append = true; Chris@0: Chris@14: if (\is_array($data) || ($data instanceof \Traversable && !$this->serializer->supportsNormalization($data, $this->format))) { Chris@0: foreach ($data as $key => $data) { Chris@0: //Ah this is the magic @ attribute types. Chris@0: if (0 === strpos($key, '@') && $this->isElementNameValid($attributeName = substr($key, 1))) { Chris@0: if (!is_scalar($data)) { Chris@0: $data = $this->serializer->normalize($data, $this->format, $this->context); Chris@0: } Chris@0: $parentNode->setAttribute($attributeName, $data); Chris@14: } elseif ('#' === $key) { Chris@0: $append = $this->selectNodeType($parentNode, $data); Chris@14: } elseif (\is_array($data) && false === is_numeric($key)) { Chris@0: // Is this array fully numeric keys? Chris@0: if (ctype_digit(implode('', array_keys($data)))) { Chris@0: /* Chris@0: * Create nodes to append to $parentNode based on the $key of this array Chris@0: * Produces 01 Chris@17: * From ["item" => [0,1]];. Chris@0: */ Chris@0: foreach ($data as $subData) { Chris@0: $append = $this->appendNode($parentNode, $subData, $key); Chris@0: } Chris@0: } else { Chris@0: $append = $this->appendNode($parentNode, $data, $key); Chris@0: } Chris@0: } elseif (is_numeric($key) || !$this->isElementNameValid($key)) { Chris@0: $append = $this->appendNode($parentNode, $data, 'item', $key); Chris@14: } elseif (null !== $data || !isset($this->context['remove_empty_tags']) || false === $this->context['remove_empty_tags']) { Chris@0: $append = $this->appendNode($parentNode, $data, $key); Chris@0: } Chris@0: } Chris@0: Chris@0: return $append; Chris@0: } Chris@0: Chris@14: if (\is_object($data)) { Chris@0: $data = $this->serializer->normalize($data, $this->format, $this->context); Chris@0: if (null !== $data && !is_scalar($data)) { Chris@0: return $this->buildXml($parentNode, $data, $xmlRootNodeName); Chris@0: } Chris@0: Chris@0: // top level data object was normalized into a scalar Chris@0: if (!$parentNode->parentNode->parentNode) { Chris@0: $root = $parentNode->parentNode; Chris@0: $root->removeChild($parentNode); Chris@0: Chris@0: return $this->appendNode($root, $data, $xmlRootNodeName); Chris@0: } Chris@0: Chris@0: return $this->appendNode($parentNode, $data, 'data'); Chris@0: } Chris@0: Chris@14: throw new NotEncodableValueException(sprintf('An unexpected value could not be serialized: %s', var_export($data, true))); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Selects the type of node to create and appends it to the parent. Chris@0: * Chris@0: * @param \DOMNode $parentNode Chris@0: * @param array|object $data Chris@0: * @param string $nodeName Chris@0: * @param string $key Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: private function appendNode(\DOMNode $parentNode, $data, $nodeName, $key = null) Chris@0: { Chris@0: $node = $this->dom->createElement($nodeName); Chris@0: if (null !== $key) { Chris@0: $node->setAttribute('key', $key); Chris@0: } Chris@0: $appendNode = $this->selectNodeType($node, $data); Chris@0: // we may have decided not to append this node, either in error or if its $nodeName is not valid Chris@0: if ($appendNode) { Chris@0: $parentNode->appendChild($node); Chris@0: } Chris@0: Chris@0: return $appendNode; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if a value contains any characters which would require CDATA wrapping. Chris@0: * Chris@0: * @param string $val Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: private function needsCdataWrapping($val) Chris@0: { Chris@0: return 0 < preg_match('/[<>&]/', $val); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests the value being passed and decide what sort of element to create. Chris@0: * Chris@0: * @param \DOMNode $node Chris@0: * @param mixed $val Chris@0: * Chris@0: * @return bool Chris@0: * Chris@14: * @throws NotEncodableValueException Chris@0: */ Chris@0: private function selectNodeType(\DOMNode $node, $val) Chris@0: { Chris@14: if (\is_array($val)) { Chris@0: return $this->buildXml($node, $val); Chris@0: } elseif ($val instanceof \SimpleXMLElement) { Chris@0: $child = $this->dom->importNode(dom_import_simplexml($val), true); Chris@0: $node->appendChild($child); Chris@0: } elseif ($val instanceof \Traversable) { Chris@0: $this->buildXml($node, $val); Chris@14: } elseif (\is_object($val)) { Chris@0: return $this->selectNodeType($node, $this->serializer->normalize($val, $this->format, $this->context)); Chris@0: } elseif (is_numeric($val)) { Chris@0: return $this->appendText($node, (string) $val); Chris@14: } elseif (\is_string($val) && $this->needsCdataWrapping($val)) { Chris@0: return $this->appendCData($node, $val); Chris@14: } elseif (\is_string($val)) { Chris@0: return $this->appendText($node, $val); Chris@14: } elseif (\is_bool($val)) { Chris@0: return $this->appendText($node, (int) $val); Chris@0: } elseif ($val instanceof \DOMNode) { Chris@0: $child = $this->dom->importNode($val, true); Chris@0: $node->appendChild($child); Chris@0: } Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Get real XML root node name, taking serializer options into account. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@17: private function resolveXmlRootName(array $context = []) Chris@0: { Chris@0: return isset($context['xml_root_node_name']) Chris@0: ? $context['xml_root_node_name'] Chris@0: : $this->rootNodeName; Chris@0: } Chris@0: Chris@0: /** Chris@14: * Get XML option for type casting attributes Defaults to true. Chris@14: * Chris@14: * @param array $context Chris@14: * Chris@14: * @return bool Chris@14: */ Chris@17: private function resolveXmlTypeCastAttributes(array $context = []) Chris@14: { Chris@14: return isset($context['xml_type_cast_attributes']) Chris@14: ? (bool) $context['xml_type_cast_attributes'] Chris@14: : true; Chris@14: } Chris@14: Chris@14: /** Chris@0: * Create a DOM document, taking serializer options into account. Chris@0: * Chris@14: * @param array $context Options that the encoder has access to Chris@0: * Chris@0: * @return \DOMDocument Chris@0: */ Chris@0: private function createDomDocument(array $context) Chris@0: { Chris@0: $document = new \DOMDocument(); Chris@0: Chris@0: // Set an attribute on the DOM document specifying, as part of the XML declaration, Chris@17: $xmlOptions = [ Chris@0: // nicely formats output with indentation and extra space Chris@0: 'xml_format_output' => 'formatOutput', Chris@0: // the version number of the document Chris@0: 'xml_version' => 'xmlVersion', Chris@0: // the encoding of the document Chris@0: 'xml_encoding' => 'encoding', Chris@0: // whether the document is standalone Chris@0: 'xml_standalone' => 'xmlStandalone', Chris@17: ]; Chris@0: foreach ($xmlOptions as $xmlOption => $documentProperty) { Chris@0: if (isset($context[$xmlOption])) { Chris@0: $document->$documentProperty = $context[$xmlOption]; Chris@0: } Chris@0: } Chris@0: Chris@0: return $document; Chris@0: } Chris@0: }