Completed
Push — master ( de654a...431e9b )
by Бабичев
04:35 queued 01:46
created

XMLReader::fragments()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 26
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 8.7414

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 1
nop 1
dl 0
loc 26
ccs 4
cts 12
cp 0.3333
crap 8.7414
rs 8.5806
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 $copyright = [];
20
21
    /**
22
     * @var array
23
     */
24
    protected $namespaces = [];
25
26
    /**
27
     * @var \DOMDocument
28
     */
29
    protected $document;
30
31 1
    public function __construct($copyright = true)
32
    {
33 1
        if ($copyright)
34
        {
35 1
            $this->copyright = [
36 1
                'created-with' => 'https://github.com/bavix/xml',
37 1
                'created-at'   => \date('Y-m-d H:i:s')
38
            ];
39
        }
40 1
    }
41
42
    /**
43
     * @return \DOMDocument
44
     */
45 1
    protected function document(): \DOMDocument
46
    {
47 1
        if (!$this->document)
48
        {
49 1
            $this->document               = new \DOMDocument('1.0', 'utf-8');
50 1
            $this->document->formatOutput = true;
51
        }
52
53 1
        return $this->document;
54
    }
55
56
    /**
57
     * @param string $name
58
     *
59
     * @return \DOMElement
60
     */
61 1
    protected function element($name): \DOMElement
62
    {
63 1
        if (strpos($name, ':') !== false)
64
        {
65
66
            $keys = explode(':', $name);
67
68
            return $this->document()->createElementNS(
69
                $this->namespaces['xmlns:' . \current($keys)],
70
                $name
71
            );
72
        }
73
74 1
        return $this->document()->createElement($name);
75
    }
76
77
    /**
78
     * @param \SimpleXMLElement $element
79
     * @param string            $property
80
     *
81
     * @return array
82
     */
83 1
    protected function _property(\SimpleXMLElement $element, $property): array
84
    {
85 1
        $output = [];
86
87 1
        if (method_exists($element, $property))
88
        {
89
90 1
            $properties = $element->$property();
91
92 1
            if ($properties)
93
            {
94 1
                $output['@' . $property] = is_array($properties) ?
95 1
                    $properties : $this->_asArray($properties);
96
97 1
                if (empty($output['@' . $property]))
98
                {
99
                    Arr::remove($output, '@' . $property);
100
                }
101
            }
102
103
        }
104
105 1
        return $output;
106
    }
107
108
    /**
109
     * @param \SimpleXMLElement $element
110
     *
111
     * @return array|string
112
     *
113
     * @codeCoverageIgnore
114
     */
115
    protected function _asData(\SimpleXMLElement $element)
116
    {
117
        $output = $this->_property($element, 'attributes');
118
119
        if (!$element->count())
120
        {
121
            $output['@value'] = (string)$element;
122
123
            if (!isset($output['@attributes']))
124
            {
125
                $output = $output['@value'];
126
            }
127
        }
128
129
        return $output;
130
    }
131
132
    /**
133
     * @param \SimpleXMLElement $element
134
     *
135
     * @return array|string
136
     *
137
     * @codeCoverageIgnore
138
     */
139
    protected function _asArray(\SimpleXMLElement $element)
140
    {
141
        $output = $this->_asData($element);
142
143
        if (!$element->count())
144
        {
145
            return $output;
146
        }
147
148
        return $this->_pushArray($output, $element);
149
    }
150
151
    /**
152
     * @param array             $output
153
     * @param \SimpleXMLElement $element
154
     *
155
     * @return array
156
     *
157
     * @codeCoverageIgnore
158
     */
159
    protected function _pushArray(array &$output, \SimpleXMLElement $element): array
160
    {
161
        $first = [];
162
163
        /**
164
         * @var \SimpleXMLElement $item
165
         */
166
        foreach ($element as $key => $item)
167
        {
168
            if (!isset($output[$key]))
169
            {
170
                $first[$key]  = true;
171
                $output[$key] = $this->_asArray($item);
172
                continue;
173
            }
174
175
            if (!empty($first[$key]))
176
            {
177
                $output[$key] = [$output[$key]];
178
            }
179
180
            $output[$key][] = $this->_asArray($item);
181
            $first[$key]    = false;
182
        }
183
184
        return $output;
185
    }
