XMLReader::_asData()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 12
ccs 0
cts 0
cp 0
crap 12
rs 10
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 $namespaces = [];
20
21
    /**
22
     * @var \DOMDocument
23
     */
24
    protected $document;
25
26
    /**
27
     * @var Options
28
     */
29
    protected $options;
30
31
    /**
32
     * XMLReader constructor.
33
     * @param Options $options
34
     */
35 1
    public function __construct(Options $options = null)
36
    {
37 1
        if ($options) {
38
            $this->options = $options;
39
        }
40 1
    }
41
42
    /**
43
     * @return Options
44
     */
45 1
    protected function options(): Options
46
    {
47 1
        if (!$this->options) {
48 1
            $this->options = new Options();
49
        }
50 1
        return $this->options;
51
    }
52
53
    /**
54
     * @param array|\Traversable $storage
55
     * @param string $name
56
     * @param array $attributes
57
     * @return string
58
     */
59
    public static function toXml($storage, string $name = null, array $attributes = []): string
60
    {
61
        return (new static())
62
            ->asXML($storage, $name, $attributes);
63
    }
64
65
    /**
66
     * @return \DOMDocument
67
     */
68 1
    public function makeDocument(): \DOMDocument
69
    {
70 1
        $document = new \DOMDocument(
71 1
            $this->options()->getVersion(),
72 1
            $this->options()->getCharset()
73
        );
74
75 1
        $document->formatOutput = $this->options()->isFormatOutput();
76
77 1
        return $document;
78
    }
79
80
    /**
81
     * @return \DOMDocument
82
     */
83 1
    protected function document(): \DOMDocument
84
    {
85 1
        if (!$this->document)
86
        {
87 1
            $this->document = $this->makeDocument();
88
        }
89
90 1
        return $this->document;
91
    }
92
93
    /**
94
     * @param string $name
95
     * @param mixed $value
96
     * @return DOMElement
97
     */
98 1
    protected function createElement(string $name, $value = null): \DOMElement
99
    {
100 1
        return $this->document()->createElement($name, $value);
101
    }
102
103
    /**
104
     * @param string $name
105
     *
106
     * @return \DOMElement
107
     */
108 1
    protected function element(string $name): \DOMElement
109
    {
110 1
        if (\strpos($name, ':') !== false)
111
        {
112
            $keys = explode(':', $name);
113
114
            return $this->document()->createElementNS(
115
                $this->namespaces[$this->options()->getNamespace() . ':' . \current($keys)],
116
                $name
117
            );
118
        }
119
120 1
        return $this->createElement($name);
121
    }
122
123
    /**
124
     * @param \SimpleXMLElement $element
125
     * @param string            $property
126
     *
127
     * @return array
128
     */
129 1
    protected function _property(\SimpleXMLElement $element, $property): array
130
    {
131 1
        $output = [];
132
133 1
        if (\method_exists($element, $property)) {
134 1
            $properties = $element->$property();
135
136 1
            if ($properties) {
137
138 1
                $data = \is_array($properties) ?
139 1
                    $properties : $this->_asArray($properties);
140
141 1
                if ($data !== null || $data === '') {
0 ignored issues
show
introduced by
The condition $data !== null is always true.
Loading history...
142 1
                    $output['@' . $property] = $data;
143
                }
144
            }
145
        }
146
147 1
        return $output;
148
    }
149
150
    /**
151
     * @param \SimpleXMLElement $element
152
     *
153
     * @return array|string
154
     *
155
     * @codeCoverageIgnore
156
     */
157
    protected function _asData(\SimpleXMLElement $element)
158
    {
159
        $output = $this->_property($element, 'attributes');
160
161
        if (!$element->count()) {
162
            $output['@value'] = (string)$element;
163
            if (!isset($output['@attributes'])) {
164
                $output = $output['@value'];
165
            }
166
        }
167
168
        return $output;
169
    }
170
171
    /**
172
     * @param \SimpleXMLElement $element
173
     *
174
     * @return array|string
175
     *
176
     * @codeCoverageIgnore
177
     */
178
    protected function _asArray(\SimpleXMLElement $element)
179
    {
180
        $output = $this->_asData($element);
181
182
        if (!$element->count()) {
183
            return $output;
184
        }
185
186
        return $this->_pushArray($output, $element);
0 ignored issues
show
Bug introduced by
It seems like $output can also be of type string; however, parameter $output of Bavix\XMLReader\XMLReader::_pushArray() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

186
        return $this->_pushArray(/** @scrutinizer ignore-type */ $output, $element);
Loading history...
187
    }
