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