1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Bavix\XMLReader; |
4
|
|
|
|
5
|
|
|
use Bavix\Foundation\SharedInstance; |
6
|
|
|
use Bavix\Helpers\File; |
7
|
|
|
use Bavix\Helpers\Arr; |
8
|
|
|
use Bavix\Exceptions; |
9
|
|
|
use DOMElement; |
10
|
|
|
|
11
|
|
|
class XMLReader |
12
|
|
|
{ |
13
|
|
|
|
14
|
|
|
use SharedInstance; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* @var array |
18
|
|
|
*/ |
19
|
|
|
protected $copyright = [ |
20
|
|
|
'created-with' => 'https://github.com/bavix/xml' |
21
|
|
|
]; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var \DOMDocument |
25
|
|
|
*/ |
26
|
|
|
protected $document; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @return \DOMDocument |
30
|
|
|
*/ |
31
|
1 |
|
protected function document() |
32
|
|
|
{ |
33
|
1 |
|
if (!$this->document) |
34
|
|
|
{ |
35
|
1 |
|
$this->document = new \DOMDocument('1.0', 'utf-8'); |
36
|
1 |
|
$this->document->formatOutput = true; |
37
|
1 |
|
$this->copyright['created-at'] = \date('Y-m-d H:i:s'); |
38
|
|
|
} |
39
|
|
|
|
40
|
1 |
|
return $this->document; |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @param string $name |
45
|
|
|
* |
46
|
|
|
* @return \DOMElement |
47
|
|
|
*/ |
48
|
1 |
|
protected function element($name) |
49
|
|
|
{ |
50
|
1 |
|
return $this->document()->createElement($name); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @param \SimpleXMLElement $element |
55
|
|
|
* @param string $property |
56
|
|
|
* |
57
|
|
|
* @return array |
58
|
|
|
*/ |
59
|
1 |
|
protected function _property(\SimpleXMLElement $element, $property) |
60
|
|
|
{ |
61
|
1 |
|
$output = []; |
62
|
|
|
|
63
|
1 |
|
if (method_exists($element, $property)) |
64
|
|
|
{ |
65
|
|
|
|
66
|
1 |
|
$properties = $element->$property(); |
67
|
|
|
|
68
|
1 |
|
if ($properties) |
69
|
|
|
{ |
70
|
1 |
|
$output['@' . $property] = is_array($properties) ? |
71
|
1 |
|
$properties : $this->_asArray($properties); |
72
|
|
|
|
73
|
1 |
|
if (empty($output['@' . $property])) |
74
|
|
|
{ |
75
|
|
|
Arr::remove($output, '@' . $property); |
76
|
|
|
} |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
} |
80
|
|
|
|
81
|
1 |
|
return $output; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @param \SimpleXMLElement $element |
86
|
|
|
* |
87
|
|
|
* @return array|string |
88
|
|
|
* |
89
|
|
|
* @codeCoverageIgnore |
90
|
|
|
*/ |
91
|
|
|
protected function _asData(\SimpleXMLElement $element) |
92
|
|
|
{ |
93
|
|
|
$output = $this->_property($element, 'attributes'); |
94
|
|
|
|
95
|
|
|
if (!$element->count()) |
96
|
|
|
{ |
97
|
|
|
$output['@value'] = (string)$element; |
98
|
|
|
|
99
|
|
|
if (!isset($output['@attributes'])) |
100
|
|
|
{ |
101
|
|
|
$output = $output['@value']; |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
return $output; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* @param \SimpleXMLElement $element |
110
|
|
|
* |
111
|
|
|
* @return array|string |
112
|
|
|
* |
113
|
|
|
* @codeCoverageIgnore |
114
|
|
|
*/ |
115
|
|
|
protected function _asArray(\SimpleXMLElement $element) |
116
|
|
|
{ |
117
|
|
|
$output = $this->_asData($element); |
118
|
|
|
|
119
|
|
|
if (!$element->count()) |
120
|
|
|
{ |
121
|
|
|
return $output; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
return $this->_pushArray($output, $element); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @param array $output |
129
|
|
|
* @param \SimpleXMLElement $element |
130
|
|
|
* |
131
|
|
|
* @return array |
132
|
|
|
* |
133
|
|
|
* @codeCoverageIgnore |
134
|
|
|
*/ |
135
|
|
|
protected function _pushArray(array &$output, \SimpleXMLElement $element) |
136
|
|
|
{ |
137
|
|
|
$first = []; |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* @var \SimpleXMLElement $item |
141
|
|
|
*/ |
142
|
|
|
foreach ($element as $key => $item) |
143
|
|
|
{ |
144
|
|
|
if (!isset($output[$key])) |
145
|
|
|
{ |
146
|
|
|
$first[$key] = true; |
147
|
|
|
$output[$key] = $this->_asArray($item); |
148
|
|
|
continue; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
if (!empty($first[$key])) |
152
|
|
|
{ |
153
|
|
|
$output[$key] = [$output[$key]]; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
$output[$key][] = $this->_asArray($item); |
157
|
|
|
$first[$key] = false; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
return $output; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @param string|\DOMNode $mixed |
165
|
|
|
* |
166
|
|
|
* @return \SimpleXMLElement |
167
|
|
|
* |
168
|
|
|
* @codeCoverageIgnore |
169
|
|
|
*/ |
170
|
|
|
protected function _simpleXml($mixed) |
171
|
|
|
{ |
172
|
|
|
if ($mixed instanceof \DOMNode) |
173
|
|
|
{ |
174
|
|
|
return \simplexml_import_dom($mixed); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
if (File::isFile($mixed)) |
178
|
|
|
{ |
179
|
|
|
return \simplexml_load_file($mixed); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
return \simplexml_load_string($mixed); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* @param string|\DOMNode $mixed |
187
|
|
|
* |
188
|
|
|
* @return array |
189
|
|
|
*/ |
190
|
1 |
|
public function asArray($mixed) |
191
|
|
|
{ |
192
|
1 |
|
$data = $this->_simpleXml($mixed); |
193
|
|
|
|
194
|
1 |
|
return $data ? |
195
|
1 |
|
$this->_asArray($data) : null; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* @return \DOMDocument |
200
|
|
|
*/ |
201
|
|
|
public function asObject() |
202
|
|
|
{ |
203
|
|
|
return clone $this->document(); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @param array|\Traversable $storage |
208
|
|
|
* |
209
|
|
|
* @return array |
210
|
|
|
* |
211
|
|
|
* @codeCoverageIgnore |
212
|
|
|
*/ |
213
|
|
|
protected function _convertStorage($storage) |
214
|
|
|
{ |
215
|
|
|
if ($storage instanceof \Traversable) |
216
|
|
|
{ |
217
|
|
|
return \iterator_to_array($storage); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return $storage; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* @param array|\Traversable $storage |
225
|
|
|
* @param string $name |
226
|
|
|
* @param array $attributes |
227
|
|
|
* |
228
|
|
|
* @return string |
229
|
|
|
*/ |
230
|
1 |
|
public function asXML($storage, $name = 'bavix', array $attributes = []) |
231
|
|
|
{ |
232
|
1 |
|
$element = $this->element($name); |
233
|
|
|
|
234
|
1 |
|
foreach ($attributes as $attr => $value) { |
235
|
|
|
$element->setAttribute($attr, $value); |
236
|
|
|
} |
237
|
|
|
|
238
|
1 |
|
$this->addAttributes($element, $this->copyright); |
239
|
1 |
|
$this->document()->appendChild($element); |
240
|
1 |
|
$this->convert($element, $this->_convertStorage($storage)); |
241
|
1 |
|
$xml = $this->document()->saveXML(); |
242
|
|
|
|
243
|
1 |
|
$this->document = null; |
244
|
|
|
|
245
|
1 |
|
return $xml; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* @param DOMElement $element |
250
|
|
|
* @param mixed $storage |
251
|
|
|
* |
252
|
|
|
* @throws Exceptions\Blank |
253
|
|
|
* |
254
|
|
|
* @codeCoverageIgnore |
255
|
|
|
*/ |
256
|
|
|
protected function convert(DOMElement $element, $storage) |
257
|
|
|
{ |
258
|
|
|
if (!is_array($storage)) |
259
|
|
|
{ |
260
|
|
|
$element->nodeValue = htmlspecialchars($storage); |
261
|
|
|
|
262
|
|
|
return; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
if (empty($storage)) |
266
|
|
|
{ |
267
|
|
|
throw new Exceptions\Blank('Array is empty'); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$isInt = Arr::map(Arr::getKeys($storage), '\is_int'); |
271
|
|
|
$sequential = !Arr::in($isInt, false); |
272
|
|
|
|
273
|
|
|
foreach ($storage as $key => $data) |
274
|
|
|
{ |
275
|
|
|
if ($sequential) |
276
|
|
|
{ |
277
|
|
|
$this->sequential($element, $data); |
278
|
|
|
continue; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
$this->addNodeWithKey($key, $element, $data); |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* @param string $key |
287
|
|
|
* @param DOMElement $element |
288
|
|
|
* @param mixed $storage |
289
|
|
|
* |
290
|
|
|
* @codeCoverageIgnore |
291
|
|
|
*/ |
292
|
|
|
protected function addNodeWithKey($key, DOMElement $element, $storage) |
293
|
|
|
{ |
294
|
|
|
if ($key === '@attributes') |
295
|
|
|
{ |
296
|
|
|
$this->addAttributes($element, $storage); |
297
|
|
|
} |
298
|
|
|
else if ($key === '@value' && \is_string($storage)) |
299
|
|
|
{ |
300
|
|
|
$element->nodeValue = \htmlspecialchars($storage); |
301
|
|
|
} |
302
|
|
|
else |
303
|
|
|
{ |
304
|
|
|
$this->addNode($element, $key, $storage); |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* @param DOMElement $element |
310
|
|
|
* @param mixed $storage |
311
|
|
|
* |
312
|
|
|
* @codeCoverageIgnore |
313
|
|
|
*/ |
314
|
|
|
protected function sequential(DOMElement $element, $storage) |
315
|
|
|
{ |
316
|
|
|
if (is_array($storage)) |
317
|
|
|
{ |
318
|
|
|
$this->addCollectionNode($element, $storage); |
319
|
|
|
|
320
|
|
|
return; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
$this->addSequentialNode($element, $storage); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* @param DOMElement $element |
328
|
|
|
* @param string $key |
329
|
|
|
* @param mixed $value |
330
|
|
|
* |
331
|
|
|
* @throws Exceptions\Blank |
332
|
|
|
* |
333
|
|
|
* @codeCoverageIgnore |
334
|
|
|
*/ |
335
|
|
|
protected function addNode(DOMElement $element, $key, $value) |
336
|
|
|
{ |
337
|
|
|
$key = \str_replace(' ', '-', $key); |
338
|
|
|
$child = $this->document()->createElement($key); |
339
|
|
|
$element->appendChild($child); |
340
|
|
|
$this->convert($child, $value); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* @param DOMElement $element |
345
|
|
|
* @param mixed $value |
346
|
|
|
* |
347
|
|
|
* @throws Exceptions\Blank |
348
|
|
|
* |
349
|
|
|
* @codeCoverageIgnore |
350
|
|
|
*/ |
351
|
|
|
protected function addCollectionNode(DOMElement $element, $value) |
352
|
|
|
{ |
353
|
|
|
if ($element->childNodes->length === 0) |
354
|
|
|
{ |
355
|
|
|
$this->convert($element, $value); |
356
|
|
|
|
357
|
|
|
return; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* @var $child DOMElement |
362
|
|
|
*/ |
363
|
|
|
$child = $this->document()->createElement($element->nodeName); |
364
|
|
|
// $child = $element->cloneNode(); |
|
|
|
|
365
|
|
|
$element->parentNode->appendChild($child); |
366
|
|
|
$this->convert($child, $value); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* @param DOMElement $element |
371
|
|
|
* @param mixed $value |
372
|
|
|
* |
373
|
|
|
* @codeCoverageIgnore |
374
|
|
|
*/ |
375
|
|
|
protected function addSequentialNode(DOMElement $element, $value) |
376
|
|
|
{ |
377
|
|
|
if (empty($element->nodeValue)) |
378
|
|
|
{ |
379
|
|
|
$element->nodeValue = \htmlspecialchars($value); |
380
|
|
|
|
381
|
|
|
return; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
$child = $this->document()->createElement($element->nodeName); |
385
|
|
|
// $child = $element->cloneNode(); |
|
|
|
|
386
|
|
|
$child->nodeValue = \htmlspecialchars($value); |
387
|
|
|
$element->parentNode->appendChild($child); |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* @param DOMElement $element |
392
|
|
|
* @param array|\Traversable $storage |
393
|
|
|
* |
394
|
|
|
* @codeCoverageIgnore |
395
|
|
|
*/ |
396
|
|
|
protected function addAttributes(DOMElement $element, $storage) |
397
|
|
|
{ |
398
|
|
|
foreach ($storage as $attrKey => $attrVal) |
399
|
|
|
{ |
400
|
|
|
$element->setAttribute($attrKey, $attrVal); |
401
|
|
|
} |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
} |
405
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.