188
189
    /**
190
     * @param array             $output
191
     * @param \SimpleXMLElement $element
192
     *
193
     * @return array
194
     *
195
     * @codeCoverageIgnore
196
     */
197
    protected function _pushArray(array &$output, \SimpleXMLElement $element): array
198
    {
199
        $first = [];
200
201
        /**
202
         * @var \SimpleXMLElement $item
203
         */
204
        foreach ($element as $key => $item)
205
        {
206
            if (!isset($output[$key])) {
207
                $first[$key]  = true;
208
                $output[$key] = $this->_asArray($item);
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type null; however, parameter $element of Bavix\XMLReader\XMLReader::_asArray() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

208
                $output[$key] = $this->_asArray(/** @scrutinizer ignore-type */ $item);
Loading history...
209
                continue;
210
            }
211
212
            if (!empty($first[$key])) {
213
                $output[$key] = [$output[$key]];
214
            }
215
216
            $output[$key][] = $this->_asArray($item);
217
            $first[$key]    = false;
218
        }
219
220
        return $output;
221
    }
222
223
    /**
224
     * @param string|\DOMNode $mixed
225
     *
226
     * @return \SimpleXMLElement
227
     *
228
     * @codeCoverageIgnore
229
     */
230
    protected function _simpleXml($mixed): \SimpleXMLElement
231
    {
232
        if ($mixed instanceof \DOMNode) {
233
            return \simplexml_import_dom($mixed);
234
        }
235
236
        if (File::isFile($mixed))
237
        {
238
            $mixed = \file_get_contents($mixed);
239
        }
240
241
        return \simplexml_load_string($mixed);
242
    }
243
244
    /**
245
     * @param string|\DOMNode $mixed
246
     *
247
     * @return array|null
248
     */
249 1
    public function asArray($mixed): ?array
250
    {
251 1
        $data = $this->_simpleXml($mixed);
252 1
        return $data ? $this->_asArray($data) : null;
0 ignored issues
show
introduced by
$data is of type SimpleXMLElement, thus it always evaluated to true.
Loading history...
253
    }
254
255
    /**
256
     * @return \DOMDocument
257
     */
258
    public function asObject(): \DOMDocument
259
    {
260
        return clone $this->document();
261
    }
262
263
    /**
264
     * @param array|\Traversable $storage
265
     *
266
     * @return array
267
     *
268
     * @codeCoverageIgnore
269
     */
270
    protected function _convertStorage($storage): array
271
    {
272
        if ($storage instanceof \Traversable) {
273
            return \iterator_to_array($storage);
274
        }
275
276
        return $storage;
277
    }
278
279
    /**
280
     * @param array|\Traversable $storage
281
     * @param string             $name
282
     * @param array              $attributes
283
     *
284
     * @return string
285
     */
286 1
    public function asXML($storage, string $name = null, array $attributes = []): string
287
    {
288 1
        $storage = $this->fragments($storage);
0 ignored issues
show
Bug introduced by
It seems like $storage can also be of type Traversable; however, parameter $storage of Bavix\XMLReader\XMLReader::fragments() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

288
        $storage = $this->fragments(/** @scrutinizer ignore-type */ $storage);
Loading history...
289 1
        $element = $this->element($name ?? $this->options()->getRootName());
290
291 1
        $this->addAttributes($element, $attributes);
292 1
        $this->document()->appendChild($element);
293 1
        $this->convert($element, $this->_convertStorage($storage));
294 1
        $xml = $this->document()->saveXML();
295
296 1
        $this->document = null;
297
298 1
        return $xml;
299
    }
300
301
    /**
302
     * @param DOMElement $element
303
     * @param mixed      $storage
304
     *
305
     * @throws Exceptions\Blank
306
     *
307
     * @codeCoverageIgnore
308
     */
309
    protected function convert(DOMElement $element, $storage): void
310
    {
311
        if (\is_object($storage)) {
312
            $element->appendChild($element->ownerDocument->importNode($storage));
0 ignored issues
show
Bug introduced by
The method importNode() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

312
            $element->appendChild($element->ownerDocument->/** @scrutinizer ignore-call */ importNode($storage));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
313
            return;
314
        }
315
316
        if (!\is_array($storage)) {
317
            $element->nodeValue = \htmlspecialchars($storage);
318
            return;
319
        }
320
321
        if (empty($storage)) {
322
            throw new Exceptions\Blank('Array is empty');
323
        }
324
325
        $isInt      = Arr::map(Arr::getKeys($storage), '\is_int');
326
        $sequential = !Arr::in($isInt, false);
327
328
        foreach ($storage as $key => $data) {
329
            if ($sequential) {
330
                $this->sequential($element, $data);
331
                continue;
332
            }
333
334
            $this->addNodeWithKey($key, $element, $data);
335
        }
336
    }