186
187
    /**
188
     * @param string|\DOMNode $mixed
189
     *
190
     * @return \SimpleXMLElement
191
     *
192
     * @codeCoverageIgnore
193
     */
194
    protected function _simpleXml($mixed): \SimpleXMLElement
195
    {
196
        if ($mixed instanceof \DOMNode)
197
        {
198
            return \simplexml_import_dom($mixed);
199
        }
200
201
        if (File::isFile($mixed))
202
        {
203
            return \simplexml_load_file($mixed);
204
        }
205
206
        return \simplexml_load_string($mixed);
207
    }
208
209
    /**
210
     * @param string|\DOMNode $mixed
211
     *
212
     * @return array|null
213
     */
214 1
    public function asArray($mixed)
215
    {
216 1
        $data = $this->_simpleXml($mixed);
217
218 1
        return $data ?
219 1
            $this->_asArray($data) : null;
220
    }
221
222
    /**
223
     * @return \DOMDocument
224
     */
225
    public function asObject(): \DOMDocument
226
    {
227
        return clone $this->document();
228
    }
229
230
    /**
231
     * @param array|\Traversable $storage
232
     *
233
     * @return array
234
     *
235
     * @codeCoverageIgnore
236
     */
237
    protected function _convertStorage($storage): array
238
    {
239
        if ($storage instanceof \Traversable)
240
        {
241
            return \iterator_to_array($storage);
242
        }
243
244
        return $storage;
245
    }
246
247
    /**
248
     * @param array|\Traversable $storage
249
     * @param string             $name
250
     * @param array              $attributes
251
     *
252
     * @return string
253
     */
254 1
    public function asXML($storage, string $name = 'bavix', array $attributes = []): string
255
    {
256 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

256
        $storage = $this->fragments(/** @scrutinizer ignore-type */ $storage);
Loading history...
257 1
        $element = $this->element($name);
258
259 1
        $this->addAttributes($element, $attributes);
260 1
        $this->addAttributes($element, $this->copyright);
261 1
        $this->document()->appendChild($element);
262 1
        $this->convert($element, $this->_convertStorage($storage));
263 1
        $xml = $this->document()->saveXML();
264
265 1
        $this->document = null;
266
267 1
        return $xml;
268
    }
269
270
    /**
271
     * @param DOMElement $element
272
     * @param mixed      $storage
273
     *
274
     * @throws Exceptions\Blank
275
     *
276
     * @codeCoverageIgnore
277
     */
278
    protected function convert(DOMElement $element, $storage)
279
    {
280
        if (\is_object($storage))
281
        {
282
            $element->appendChild(
283
                $element->ownerDocument->importNode($storage)
284
            );
285
286
            return;
287
        }
288
289
        if (!\is_array($storage))
290
        {
291
            $element->nodeValue = htmlspecialchars($storage);
292
293
            return;
294
        }
295
296
        if (empty($storage))
297
        {
298
            throw new Exceptions\Blank('Array is empty');
299
        }
300
301
        $isInt      = Arr::map(Arr::getKeys($storage), '\is_int');
302
        $sequential = !Arr::in($isInt, false);
303
304
        foreach ($storage as $key => $data)
305
        {
306
            if ($sequential)
307
            {
308
                $this->sequential($element, $data);
309
                continue;
310
            }
311
312
            $this->addNodeWithKey($key, $element, $data);
313
        }
314
    }
315
316
    protected function fragments(array $storage)
317
    {
318
319 1
        Arr::walkRecursive($storage, function (&$value) {
320
321 1
            if (\is_object($value)) {
322
323
                if ($value instanceof CData)
324
                {
325
                    $value = $this->document()->createCDATASection((string)$value);
326
                    return;
327
                }
328
329
                if ($value instanceof Raw)
330
                {
331
                    $fragment = $this->document()->createDocumentFragment();
332
                    $fragment->appendXML((string)$value);
333
                    $value = $fragment;
334
                    return;
335
                }
336
337
            }
338
339 1
        });
340
341 1
        return $storage;
342
343
    }
