Passed
Push — master ( d08c31...9d81ca )
by Бабичев
04:33
created

XMLReader::options()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
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
                    $properties :
140 1
                    $this->_asArray($properties);
141
142 1
                if ($data !== null || $data === '') {
0 ignored issues
show
introduced by
The condition $data !== null is always true.
Loading history...
143 1
                    $output['@' . $property] = $data;
144
                }
145
            }
146
        }
147
148 1
        return $output;
149
    }
150
151
    /**
152
     * @param \SimpleXMLElement $element
153
     *
154
     * @return array|string
155
     *
156
     * @codeCoverageIgnore
157
     */
158
    protected function _asData(\SimpleXMLElement $element)
159
    {
160
        $output = $this->_property($element, 'attributes');
161
162
        if (!$element->count()) {
163
            $output['@value'] = (string)$element;
164
            if (!isset($output['@attributes'])) {
165
                $output = $output['@value'];
166
            }
167
        }
168
169
        return $output;
170
    }
171
172
    /**
173
     * @param \SimpleXMLElement $element
174
     *
175
     * @return array|string
176
     *
177
     * @codeCoverageIgnore
178
     */
179
    protected function _asArray(\SimpleXMLElement $element)
180
    {
181
        $output = $this->_asData($element);
182
183
        if (!$element->count()) {
184
            return $output;
185
        }
186
187
        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

187
        return $this->_pushArray(/** @scrutinizer ignore-type */ $output, $element);
Loading history...
188
    }
189
190
    /**
191
     * @param array             $output
192
     * @param \SimpleXMLElement $element
193
     *
194
     * @return array
195
     *
196
     * @codeCoverageIgnore
197
     */
198
    protected function _pushArray(array &$output, \SimpleXMLElement $element): array
199
    {
200
        $first = [];
201
202
        /**
203
         * @var \SimpleXMLElement $item
204
         */
205
        foreach ($element as $key => $item)
206
        {
207
            if (!isset($output[$key])) {
208
                $first[$key]  = true;
209
                $output[$key] = $this->_asArray($item);
210
                continue;
211
            }
212
213
            if (!empty($first[$key])) {
214
                $output[$key] = [$output[$key]];
215
            }
216
217
            $output[$key][] = $this->_asArray($item);
218
            $first[$key]    = false;
219
        }
220
221
        return $output;
222
    }
223
224
    /**
225
     * @param string|\DOMNode $mixed
226
     *
227
     * @return \SimpleXMLElement
228
     *
229
     * @codeCoverageIgnore
230
     */
231
    protected function _simpleXml($mixed): \SimpleXMLElement
232
    {
233
        if ($mixed instanceof \DOMNode) {
234
            return \simplexml_import_dom($mixed);
235
        }
236
237
        if (File::isFile($mixed))
238
        {
239
            $mixed = \file_get_contents($mixed);
240
        }
241
242
        return \simplexml_load_string($mixed);
243
    }
244
245
    /**
246
     * @param string|\DOMNode $mixed
247
     *
248
     * @return array|null
249
     */
250 1
    public function asArray($mixed): ?array
251
    {
252 1
        $data = $this->_simpleXml($mixed);
253 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...
254
    }
255
256
    /**
257
     * @return \DOMDocument
258
     */
259
    public function asObject(): \DOMDocument
260
    {
261
        return clone $this->document();
262
    }
263
264
    /**
265
     * @param array|\Traversable $storage
266
     *
267
     * @return array
268
     *
269
     * @codeCoverageIgnore
270
     */
271
    protected function _convertStorage($storage): array
272
    {
273
        if ($storage instanceof \Traversable) {
274
            return \iterator_to_array($storage);
275
        }
276
277
        return $storage;
278
    }
279
280
    /**
281
     * @param array|\Traversable $storage
282
     * @param string             $name
283
     * @param array              $attributes
284
     *
285
     * @return string
286
     */
287 1
    public function asXML($storage, string $name = null, array $attributes = []): string
288
    {
289 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

289
        $storage = $this->fragments(/** @scrutinizer ignore-type */ $storage);
Loading history...
290 1
        $element = $this->element($name ?? $this->options()->getRootName());
291
292 1
        $this->addAttributes($element, $attributes);
293 1
        $this->document()->appendChild($element);
294 1
        $this->convert($element, $this->_convertStorage($storage));
295 1
        $xml = $this->document()->saveXML();
296
297 1
        $this->document = null;
298
299 1
        return $xml;
300
    }
301
302
    /**
303
     * @param DOMElement $element
304
     * @param mixed      $storage
305
     *
306
     * @throws Exceptions\Blank
307
     *
308
     * @codeCoverageIgnore
309
     */
310
    protected function convert(DOMElement $element, $storage): void
