XmlLocation.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <?php
  2. namespace GuzzleHttp\Command\Guzzle\ResponseLocation;
  3. use GuzzleHttp\Command\Guzzle\Parameter;
  4. use GuzzleHttp\Command\Result;
  5. use GuzzleHttp\Command\ResultInterface;
  6. use Psr\Http\Message\ResponseInterface;
  7. /**
  8. * Extracts elements from an XML document
  9. */
  10. class XmlLocation extends AbstractLocation
  11. {
  12. /** @var \SimpleXMLElement XML document being visited */
  13. private $xml;
  14. /**
  15. * Set the name of the location
  16. *
  17. * @param string $locationName
  18. */
  19. public function __construct($locationName = 'xml')
  20. {
  21. parent::__construct($locationName);
  22. }
  23. /**
  24. * @param ResultInterface $result
  25. * @param ResponseInterface $response
  26. * @param Parameter $model
  27. * @return ResultInterface
  28. */
  29. public function before(
  30. ResultInterface $result,
  31. ResponseInterface $response,
  32. Parameter $model
  33. ) {
  34. $this->xml = simplexml_load_string((string) $response->getBody());
  35. return $result;
  36. }
  37. /**
  38. * @param ResultInterface $result
  39. * @param ResponseInterface $response
  40. * @param Parameter $model
  41. * @return Result|ResultInterface
  42. */
  43. public function after(
  44. ResultInterface $result,
  45. ResponseInterface $response,
  46. Parameter $model
  47. ) {
  48. // Handle additional, undefined properties
  49. $additional = $model->getAdditionalProperties();
  50. if ($additional instanceof Parameter &&
  51. $additional->getLocation() == $this->locationName
  52. ) {
  53. $result = new Result(array_merge(
  54. $result->toArray(),
  55. self::xmlToArray($this->xml)
  56. ));
  57. }
  58. $this->xml = null;
  59. return $result;
  60. }
  61. /**
  62. * @param ResultInterface $result
  63. * @param ResponseInterface $response
  64. * @param Parameter $param
  65. * @return ResultInterface
  66. */
  67. public function visit(
  68. ResultInterface $result,
  69. ResponseInterface $response,
  70. Parameter $param
  71. ) {
  72. $sentAs = $param->getWireName();
  73. $ns = null;
  74. if (strstr($sentAs, ':')) {
  75. list($ns, $sentAs) = explode(':', $sentAs);
  76. }
  77. // Process the primary property
  78. if (count($this->xml->children($ns, true)->{$sentAs})) {
  79. $result[$param->getName()] = $this->recursiveProcess(
  80. $param,
  81. $this->xml->children($ns, true)->{$sentAs}
  82. );
  83. }
  84. return $result;
  85. }
  86. /**
  87. * Recursively process a parameter while applying filters
  88. *
  89. * @param Parameter $param API parameter being processed
  90. * @param \SimpleXMLElement $node Node being processed
  91. * @return array
  92. */
  93. private function recursiveProcess(
  94. Parameter $param,
  95. \SimpleXMLElement $node
  96. ) {
  97. $result = [];
  98. $type = $param->getType();
  99. if ($type == 'object') {
  100. $result = $this->processObject($param, $node);
  101. } elseif ($type == 'array') {
  102. $result = $this->processArray($param, $node);
  103. } else {
  104. // We are probably handling a flat data node (i.e. string or
  105. // integer), so let's check if it's childless, which indicates a
  106. // node containing plain text.
  107. if ($node->children()->count() == 0) {
  108. // Retrieve text from node
  109. $result = (string) $node;
  110. }
  111. }
  112. // Filter out the value
  113. if (isset($result)) {
  114. $result = $param->filter($result);
  115. }
  116. return $result;
  117. }
  118. /**
  119. * @param Parameter $param
  120. * @param \SimpleXMLElement $node
  121. * @return array
  122. */
  123. private function processArray(Parameter $param, \SimpleXMLElement $node)
  124. {
  125. // Cast to an array if the value was a string, but should be an array
  126. $items = $param->getItems();
  127. $sentAs = $items->getWireName();
  128. $result = [];
  129. $ns = null;
  130. if (strstr($sentAs, ':')) {
  131. // Get namespace from the wire name
  132. list($ns, $sentAs) = explode(':', $sentAs);
  133. } else {
  134. // Get namespace from data
  135. $ns = $items->getData('xmlNs');
  136. }
  137. if ($sentAs === null) {
  138. // A general collection of nodes
  139. foreach ($node as $child) {
  140. $result[] = $this->recursiveProcess($items, $child);
  141. }
  142. } else {
  143. // A collection of named, repeating nodes
  144. // (i.e. <collection><foo></foo><foo></foo></collection>)
  145. $children = $node->children($ns, true)->{$sentAs};
  146. foreach ($children as $child) {
  147. $result[] = $this->recursiveProcess($items, $child);
  148. }
  149. }
  150. return $result;
  151. }
  152. /**
  153. * Process an object
  154. *
  155. * @param Parameter $param API parameter being parsed
  156. * @param \SimpleXMLElement $node Value to process
  157. * @return array
  158. */
  159. private function processObject(Parameter $param, \SimpleXMLElement $node)
  160. {
  161. $result = $knownProps = $knownAttributes = [];
  162. // Handle known properties
  163. if ($properties = $param->getProperties()) {
  164. foreach ($properties as $property) {
  165. $name = $property->getName();
  166. $sentAs = $property->getWireName();
  167. $knownProps[$sentAs] = 1;
  168. if (strpos($sentAs, ':')) {
  169. list($ns, $sentAs) = explode(':', $sentAs);
  170. } else {
  171. $ns = $property->getData('xmlNs');
  172. }
  173. if ($property->getData('xmlAttribute')) {
  174. // Handle XML attributes
  175. $result[$name] = (string) $node->attributes($ns, true)->{$sentAs};
  176. $knownAttributes[$sentAs] = 1;
  177. } elseif (count($node->children($ns, true)->{$sentAs})) {
  178. // Found a child node matching wire name
  179. $childNode = $node->children($ns, true)->{$sentAs};
  180. $result[$name] = $this->recursiveProcess(
  181. $property,
  182. $childNode
  183. );
  184. }
  185. }
  186. }
  187. // Handle additional, undefined properties
  188. $additional = $param->getAdditionalProperties();
  189. if ($additional instanceof Parameter) {
  190. // Process all child elements according to the given schema
  191. foreach ($node->children($additional->getData('xmlNs'), true) as $childNode) {
  192. $sentAs = $childNode->getName();
  193. if (!isset($knownProps[$sentAs])) {
  194. $result[$sentAs] = $this->recursiveProcess(
  195. $additional,
  196. $childNode
  197. );
  198. }
  199. }
  200. } elseif ($additional === null || $additional === true) {
  201. // Blindly transform the XML into an array preserving as much data
  202. // as possible. Remove processed, aliased properties.
  203. $array = array_diff_key(self::xmlToArray($node), $knownProps);
  204. // Remove @attributes that were explicitly plucked from the
  205. // attributes list.
  206. if (isset($array['@attributes']) && $knownAttributes) {
  207. $array['@attributes'] = array_diff_key($array['@attributes'], $knownProps);
  208. if (!$array['@attributes']) {
  209. unset($array['@attributes']);
  210. }
  211. }
  212. // Merge it together with the original result
  213. $result = array_merge($array, $result);
  214. }
  215. return $result;
  216. }
  217. /**
  218. * Convert an XML document to an array.
  219. *
  220. * @param \SimpleXMLElement $xml
  221. * @param int $nesting
  222. * @param null $ns
  223. *
  224. * @return array
  225. */
  226. private static function xmlToArray(
  227. \SimpleXMLElement $xml,
  228. $ns = null,
  229. $nesting = 0
  230. ) {
  231. $result = [];
  232. $children = $xml->children($ns, true);
  233. foreach ($children as $name => $child) {
  234. $attributes = (array) $child->attributes($ns, true);
  235. if (!isset($result[$name])) {
  236. $childArray = self::xmlToArray($child, $ns, $nesting + 1);
  237. $result[$name] = $attributes
  238. ? array_merge($attributes, $childArray)
  239. : $childArray;
  240. continue;
  241. }
  242. // A child element with this name exists so we're assuming
  243. // that the node contains a list of elements
  244. if (!is_array($result[$name])) {
  245. $result[$name] = [$result[$name]];
  246. } elseif (!isset($result[$name][0])) {
  247. // Convert the first child into the first element of a numerically indexed array
  248. $firstResult = $result[$name];
  249. $result[$name] = [];
  250. $result[$name][] = $firstResult;
  251. }
  252. $childArray = self::xmlToArray($child, $ns, $nesting + 1);
  253. if ($attributes) {
  254. $result[$name][] = array_merge($attributes, $childArray);
  255. } else {
  256. $result[$name][] = $childArray;
  257. }
  258. }
  259. // Extract text from node
  260. $text = trim((string) $xml);
  261. if ($text === '') {
  262. $text = null;
  263. }
  264. // Process attributes
  265. $attributes = (array) $xml->attributes($ns, true);
  266. if ($attributes) {
  267. if ($text !== null) {
  268. $result['value'] = $text;
  269. }
  270. $result = array_merge($attributes, $result);
  271. } elseif ($text !== null) {
  272. $result = $text;
  273. }
  274. // Make sure we're always returning an array
  275. if ($nesting == 0 && !is_array($result)) {
  276. $result = [$result];
  277. }
  278. return $result;
  279. }
  280. }