Cake/Utility/Xml.php

1 <?php
2 /**
3 * XML handling for Cake.
4 *
5 * The methods in these classes enable the datasources that use XML to work.
6 *
7 * PHP 5
8 *
9 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10 * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
11 *
12 * Licensed under The MIT License
13 * Redistributions of files must retain the above copyright notice.
14 *
15 * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
16 * @link http://cakephp.org CakePHP(tm) Project
17 * @package Cake.Utility
18 * @since CakePHP v .0.10.3.1400
19 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
20 */
21  
22 /**
23 * XML handling for Cake.
24 *
25 * The methods in these classes enable the datasources that use XML to work.
26 *
27 * @package Cake.Utility
28 */
29 class Xml {
30  
31 /**
32 * Initialize SimpleXMLElement or DOMDocument from a given XML string, file path, URL or array.
33 *
34 * ### Usage:
35 *
36 * Building XML from a string:
37 *
38 * `$xml = Xml::build('<example>text</example>');`
39 *
40 * Building XML from string (output DOMDocument):
41 *
42 * `$xml = Xml::build('<example>text</example>', array('return' => 'domdocument'));`
43 *
44 * Building XML from a file path:
45 *
46 * `$xml = Xml::build('/path/to/an/xml/file.xml');`
47 *
48 * Building from a remote URL:
49 *
50 * `$xml = Xml::build('http://example.com/example.xml');`
51 *
52 * Building from an array:
53 *
54 * {{{
55 * $value = array(
56 * 'tags' => array(
57 * 'tag' => array(
58 * array(
59 * 'id' => '1',
60 * 'name' => 'defect'
61 * ),
62 * array(
63 * 'id' => '2',
64 * 'name' => 'enhancement'
65 * )
66 * )
67 * )
68 * );
69 * $xml = Xml::build($value);
70 * }}}
71 *
72 * When building XML from an array ensure that there is only one top level element.
73 *
74 * ### Options
75 *
76 * - `return` Can be 'simplexml' to return object of SimpleXMLElement or 'domdocument' to return DOMDocument.
77 * - If using array as input, you can pass `options` from Xml::fromArray.
78 *
79 * @param mixed $input XML string, a path to a file, an URL or an array
80 * @param array $options The options to use
81 * @return SimpleXMLElement|DOMDocument SimpleXMLElement or DOMDocument
82 * @throws XmlException
83 */
84 public static function build($input, $options = array()) {
85 if (!is_array($options)) {
86 $options = array('return' => (string)$options);
87 }
88 $defaults = array(
89 'return' => 'simplexml'
90 );
91 $options = array_merge($defaults, $options);
92  
93 if (is_array($input) || is_object($input)) {
94 return self::fromArray((array)$input, $options);
95 } elseif (strpos($input, '<') !== false) {
96 if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
97 return new SimpleXMLElement($input, LIBXML_NOCDATA);
98 }
99 $dom = new DOMDocument();
100 $dom->loadXML($input);
101 return $dom;
102 } elseif (file_exists($input) || strpos($input, 'http://') === 0 || strpos($input, 'https://') === 0) {
103 if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
104 return new SimpleXMLElement($input, LIBXML_NOCDATA, true);
105 }
106 $dom = new DOMDocument();
107 $dom->load($input);
108 return $dom;
109 } elseif (!is_string($input)) {
110 throw new XmlException(__d('cake_dev', 'Invalid input.'));
111 }
112 throw new XmlException(__d('cake_dev', 'XML cannot be read.'));
113 }
114  
115 /**
116 * Transform an array into a SimpleXMLElement
117 *
118 * ### Options
119 *
120 * - `format` If create childs ('tags') or attributes ('attribute').
121 * - `version` Version of XML document. Default is 1.0.
122 * - `encoding` Encoding of XML document. If null remove from XML header. Default is the some of application.
123 * - `return` If return object of SimpleXMLElement ('simplexml') or DOMDocument ('domdocument'). Default is SimpleXMLElement.
124 *
125 * Using the following data:
126 *
127 * {{{
128 * $value = array(
129 * 'root' => array(
130 * 'tag' => array(
131 * 'id' => 1,
132 * 'value' => 'defect',
133 * '@' => 'description'
134 * )
135 * )
136 * );
137 * }}}
138 *
139 * Calling `Xml::fromArray($value, 'tags');` Will generate:
140 *
141 * `<root><tag><id>1</id><value>defect</value>description</tag></root>`
142 *
143 * And calling `Xml::fromArray($value, 'attribute');` Will generate:
144 *
145 * `<root><tag id="1" value="defect">description</tag></root>`
146 *
147 * @param array $input Array with data
148 * @param array $options The options to use
149 * @return SimpleXMLElement|DOMDocument SimpleXMLElement or DOMDocument
150 * @throws XmlException
151 */
152 public static function fromArray($input, $options = array()) {
153 if (!is_array($input) || count($input) !== 1) {
154 throw new XmlException(__d('cake_dev', 'Invalid input.'));
155 }
156 $key = key($input);
157 if (is_integer($key)) {
158 throw new XmlException(__d('cake_dev', 'The key of input must be alphanumeric'));
159 }
160  
161 if (!is_array($options)) {
162 $options = array('format' => (string)$options);
163 }
164 $defaults = array(
165 'format' => 'tags',
166 'version' => '1.0',
167 'encoding' => Configure::read('App.encoding'),
168 'return' => 'simplexml'
169 );
170 $options = array_merge($defaults, $options);
171  
172 $dom = new DOMDocument($options['version'], $options['encoding']);
173 self::_fromArray($dom, $dom, $input, $options['format']);
174  
175 $options['return'] = strtolower($options['return']);
176 if ($options['return'] === 'simplexml' || $options['return'] === 'simplexmlelement') {
177 return new SimpleXMLElement($dom->saveXML());
178 }
179 return $dom;
180 }
181  
182 /**
183 * Recursive method to create childs from array
184 *
185 * @param DOMDocument $dom Handler to DOMDocument
186 * @param DOMElement $node Handler to DOMElement (child)
187 * @param array $data Array of data to append to the $node.
188 * @param string $format Either 'attribute' or 'tags'. This determines where nested keys go.
189 * @return void
190 * @throws XmlException
191 */
192 protected static function _fromArray($dom, $node, &$data, $format) {
193 if (empty($data) || !is_array($data)) {
194 return;
195 }
196 foreach ($data as $key => $value) {
197 if (is_string($key)) {
198 if (!is_array($value)) {
199 if (is_bool($value)) {
200 $value = (int)$value;
201 } elseif ($value === null) {
202 $value = '';
203 }
204 $isNamespace = strpos($key, 'xmlns:');
205 if ($isNamespace !== false) {
206 $node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, $value);
207 continue;
208 }
209 if ($key[0] !== '@' && $format === 'tags') {
210 $child = null;
211 if (!is_numeric($value)) {
212 // Escape special characters
213 // http://www.w3.org/TR/REC-xml/#syntax
214 // https://bugs.php.net/bug.php?id=36795
215 $child = $dom->createElement($key, '');
216 $child->appendChild(new DOMText($value));
217 } else {
218 $child = $dom->createElement($key, $value);
219 }
220 $node->appendChild($child);
221 } else {
222 if ($key[0] === '@') {
223 $key = substr($key, 1);
224 }
225 $attribute = $dom->createAttribute($key);
226 $attribute->appendChild($dom->createTextNode($value));
227 $node->appendChild($attribute);
228 }
229 } else {
230 if ($key[0] === '@') {
231 throw new XmlException(__d('cake_dev', 'Invalid array'));
232 }
233 if (is_numeric(implode('', array_keys($value)))) { // List
234 foreach ($value as $item) {
235 $itemData = compact('dom', 'node', 'key', 'format');
236 $itemData['value'] = $item;
237 self::_createChild($itemData);
238 }
239 } else { // Struct
240 self::_createChild(compact('dom', 'node', 'key', 'value', 'format'));
241 }
242 }
243 } else {
244 throw new XmlException(__d('cake_dev', 'Invalid array'));
245 }
246 }
247 }
248  
249 /**
250 * Helper to _fromArray(). It will create childs of arrays
251 *
252 * @param array $data Array with informations to create childs
253 * @return void
254 */
255 protected static function _createChild($data) {
256 extract($data);
257 $childNS = $childValue = null;
258 if (is_array($value)) {
259 if (isset($value['@'])) {
260 $childValue = (string)$value['@'];
261 unset($value['@']);
262 }
263 if (isset($value['xmlns:'])) {
264 $childNS = $value['xmlns:'];
265 unset($value['xmlns:']);
266 }
267 } elseif (!empty($value) || $value === 0) {
268 $childValue = (string)$value;
269 }
270  
271 if ($childValue) {
272 $child = $dom->createElement($key, $childValue);
273 } else {
274 $child = $dom->createElement($key);
275 }
276 if ($childNS) {
277 $child->setAttribute('xmlns', $childNS);
278 }
279  
280 self::_fromArray($dom, $child, $value, $format);
281 $node->appendChild($child);
282 }
283  
284 /**
285 * Returns this XML structure as a array.
286 *
287 * @param SimpleXMLElement|DOMDocument|DOMNode $obj SimpleXMLElement, DOMDocument or DOMNode instance
288 * @return array Array representation of the XML structure.
289 * @throws XmlException
290 */
291 public static function toArray($obj) {
292 if ($obj instanceof DOMNode) {
293 $obj = simplexml_import_dom($obj);
294 }
295 if (!($obj instanceof SimpleXMLElement)) {
296 throw new XmlException(__d('cake_dev', 'The input is not instance of SimpleXMLElement, DOMDocument or DOMNode.'));
297 }
298 $result = array();
299 $namespaces = array_merge(array('' => ''), $obj->getNamespaces(true));
300 self::_toArray($obj, $result, '', array_keys($namespaces));
301 return $result;
302 }
303  
304 /**
305 * Recursive method to toArray
306 *
307 * @param SimpleXMLElement $xml SimpleXMLElement object
308 * @param array $parentData Parent array with data
309 * @param string $ns Namespace of current child
310 * @param array $namespaces List of namespaces in XML
311 * @return void
312 */
313 protected static function _toArray($xml, &$parentData, $ns, $namespaces) {
314 $data = array();
315  
316 foreach ($namespaces as $namespace) {
317 foreach ($xml->attributes($namespace, true) as $key => $value) {
318 if (!empty($namespace)) {
319 $key = $namespace . ':' . $key;
320 }
321 $data['@' . $key] = (string)$value;
322 }
323  
324 foreach ($xml->children($namespace, true) as $child) {
325 self::_toArray($child, $data, $namespace, $namespaces);
326 }
327 }
328  
329 $asString = trim((string)$xml);
330 if (empty($data)) {
331 $data = $asString;
332 } elseif (!empty($asString)) {
333 $data['@'] = $asString;
334 }
335  
336 if (!empty($ns)) {
337 $ns .= ':';
338 }
339 $name = $ns . $xml->getName();
340 if (isset($parentData[$name])) {
341 if (!is_array($parentData[$name]) || !isset($parentData[$name][0])) {
342 $parentData[$name] = array($parentData[$name]);
343 }
344 $parentData[$name][] = $data;
345 } else {
346 $parentData[$name] = $data;
347 }
348 }
349  
350 }
351  
352