311
    {
312
        if (\is_object($storage)) {
313
            $element->appendChild($element->ownerDocument->importNode($storage));
314
            return;
315
        }
316
317
        if (!\is_array($storage)) {
318
            $element->nodeValue = \htmlspecialchars($storage);
319
            return;
320
        }
321
322
        if (empty($storage)) {
323
            throw new Exceptions\Blank('Array is empty');
324
        }
325
326
        $isInt      = Arr::map(Arr::getKeys($storage), '\is_int');
327
        $sequential = !Arr::in($isInt, false);
328
329
        foreach ($storage as $key => $data) {
330
            if ($sequential) {
331
                $this->sequential($element, $data);
332
                continue;
333
            }
334
335
            $this->addNodeWithKey($key, $element, $data);
336
        }
337
    }
338
339
    /**
340
     * @param array $storage
341
     * @return array
342
     */
343
    protected function fragments(array $storage): array
344
    {
345 1
        Arr::walkRecursive($storage, function (&$value) {
346 1
            if (\is_object($value) && $value instanceof Raw) {
347
                $value = $value->fragment($this->document());
348
            }
349 1
        });
350
351 1
        return $storage;
352
    }
353
354
    /**
355
     * @param string     $key
356
     * @param DOMElement $element
357
     * @param mixed      $storage
358
     *
359
     * @codeCoverageIgnore
360
     */
361
    protected function addNodeWithKey($key, DOMElement $element, $storage): void
362
    {
363
        if ($key === '@attributes') {
364
            $this->addAttributes($element, $storage);
365
            return;
366
        }
367
368
        if ($key === '@value') {
369
370
            if (\is_string($storage)) {
371
                $element->nodeValue = $storage;
372
                return;
373
            }
374
375
            $dom      = new \DOMDocument();
376
            $fragment = $element->ownerDocument->createDocumentFragment();
377
            $dom->loadXML(static::toXml($storage, 'root', $this->namespaces));
378
379
            /**
380
             * @var $childNode \DOMText
381
             */
382
            foreach ($dom->firstChild->childNodes as $childNode) {
383
                $fragment->appendXML($childNode->ownerDocument->saveXML($childNode));
384
            }
385
386
            $element->appendChild($fragment);
387
            return;
388
        }
389
390
        $this->addNode($element, $key, $storage);
391
    }
392
393
    /**
394
     * @param DOMElement $element
395
     * @param mixed      $storage
396
     *
397
     * @codeCoverageIgnore
398
     */
399
    protected function sequential(DOMElement $element, $storage): void
400
    {
401
        if (\is_array($storage)) {
402
            $this->addCollectionNode($element, $storage);
403
            return;
404
        }
405
406
        $this->addSequentialNode($element, $storage);
407
    }
408
409
    /**
410
     * @param DOMElement $element
411
     * @param string     $key
412
     * @param mixed      $value
413
     *
414
     * @throws Exceptions\Blank
415
     *
416
     * @codeCoverageIgnore
417
     */
418
    protected function addNode(DOMElement $element, $key, $value): void
419
    {
420
        $key   = \str_replace(' ', '-', $key);
421
        $child = $this->element($key);
422
        $element->appendChild($child);
423
        $this->convert($child, $value);
424
    }
425
426
    /**
427
     * @param DOMElement $element
428
     * @param mixed      $value
429
     *
430
     * @throws Exceptions\Blank
431
     *
432
     * @codeCoverageIgnore
433
     */
434
    protected function addCollectionNode(DOMElement $element, $value): void
435
    {
436
        if ($element->childNodes->length === 0) {
437
            $this->convert($element, $value);
438
            return;
439
        }
440
441
        /**
442
         * @var $child DOMElement
443
         */
444
        $child = $this->element($element->nodeName);
445
        $element->parentNode->appendChild($child);
446
        $this->convert($child, $value);
447
    }
448
449
    /**
450
     * @param DOMElement $element
451
     * @param mixed      $value
452
     *
453
     * @codeCoverageIgnore
454
     */
455
    protected function addSequentialNode(DOMElement $element, $value): void
456
    {
457
        if (empty($element->nodeValue)) {
458
            $element->nodeValue = \htmlspecialchars($value);
459
            return;
460
        }
461
462
        $child = $this->element($element->nodeName);
463
        $child->nodeValue = \htmlspecialchars($value);
464
        $element->parentNode->appendChild($child);
465
    }
466
467
    /**
468
     * @param DOMElement         $element
469
     * @param array|\Traversable $storage
470
     *
471
     * @codeCoverageIgnore
472
     */
473
    protected function addAttributes(DOMElement $element, $storage): void
474
    {
475
        foreach ($storage as $attrKey => $attrVal) {
476
            if (strpos($attrKey, ':') !== false) {
477
                $this->namespaces[$attrKey] = $attrVal;
478
            }
479
480
            $element->setAttribute($attrKey, $attrVal);
481
        }
482
    }
483
484
}
485