Xml.php 9.5 KB

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