Completed
Push — master ( 8b8bac...fbf963 )
by Бабичев
04:42 queued 02:21
created

XMLReader::addNodeWithKey()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 43
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 6
nop 3
dl 0
loc 43
ccs 0
cts 0
cp 0
crap 56
rs 6.7272
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
        '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
        $this->addAttributes($element, $attributes);
235 1
        $this->addAttributes($element, $this->copyright);
236 1
        $this->document()->appendChild($element);
237 1
        $this->convert($element, $this->_convertStorage($storage));
238 1
        $xml = $this->document()->saveXML();
239
240 1
        $this->document = null;
241
242 1
        return $xml;
243
    }
244
245
    /**
246
     * @param DOMElement $element
247
     * @param mixed      $storage
248
     *
249
     * @throws Exceptions\Blank
250
     *
251
     * @codeCoverageIgnore
252
     */
253
    protected function convert(DOMElement $element, $storage)
254
    {
255
        if (!is_array($storage))
256
        {
257
            $element->nodeValue = htmlspecialchars($storage);
258
259
            return;
260
        }
261
262
        if (empty($storage))
263
        {
264
            throw new Exceptions\Blank('Array is empty');
265
        }
266
267
        $isInt      = Arr::map(Arr::getKeys($storage), '\is_int');
268
        $sequential = !Arr::in($isInt, false);
269
270
        foreach ($storage as $key => $data)
271
        {
272
            if ($sequential)
273
            {
274
                $this->sequential($element, $data);
275
                continue;
276
            }
277
278
            $this->addNodeWithKey($key, $element, $data);
279
        }
280
    }
281
282
    /**
283
     * @param string     $key
284
     * @param DOMElement $element
285
     * @param mixed      $storage
286
     *
287
     * @codeCoverageIgnore
288
     */
289
    protected function addNodeWithKey($key, DOMElement $element, $storage)
290
    {
291
        if ($key === '@attributes')
292
        {
293
            $this->addAttributes($element, $storage);
294
        }
295
        else if ($key === '@value')
296
        {
297
298
            if (\is_object($storage) && $storage instanceof CData)
299
            {
300
                $fragment = $element->ownerDocument->createDocumentFragment();
301
                $fragment->appendXML((string)$storage);
302
                $element->appendChild($fragment);
303
                return;
304
            }
305
306
            if (\is_string($storage))
307
            {
308
                $element->nodeValue = $storage;
309
310
                return;
311
            }
312
313
            $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

313
            $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...
314
            $dom->loadXML(
315
                (new XMLReader())->asXML($storage)
316
            );
317
318
            $fragment = $element->ownerDocument->createDocumentFragment();
319
320
            foreach ($dom->firstChild->childNodes as $value)
321
            {
322
                $fragment->appendXML(
323
                    $value->ownerDocument->saveXML($value)
324
                );
325
            }
326
327
            $element->appendChild($fragment);
328
        }
329
        else
330
        {
331
            $this->addNode($element, $key, $storage);
332
        }
333
    }
334
335
    /**
336
     * @param DOMElement $element
337
     * @param mixed      $storage
338
     *
339
     * @codeCoverageIgnore
340
     */
341
    protected function sequential(DOMElement $element, $storage)
342
    {
343
        if (is_array($storage))
344
        {
345
            $this->addCollectionNode($element, $storage);
346
347
            return;
348
        }
349
350
        $this->addSequentialNode($element, $storage);
351
    }
352
353
    /**
354
     * @param DOMElement $element
355
     * @param string     $key
356
     * @param mixed      $value
357
     *
358
     * @throws Exceptions\Blank
359
     *
360
     * @codeCoverageIgnore
361
     */
362
    protected function addNode(DOMElement $element, $key, $value)
363
    {
364
        $key   = \str_replace(' ', '-', $key);
365
        $child = $this->document()->createElement($key);
366
        $element->appendChild($child);
367
        $this->convert($child, $value);
368
    }
369
370
    /**
371
     * @param DOMElement $element
372
     * @param mixed      $value
373
     *
374
     * @throws Exceptions\Blank
375
     *
376
     * @codeCoverageIgnore
377
     */
378
    protected function addCollectionNode(DOMElement $element, $value)
379
    {
380
        if ($element->childNodes->length === 0)
381
        {
382
            $this->convert($element, $value);
383
384
            return;
385
        }
386
387
        /**
388
         * @var $child DOMElement
389
         */
390
        $child = $this->document()->createElement($element->nodeName);
391
//        $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...
392
        $element->parentNode->appendChild($child);
393
        $this->convert($child, $value);
394
    }
395
396
    /**
397
     * @param DOMElement $element
398
     * @param mixed      $value
399
     *
400
     * @codeCoverageIgnore
401
     */
402
    protected function addSequentialNode(DOMElement $element, $value)
403
    {
404
        if (empty($element->nodeValue))
405
        {
406
            $element->nodeValue = \htmlspecialchars($value);
407
408
            return;
409
        }
410
411
        $child = $this->document()->createElement($element->nodeName);
412
//        $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...
413
        $child->nodeValue = \htmlspecialchars($value);
414
        $element->parentNode->appendChild($child);
415
    }
416
417
    /**
418
     * @param DOMElement         $element
419
     * @param array|\Traversable $storage
420
     *
421
     * @codeCoverageIgnore
422
     */
423
    protected function addAttributes(DOMElement $element, $storage)
424
    {
425
        foreach ($storage as $attrKey => $attrVal)
426
        {
427
            $element->setAttribute($attrKey, $attrVal);
428
        }
429
    }
430
431
}
432