344
345
    /**
346
     * @param string     $key
347
     * @param DOMElement $element
348
     * @param mixed      $storage
349
     *
350
     * @codeCoverageIgnore
351
     */
352
    protected function addNodeWithKey($key, DOMElement $element, $storage)
353
    {
354
        if ($key === '@attributes')
355
        {
356
357
            $this->addAttributes($element, $storage);
358
359
        }
360
        else if ($key === '@value')
361
        {
362
363
            if (\is_string($storage))
364
            {
365
                $element->nodeValue = $storage;
366
367
                return;
368
            }
369
370
            $dom      = new \DOMDocument();
0 ignored issues
show
Bug introduced by
The call to DOMDocument::__construct() has too few arguments starting with version. ( Ignorable by Annotation )

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

370
            $dom      = /** @scrutinizer ignore-call */ new \DOMDocument();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
371
            $fragment = $element->ownerDocument->createDocumentFragment();
372
373
            $dom->loadXML(
374
                (new XMLReader())->asXML($storage, 'root', $this->namespaces)
375
            );
376
377
            /**
378
             * @var $childNode \DOMText
379
             */
380
            foreach ($dom->firstChild->childNodes as $childNode)
381
            {
382
                $fragment->appendXML(
383
                    $childNode->ownerDocument->saveXML($childNode)
384
                );
385
            }
386
387
            $element->appendChild($fragment);
388
389
        }
390
        else
391
        {
392
            $this->addNode($element, $key, $storage);
393
        }
394
    }
395
396
    /**
397
     * @param DOMElement $element
398
     * @param mixed      $storage
399
     *
400
     * @codeCoverageIgnore
401
     */
402
    protected function sequential(DOMElement $element, $storage)
403
    {
404
        if (\is_array($storage))
405
        {
406
            $this->addCollectionNode($element, $storage);
407
408
            return;
409
        }
410
411
        $this->addSequentialNode($element, $storage);
412
    }
413
414
    /**
415
     * @param DOMElement $element
416
     * @param string     $key
417
     * @param mixed      $value
418
     *
419
     * @throws Exceptions\Blank
420
     *
421
     * @codeCoverageIgnore
422
     */
423
    protected function addNode(DOMElement $element, $key, $value)
424
    {
425
        $key   = \str_replace(' ', '-', $key);
426
        $child = $this->element($key);
427
        $element->appendChild($child);
428
        $this->convert($child, $value);
429
    }
430
431
    /**
432
     * @param DOMElement $element
433
     * @param mixed      $value
434
     *
435
     * @throws Exceptions\Blank
436
     *
437
     * @codeCoverageIgnore
438
     */
439
    protected function addCollectionNode(DOMElement $element, $value)
440
    {
441
        if ($element->childNodes->length === 0)
442
        {
443
            $this->convert($element, $value);
444
445
            return;
446
        }
447
448
        /**
449
         * @var $child DOMElement
450
         */
451
        $child = $this->element($element->nodeName);
452
//        $child = $element->cloneNode();
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
453
        $element->parentNode->appendChild($child);
454
        $this->convert($child, $value);
455
    }
456
457
    /**
458
     * @param DOMElement $element
459
     * @param mixed      $value
460
     *
461
     * @codeCoverageIgnore
462
     */
463
    protected function addSequentialNode(DOMElement $element, $value)
464
    {
465
        if (empty($element->nodeValue))
466
        {
467
            $element->nodeValue = \htmlspecialchars($value);
468
469
            return;
470
        }
471
472
        $child = $this->element($element->nodeName);
473
//        $child = $element->cloneNode();
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
474
        $child->nodeValue = \htmlspecialchars($value);
475
        $element->parentNode->appendChild($child);
476
    }
477
478
    /**
479
     * @param DOMElement         $element
480
     * @param array|\Traversable $storage
481
     *
482
     * @codeCoverageIgnore
483
     */
484
    protected function addAttributes(DOMElement $element, $storage)
485
    {
486
        foreach ($storage as $attrKey => $attrVal)
487
        {
488
            if (strpos($attrKey, ':') !== false)
489
            {
490
                $this->namespaces[$attrKey] = $attrVal;
491
            }
492
493
            $element->setAttribute($attrKey, $attrVal);
494
        }
495
    }
496
497
}
498