337
338
    /**
339
     * @param array $storage
340
     * @return array
341
     */
342
    protected function fragments(array $storage): array
343
    {
344 1
        Arr::walkRecursive($storage, function(&$value) {
345 1
            if (\is_object($value) && $value instanceof Raw) {
346
                $value = $value->fragment($this->document());
347
            }
348 1
        });
349
350 1
        return $storage;
351
    }
352
353
    /**
354
     * @param string     $key
355
     * @param DOMElement $element
356
     * @param mixed      $storage
357
     *
358
     * @codeCoverageIgnore
359
     */
360
    protected function addNodeWithKey($key, DOMElement $element, $storage): void
361
    {
362
        if ($key === '@attributes') {
363
            $this->addAttributes($element, $storage);
364
            return;
365
        }
366
367
        if ($key === '@value') {
368
369
            if (\is_string($storage)) {
370
                $element->nodeValue = $storage;
371
                return;
372
            }
373
374
            $dom      = new \DOMDocument();
375
            $fragment = $element->ownerDocument->createDocumentFragment();
376
            $dom->loadXML(static::toXml($storage, 'root', $this->namespaces));
377
378
            /**
379
             * @var $childNode \DOMText
380
             */
381
            foreach ($dom->firstChild->childNodes as $childNode) {
382
                $fragment->appendXML($childNode->ownerDocument->saveXML($childNode));
383
            }
384
385
            $element->appendChild($fragment);
386
            return;
387
        }
388
389
        $this->addNode($element, $key, $storage);
390
    }
391
392
    /**
393
     * @param DOMElement $element
394
     * @param mixed      $storage
395
     *
396
     * @codeCoverageIgnore
397
     */
398
    protected function sequential(DOMElement $element, $storage): void
399
    {
400
        if (\is_array($storage)) {
401
            $this->addCollectionNode($element, $storage);
402
            return;
403
        }
404
405
        $this->addSequentialNode($element, $storage);
406
    }
407
408
    /**
409
     * @param DOMElement $element
410
     * @param string     $key
411
     * @param mixed      $value
412
     *
413
     * @throws Exceptions\Blank
414
     *
415
     * @codeCoverageIgnore
416
     */
417
    protected function addNode(DOMElement $element, $key, $value): void
418
    {
419
        $key   = \str_replace(' ', '-', $key);
420
        $child = $this->element($key);
421
        $element->appendChild($child);
422
        $this->convert($child, $value);
423
    }
424
425
    /**
426
     * @param DOMElement $element
427
     * @param mixed      $value
428
     *
429
     * @throws Exceptions\Blank
430
     *
431
     * @codeCoverageIgnore
432
     */
433
    protected function addCollectionNode(DOMElement $element, $value): void
434
    {
435
        if ($element->childNodes->length === 0) {
436
            $this->convert($element, $value);
437
            return;
438
        }
439
440
        /**
441
         * @var $child DOMElement
442
         */
443
        $child = $this->element($element->nodeName);
444
        $element->parentNode->appendChild($child);
0 ignored issues
show
Bug introduced by
The method appendChild() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

444
        $element->parentNode->/** @scrutinizer ignore-call */ 
445
                              appendChild($child);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
445
        $this->convert($child, $value);
446
    }
447
448
    /**
449
     * @param DOMElement $element
450
     * @param mixed      $value
451
     *
452
     * @codeCoverageIgnore
453
     */
454
    protected function addSequentialNode(DOMElement $element, $value): void
455
    {
456
        if (empty($element->nodeValue)) {
457
            $element->nodeValue = \htmlspecialchars($value);
458
            return;
459
        }
460
461
        $child = $this->element($element->nodeName);
462
        $child->nodeValue = \htmlspecialchars($value);
463
        $element->parentNode->appendChild($child);
464
    }
465
466
    /**
467
     * @param DOMElement         $element
468
     * @param array|\Traversable $storage
469
     *
470
     * @codeCoverageIgnore
471
     */
472
    protected function addAttributes(DOMElement $element, $storage): void
473
    {
474
        foreach ($storage as $attrKey => $attrVal) {
475
            if (strpos($attrKey, ':') !== false) {
476
                $this->namespaces[$attrKey] = $attrVal;
477
            }
478
479
            $element->setAttribute($attrKey, $attrVal);
480
        }
481
    }
482
483
}
484