Mercurial > hg > isophonics-drupal-site
comparison vendor/symfony/serializer/Encoder/XmlEncoder.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 <?php | |
2 | |
3 /* | |
4 * This file is part of the Symfony package. | |
5 * | |
6 * (c) Fabien Potencier <fabien@symfony.com> | |
7 * | |
8 * For the full copyright and license information, please view the LICENSE | |
9 * file that was distributed with this source code. | |
10 */ | |
11 | |
12 namespace Symfony\Component\Serializer\Encoder; | |
13 | |
14 use Symfony\Component\Serializer\Exception\UnexpectedValueException; | |
15 | |
16 /** | |
17 * Encodes XML data. | |
18 * | |
19 * @author Jordi Boggiano <j.boggiano@seld.be> | |
20 * @author John Wards <jwards@whiteoctober.co.uk> | |
21 * @author Fabian Vogler <fabian@equivalence.ch> | |
22 * @author Kévin Dunglas <dunglas@gmail.com> | |
23 */ | |
24 class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface | |
25 { | |
26 /** | |
27 * @var \DOMDocument | |
28 */ | |
29 private $dom; | |
30 private $format; | |
31 private $context; | |
32 private $rootNodeName = 'response'; | |
33 private $loadOptions; | |
34 | |
35 /** | |
36 * Construct new XmlEncoder and allow to change the root node element name. | |
37 * | |
38 * @param string $rootNodeName | |
39 * @param int|null $loadOptions A bit field of LIBXML_* constants | |
40 */ | |
41 public function __construct($rootNodeName = 'response', $loadOptions = null) | |
42 { | |
43 $this->rootNodeName = $rootNodeName; | |
44 $this->loadOptions = null !== $loadOptions ? $loadOptions : LIBXML_NONET | LIBXML_NOBLANKS; | |
45 } | |
46 | |
47 /** | |
48 * {@inheritdoc} | |
49 */ | |
50 public function encode($data, $format, array $context = array()) | |
51 { | |
52 if ($data instanceof \DOMDocument) { | |
53 return $data->saveXML(); | |
54 } | |
55 | |
56 $xmlRootNodeName = $this->resolveXmlRootName($context); | |
57 | |
58 $this->dom = $this->createDomDocument($context); | |
59 $this->format = $format; | |
60 $this->context = $context; | |
61 | |
62 if (null !== $data && !is_scalar($data)) { | |
63 $root = $this->dom->createElement($xmlRootNodeName); | |
64 $this->dom->appendChild($root); | |
65 $this->buildXml($root, $data, $xmlRootNodeName); | |
66 } else { | |
67 $this->appendNode($this->dom, $data, $xmlRootNodeName); | |
68 } | |
69 | |
70 return $this->dom->saveXML(); | |
71 } | |
72 | |
73 /** | |
74 * {@inheritdoc} | |
75 */ | |
76 public function decode($data, $format, array $context = array()) | |
77 { | |
78 if ('' === trim($data)) { | |
79 throw new UnexpectedValueException('Invalid XML data, it can not be empty.'); | |
80 } | |
81 | |
82 $internalErrors = libxml_use_internal_errors(true); | |
83 $disableEntities = libxml_disable_entity_loader(true); | |
84 libxml_clear_errors(); | |
85 | |
86 $dom = new \DOMDocument(); | |
87 $dom->loadXML($data, $this->loadOptions); | |
88 | |
89 libxml_use_internal_errors($internalErrors); | |
90 libxml_disable_entity_loader($disableEntities); | |
91 | |
92 if ($error = libxml_get_last_error()) { | |
93 libxml_clear_errors(); | |
94 | |
95 throw new UnexpectedValueException($error->message); | |
96 } | |
97 | |
98 $rootNode = null; | |
99 foreach ($dom->childNodes as $child) { | |
100 if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { | |
101 throw new UnexpectedValueException('Document types are not allowed.'); | |
102 } | |
103 if (!$rootNode && $child->nodeType !== XML_PI_NODE) { | |
104 $rootNode = $child; | |
105 } | |
106 } | |
107 | |
108 // todo: throw an exception if the root node name is not correctly configured (bc) | |
109 | |
110 if ($rootNode->hasChildNodes()) { | |
111 $xpath = new \DOMXPath($dom); | |
112 $data = array(); | |
113 foreach ($xpath->query('namespace::*', $dom->documentElement) as $nsNode) { | |
114 $data['@'.$nsNode->nodeName] = $nsNode->nodeValue; | |
115 } | |
116 | |
117 unset($data['@xmlns:xml']); | |
118 | |
119 if (empty($data)) { | |
120 return $this->parseXml($rootNode); | |
121 } | |
122 | |
123 return array_merge($data, (array) $this->parseXml($rootNode)); | |
124 } | |
125 | |
126 if (!$rootNode->hasAttributes()) { | |
127 return $rootNode->nodeValue; | |
128 } | |
129 | |
130 $data = array(); | |
131 | |
132 foreach ($rootNode->attributes as $attrKey => $attr) { | |
133 $data['@'.$attrKey] = $attr->nodeValue; | |
134 } | |
135 | |
136 $data['#'] = $rootNode->nodeValue; | |
137 | |
138 return $data; | |
139 } | |
140 | |
141 /** | |
142 * {@inheritdoc} | |
143 */ | |
144 public function supportsEncoding($format) | |
145 { | |
146 return 'xml' === $format; | |
147 } | |
148 | |
149 /** | |
150 * {@inheritdoc} | |
151 */ | |
152 public function supportsDecoding($format) | |
153 { | |
154 return 'xml' === $format; | |
155 } | |
156 | |
157 /** | |
158 * Sets the root node name. | |
159 * | |
160 * @param string $name root node name | |
161 */ | |
162 public function setRootNodeName($name) | |
163 { | |
164 $this->rootNodeName = $name; | |
165 } | |
166 | |
167 /** | |
168 * Returns the root node name. | |
169 * | |
170 * @return string | |
171 */ | |
172 public function getRootNodeName() | |
173 { | |
174 return $this->rootNodeName; | |
175 } | |
176 | |
177 /** | |
178 * @param \DOMNode $node | |
179 * @param string $val | |
180 * | |
181 * @return bool | |
182 */ | |
183 final protected function appendXMLString(\DOMNode $node, $val) | |
184 { | |
185 if (strlen($val) > 0) { | |
186 $frag = $this->dom->createDocumentFragment(); | |
187 $frag->appendXML($val); | |
188 $node->appendChild($frag); | |
189 | |
190 return true; | |
191 } | |
192 | |
193 return false; | |
194 } | |
195 | |
196 /** | |
197 * @param \DOMNode $node | |
198 * @param string $val | |
199 * | |
200 * @return bool | |
201 */ | |
202 final protected function appendText(\DOMNode $node, $val) | |
203 { | |
204 $nodeText = $this->dom->createTextNode($val); | |
205 $node->appendChild($nodeText); | |
206 | |
207 return true; | |
208 } | |
209 | |
210 /** | |
211 * @param \DOMNode $node | |
212 * @param string $val | |
213 * | |
214 * @return bool | |
215 */ | |
216 final protected function appendCData(\DOMNode $node, $val) | |
217 { | |
218 $nodeText = $this->dom->createCDATASection($val); | |
219 $node->appendChild($nodeText); | |
220 | |
221 return true; | |
222 } | |
223 | |
224 /** | |
225 * @param \DOMNode $node | |
226 * @param \DOMDocumentFragment $fragment | |
227 * | |
228 * @return bool | |
229 */ | |
230 final protected function appendDocumentFragment(\DOMNode $node, $fragment) | |
231 { | |
232 if ($fragment instanceof \DOMDocumentFragment) { | |
233 $node->appendChild($fragment); | |
234 | |
235 return true; | |
236 } | |
237 | |
238 return false; | |
239 } | |
240 | |
241 /** | |
242 * Checks the name is a valid xml element name. | |
243 * | |
244 * @param string $name | |
245 * | |
246 * @return bool | |
247 */ | |
248 final protected function isElementNameValid($name) | |
249 { | |
250 return $name && | |
251 false === strpos($name, ' ') && | |
252 preg_match('#^[\pL_][\pL0-9._:-]*$#ui', $name); | |
253 } | |
254 | |
255 /** | |
256 * Parse the input DOMNode into an array or a string. | |
257 * | |
258 * @param \DOMNode $node xml to parse | |
259 * | |
260 * @return array|string | |
261 */ | |
262 private function parseXml(\DOMNode $node) | |
263 { | |
264 $data = $this->parseXmlAttributes($node); | |
265 | |
266 $value = $this->parseXmlValue($node); | |
267 | |
268 if (!count($data)) { | |
269 return $value; | |
270 } | |
271 | |
272 if (!is_array($value)) { | |
273 $data['#'] = $value; | |
274 | |
275 return $data; | |
276 } | |
277 | |
278 if (1 === count($value) && key($value)) { | |
279 $data[key($value)] = current($value); | |
280 | |
281 return $data; | |
282 } | |
283 | |
284 foreach ($value as $key => $val) { | |
285 $data[$key] = $val; | |
286 } | |
287 | |
288 return $data; | |
289 } | |
290 | |
291 /** | |
292 * Parse the input DOMNode attributes into an array. | |
293 * | |
294 * @param \DOMNode $node xml to parse | |
295 * | |
296 * @return array | |
297 */ | |
298 private function parseXmlAttributes(\DOMNode $node) | |
299 { | |
300 if (!$node->hasAttributes()) { | |
301 return array(); | |
302 } | |
303 | |
304 $data = array(); | |
305 | |
306 foreach ($node->attributes as $attr) { | |
307 if (!is_numeric($attr->nodeValue)) { | |
308 $data['@'.$attr->nodeName] = $attr->nodeValue; | |
309 | |
310 continue; | |
311 } | |
312 | |
313 if (false !== $val = filter_var($attr->nodeValue, FILTER_VALIDATE_INT)) { | |
314 $data['@'.$attr->nodeName] = $val; | |
315 | |
316 continue; | |
317 } | |
318 | |
319 $data['@'.$attr->nodeName] = (float) $attr->nodeValue; | |
320 } | |
321 | |
322 return $data; | |
323 } | |
324 | |
325 /** | |
326 * Parse the input DOMNode value (content and children) into an array or a string. | |
327 * | |
328 * @param \DOMNode $node xml to parse | |
329 * | |
330 * @return array|string | |
331 */ | |
332 private function parseXmlValue(\DOMNode $node) | |
333 { | |
334 if (!$node->hasChildNodes()) { | |
335 return $node->nodeValue; | |
336 } | |
337 | |
338 if (1 === $node->childNodes->length && in_array($node->firstChild->nodeType, array(XML_TEXT_NODE, XML_CDATA_SECTION_NODE))) { | |
339 return $node->firstChild->nodeValue; | |
340 } | |
341 | |
342 $value = array(); | |
343 | |
344 foreach ($node->childNodes as $subnode) { | |
345 if ($subnode->nodeType === XML_PI_NODE) { | |
346 continue; | |
347 } | |
348 | |
349 $val = $this->parseXml($subnode); | |
350 | |
351 if ('item' === $subnode->nodeName && isset($val['@key'])) { | |
352 if (isset($val['#'])) { | |
353 $value[$val['@key']] = $val['#']; | |
354 } else { | |
355 $value[$val['@key']] = $val; | |
356 } | |
357 } else { | |
358 $value[$subnode->nodeName][] = $val; | |
359 } | |
360 } | |
361 | |
362 foreach ($value as $key => $val) { | |
363 if (is_array($val) && 1 === count($val)) { | |
364 $value[$key] = current($val); | |
365 } | |
366 } | |
367 | |
368 return $value; | |
369 } | |
370 | |
371 /** | |
372 * Parse the data and convert it to DOMElements. | |
373 * | |
374 * @param \DOMNode $parentNode | |
375 * @param array|object $data | |
376 * @param string|null $xmlRootNodeName | |
377 * | |
378 * @return bool | |
379 * | |
380 * @throws UnexpectedValueException | |
381 */ | |
382 private function buildXml(\DOMNode $parentNode, $data, $xmlRootNodeName = null) | |
383 { | |
384 $append = true; | |
385 | |
386 if (is_array($data) || ($data instanceof \Traversable && !$this->serializer->supportsNormalization($data, $this->format))) { | |
387 foreach ($data as $key => $data) { | |
388 //Ah this is the magic @ attribute types. | |
389 if (0 === strpos($key, '@') && $this->isElementNameValid($attributeName = substr($key, 1))) { | |
390 if (!is_scalar($data)) { | |
391 $data = $this->serializer->normalize($data, $this->format, $this->context); | |
392 } | |
393 $parentNode->setAttribute($attributeName, $data); | |
394 } elseif ($key === '#') { | |
395 $append = $this->selectNodeType($parentNode, $data); | |
396 } elseif (is_array($data) && false === is_numeric($key)) { | |
397 // Is this array fully numeric keys? | |
398 if (ctype_digit(implode('', array_keys($data)))) { | |
399 /* | |
400 * Create nodes to append to $parentNode based on the $key of this array | |
401 * Produces <xml><item>0</item><item>1</item></xml> | |
402 * From array("item" => array(0,1));. | |
403 */ | |
404 foreach ($data as $subData) { | |
405 $append = $this->appendNode($parentNode, $subData, $key); | |
406 } | |
407 } else { | |
408 $append = $this->appendNode($parentNode, $data, $key); | |
409 } | |
410 } elseif (is_numeric($key) || !$this->isElementNameValid($key)) { | |
411 $append = $this->appendNode($parentNode, $data, 'item', $key); | |
412 } else { | |
413 $append = $this->appendNode($parentNode, $data, $key); | |
414 } | |
415 } | |
416 | |
417 return $append; | |
418 } | |
419 | |
420 if (is_object($data)) { | |
421 $data = $this->serializer->normalize($data, $this->format, $this->context); | |
422 if (null !== $data && !is_scalar($data)) { | |
423 return $this->buildXml($parentNode, $data, $xmlRootNodeName); | |
424 } | |
425 | |
426 // top level data object was normalized into a scalar | |
427 if (!$parentNode->parentNode->parentNode) { | |
428 $root = $parentNode->parentNode; | |
429 $root->removeChild($parentNode); | |
430 | |
431 return $this->appendNode($root, $data, $xmlRootNodeName); | |
432 } | |
433 | |
434 return $this->appendNode($parentNode, $data, 'data'); | |
435 } | |
436 | |
437 throw new UnexpectedValueException(sprintf('An unexpected value could not be serialized: %s', var_export($data, true))); | |
438 } | |
439 | |
440 /** | |
441 * Selects the type of node to create and appends it to the parent. | |
442 * | |
443 * @param \DOMNode $parentNode | |
444 * @param array|object $data | |
445 * @param string $nodeName | |
446 * @param string $key | |
447 * | |
448 * @return bool | |
449 */ | |
450 private function appendNode(\DOMNode $parentNode, $data, $nodeName, $key = null) | |
451 { | |
452 $node = $this->dom->createElement($nodeName); | |
453 if (null !== $key) { | |
454 $node->setAttribute('key', $key); | |
455 } | |
456 $appendNode = $this->selectNodeType($node, $data); | |
457 // we may have decided not to append this node, either in error or if its $nodeName is not valid | |
458 if ($appendNode) { | |
459 $parentNode->appendChild($node); | |
460 } | |
461 | |
462 return $appendNode; | |
463 } | |
464 | |
465 /** | |
466 * Checks if a value contains any characters which would require CDATA wrapping. | |
467 * | |
468 * @param string $val | |
469 * | |
470 * @return bool | |
471 */ | |
472 private function needsCdataWrapping($val) | |
473 { | |
474 return 0 < preg_match('/[<>&]/', $val); | |
475 } | |
476 | |
477 /** | |
478 * Tests the value being passed and decide what sort of element to create. | |
479 * | |
480 * @param \DOMNode $node | |
481 * @param mixed $val | |
482 * | |
483 * @return bool | |
484 * | |
485 * @throws UnexpectedValueException | |
486 */ | |
487 private function selectNodeType(\DOMNode $node, $val) | |
488 { | |
489 if (is_array($val)) { | |
490 return $this->buildXml($node, $val); | |
491 } elseif ($val instanceof \SimpleXMLElement) { | |
492 $child = $this->dom->importNode(dom_import_simplexml($val), true); | |
493 $node->appendChild($child); | |
494 } elseif ($val instanceof \Traversable) { | |
495 $this->buildXml($node, $val); | |
496 } elseif (is_object($val)) { | |
497 return $this->selectNodeType($node, $this->serializer->normalize($val, $this->format, $this->context)); | |
498 } elseif (is_numeric($val)) { | |
499 return $this->appendText($node, (string) $val); | |
500 } elseif (is_string($val) && $this->needsCdataWrapping($val)) { | |
501 return $this->appendCData($node, $val); | |
502 } elseif (is_string($val)) { | |
503 return $this->appendText($node, $val); | |
504 } elseif (is_bool($val)) { | |
505 return $this->appendText($node, (int) $val); | |
506 } elseif ($val instanceof \DOMNode) { | |
507 $child = $this->dom->importNode($val, true); | |
508 $node->appendChild($child); | |
509 } | |
510 | |
511 return true; | |
512 } | |
513 | |
514 /** | |
515 * Get real XML root node name, taking serializer options into account. | |
516 * | |
517 * @param array $context | |
518 * | |
519 * @return string | |
520 */ | |
521 private function resolveXmlRootName(array $context = array()) | |
522 { | |
523 return isset($context['xml_root_node_name']) | |
524 ? $context['xml_root_node_name'] | |
525 : $this->rootNodeName; | |
526 } | |
527 | |
528 /** | |
529 * Create a DOM document, taking serializer options into account. | |
530 * | |
531 * @param array $context options that the encoder has access to | |
532 * | |
533 * @return \DOMDocument | |
534 */ | |
535 private function createDomDocument(array $context) | |
536 { | |
537 $document = new \DOMDocument(); | |
538 | |
539 // Set an attribute on the DOM document specifying, as part of the XML declaration, | |
540 $xmlOptions = array( | |
541 // nicely formats output with indentation and extra space | |
542 'xml_format_output' => 'formatOutput', | |
543 // the version number of the document | |
544 'xml_version' => 'xmlVersion', | |
545 // the encoding of the document | |
546 'xml_encoding' => 'encoding', | |
547 // whether the document is standalone | |
548 'xml_standalone' => 'xmlStandalone', | |
549 ); | |
550 foreach ($xmlOptions as $xmlOption => $documentProperty) { | |
551 if (isset($context[$xmlOption])) { | |
552 $document->$documentProperty = $context[$xmlOption]; | |
553 } | |
554 } | |
555 | |
556 return $document; | |
557 } | |
558 } |