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