Completed
Push — master ( a5bda6...390c8c )
by Бабичев
02:27
created

XMLReader::addNodeWithKey()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 5
nop 3
dl 0
loc 34
ccs 0
cts 0
cp 0
crap 30
rs 8.439
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
            if (\is_string($storage))
298
            {
299
                $element->nodeValue = \htmlspecialchars($storage);
300
301
                return;
302
            }
303
304
            $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

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