Passed
Push — master ( 96803c...b18ae0 )
by Divine Niiquaye
01:37
created

XmlAdapter::phpize()   D

Complexity

Conditions 19
Paths 17

Size

Total Lines 32
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 27
c 1
b 0
f 0
nc 17
nop 1
dl 0
loc 32
rs 4.5166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\DependencyInjection;
19
20
use Closure;
21
use DOMComment;
22
use DOMDocument;
23
use DOMElement;
24
use DOMText;
25
use Exception;
26
use LogicException;
27
use Nette\DI;
28
use Nette\InvalidStateException;
29
use ReflectionObject;
30
use RuntimeException;
31
use stdClass;
32
use XMLWriter;
33
34
/**
35
 * Reading and generating XML/Html files for DI.
36
 *
37
 * @author Divine Niiquaye Ibok <[email protected]>
38
 */
39
final class XmlAdapter implements Di\Config\Adapter
40
{
41
    public function __construct()
42
    {
43
        if (!\extension_loaded('dom') && !\extension_loaded('xmlwriter')) {
44
            throw new LogicException('Extension DOM and XmlWriter is required.');
45
        }
46
    }
47
48
    /**
49
     * Parses an XML string.
50
     *
51
     * @param string               $content          An XML string
52
     * @param null|callable|string $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
53
     *
54
     * @throws InvalidStateException When parsing of XML file returns error
55
     * @throws InvalidXmlException   When parsing of XML with schema or callable produces any errors
56
     *                               unrelated to the XML parsing itself
57
     * @throws RuntimeException      When DOM extension is missing
58
     *
59
     * @return DOMDocument
60
     */
61
    public function parse(string $content, $schemaOrCallable = null): DOMDocument
62
    {
63
        $internalErrors  = \libxml_use_internal_errors(true);
64
        $disableEntities = \libxml_disable_entity_loader(true);
65
        \libxml_clear_errors();
66
67
        $dom                  = new DOMDocument();
68
        $dom->validateOnParse = true;
69
70
        if (!$dom->loadXML($content, \LIBXML_NONET | (\defined('LIBXML_COMPACT') ? \LIBXML_COMPACT : 0))) {
71
            \libxml_disable_entity_loader($disableEntities);
72
73
            $dom->loadHTML($content);
74
        }
75
76
        $dom->normalizeDocument();
77
78
        \libxml_use_internal_errors($internalErrors);
79
        \libxml_disable_entity_loader($disableEntities);
80
81
        if (null !== $schemaOrCallable) {
82
            $internalErrors = \libxml_use_internal_errors(true);
83
            \libxml_clear_errors();
84
85
            $e = null;
86
87
            if (\is_callable($schemaOrCallable)) {
88
                try {
89
                    $valid = $schemaOrCallable($dom, $internalErrors);
90
                } catch (Exception $e) {
91
                    $valid = false;
92
                }
93
            } elseif (!\is_array($schemaOrCallable) && \is_file((string) $schemaOrCallable)) {
94
                $schemaSource = \file_get_contents((string) $schemaOrCallable);
95
                $valid        = @$dom->schemaValidateSource($schemaSource);
96
            } else {
97
                \libxml_use_internal_errors($internalErrors);
98
99
                throw new InvalidStateException(
100
                    'The schemaOrCallable argument has to be a valid path to XSD file or callable.'
101
                );
102
            }
103
104
            if (!$valid) {
105
                $messages = $this->getXmlErrors($internalErrors);
106
107
                if (empty($messages)) {
108
                    throw new RuntimeException('The XML is not valid.', 0, $e);
109
                }
110
111
                throw new InvalidStateException(\implode("\n", $messages), 0, $e);
112
            }
113
        }
114
115
        \libxml_clear_errors();
116
        \libxml_use_internal_errors($internalErrors);
117
118
        return $dom;
119
    }
120
121
    /**
122
     * Converts a \DOMElement object to a PHP array.
123
     *
124
     * The following rules applies during the conversion:
125
     *
126
     *  * Each tag is converted to a key value or an array
127
     *    if there is more than one "value"
128
     *
129
     *  * The content of a tag is set under a "value" key (<foo>bar</foo>)
130
     *    if the tag also has some nested tags
131
     *
132
     *  * The attributes are converted to keys (<foo foo="bar"/>)
133
     *
134
     *  * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
135
     *
136
     * @param DOMElement $element     A \DOMElement instance
137
     * @param bool       $checkPrefix Check prefix in an element or an attribute name
138
     *
139
     * @return mixed
140
     */
141
    public function convertDomElementToArray(DOMElement $element, bool $checkPrefix = true)
142
    {
143
        $prefix = (string) $element->prefix;
144
        $empty  = true;
145
        $config = [];
146
147
        foreach ($element->attributes as $name => $node) {
148
            if ($checkPrefix && !\in_array((string) $node->prefix, ['', $prefix], true)) {
149
                continue;
150
            }
151
            $config[$name] = $this->phpize($node->value);
152
            $empty         = false;
153
        }
154
155
        $nodeValue = false;
156
157
        foreach ($element->childNodes as $node) {
158
            if ($node instanceof DOMText) {
159
                if ('' !== \trim($node->nodeValue)) {
160
                    $nodeValue = \trim($node->nodeValue);
161
                    $empty     = false;
162
                }
163
            } elseif ($checkPrefix && $prefix != (string) $node->prefix) {
164
                continue;
165
            } elseif (!$node instanceof DOMComment) {
166
                $value = $this->convertDomElementToArray($node, $checkPrefix);
167
168
                $key = $node->localName;
169
170
                if (isset($config[$key])) {
171
                    if (!\is_array($config[$key]) || !\is_int(\key($config[$key]))) {
172
                        $config[$key] = [$config[$key]];
173
                    }
174
                    $config[$key][] = $value;
175
                } else {
176
                    $config[$key] = $value;
177
                }
178
179
                $empty = false;
180
            }
181
        }
182
183
        if (false !== $nodeValue) {
184
            $value = $this->phpize($nodeValue);
185
186
            if (\count($config)) {
187
                $config['value'] = $value;
188
            } else {
189
                $config = $value;
190
            }
191
        }
192
193
        return !$empty ? $config : null;
194
    }
195
196
    /**
197
     * Reads configuration from XML data.
198
     *
199
     * {@inheritdoc}
200
     */
201
    public function load(string $file): array
202
    {
203
        $content = $this->parse(\file_get_contents($file));
204
205
        return $this->convertDomElementToArray($content->documentElement);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->convertDom...ntent->documentElement) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
206
    }
207
208
    /**
209
     * Generates configuration in XML format,
210
     * Process the array|objects for dumping.
211
     *
212
     * @param array $config
213
     *
214
     * @return string
215
     */
216
    public function dump(array $data): string
217
    {
218
        $writer = new XMLWriter();
219
        $writer->openMemory();
220
        $writer->setIndent(true);
221
        $writer->setIndentString(\str_repeat(' ', 4));
222
223
        if (isset($config['head'], $config['body'])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $config seems to never exist and therefore isset should always be false.
Loading history...
224
            $writer->writeRaw("<!Doctype html>\n");
225
            $writer->startElement('html');
226
        } else {
227
            $writer->startDocument('1.0', 'UTF-8');
228
            $writer->startElement('container-config');
229
        }
230
231
        $data = \array_map(function ($value) {
232
            $value = $this->resolveClassObject($value);
233
234
            if (\is_array($value)) {
235
                return \array_map([$this, 'resolveClassObject'], $value);
236
            }
237
238
            return $value;
239
        }, $data);
240
241
        foreach ($data as $sectionName => $config) {
242
            if (!\is_array($config)) {
243
                $writer->writeAttribute($sectionName, $this->xmlize($config));
244
            } else {
245
                $this->addBranch($sectionName, $config, $writer);
246
            }
247
        }
248
249
        $writer->endElement();
250
        $writer->endDocument();
251
252
        return $writer->outputMemory();
253
    }
254
255
    /**
256
     * Add a branch to an XML/Html object recursively.
257
     *
258
     * @param string    $branchName
259
     * @param array     $config
260
     * @param XMLWriter $writer
261
     *
262
     * @throws RuntimeException
263
     */
264
    protected function addBranch($branchName, array $config, XMLWriter $writer): void
265
    {
266
        $branchType = null;
267
268
        foreach ($config as $key => $value) {
269
            if ($branchType === null) {
270
                if (\is_numeric($key)) {
271
                    $branchType = 'numeric';
272
                } else {
273
                    $writer->startElement($branchName);
274
                    $branchType = 'string';
275
                }
276
            } elseif ($branchType !== (\is_numeric($key) ? 'numeric' : 'string')) {
277
                throw new RuntimeException('Mixing of string and numeric keys is not allowed');
278
            }
279
280
            if ($branchType === 'numeric') {
281
                if (\is_array($value) || $value instanceof stdClass) {
282
                    $this->addBranch($branchName, (array) $value, $writer);
283
                } else {
284
                    $writer->writeElement($branchName, $this->xmlize($value));
285
                }
286
            } else {
287
                if (\is_array($value)) {
288
                    if (\array_key_exists('value', $value)) {
289
                        $newValue = $value['value'];
290
                        unset($value['value']);
291
292
                        $writer->startElement($key);
293
                        \array_walk_recursive($value, function ($item, $id) use (&$writer): void {
294
                            $writer->writeAttribute($id, $item);
295
                        });
296
297
                        $writer->writeRaw($this->xmlize($newValue));
298
                        $writer->endElement();
299
                        $writer->endAttribute();
300
                    } else {
301
                        $this->addBranch($key, $value, $writer);
302
                    }
303
                } else {
304
                    $writer->writeAttribute($key, $this->xmlize($value));
305
                }
306
            }
307
        }
308
309
        if ($branchType === 'string') {
310
            $writer->endElement();
311
        }
312
    }
313
314
    /**
315
     * Converts an xml value to a PHP type.
316
     *
317
     * @param mixed $value
318
     *
319
     * @return mixed
320
     */
321
    protected function phpize($value)
322
    {
323
        $value          = (string) $value;
324
        $lowercaseValue = \strtolower($value);
325
326
        switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^0x[0-9a-f]++$/i', $value) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^[+-]?[0-9]+(\.[0-9]+)?$/', $value) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
327
            case 'null' === $lowercaseValue:
328
                return null;
329
            case \ctype_digit($value):
330
                $raw  = $value;
331
                $cast = (int) $value;
332
333
                return '0' == $value[0] ? \octdec($value) : (((string) $raw === (string) $cast) ? $cast : $raw);
334
            case isset($value[1]) && '-' === $value[0] && \ctype_digit(\substr($value, 1)):
335
                $raw  = $value;
336
                $cast = (int) $value;
337
338
                return '0' == $value[1] ? \octdec($value) : (((string) $raw === (string) $cast) ? $cast : $raw);
339
            case 'true' === $lowercaseValue:
340
                return true;
341
            case 'false' === $lowercaseValue:
342
                return false;
343
            case isset($value[1]) && '0b' == $value[0] . $value[1] && \preg_match('/^0b[01]*$/', $value):
344
                return \bindec($value);
345
            case \is_numeric($value):
346
                return '0x' === $value[0] . $value[1] ? \hexdec($value) : (float) $value;
347
            case \preg_match('/^0x[0-9a-f]++$/i', $value):
348
                return \hexdec($value);
349
            case \preg_match('/^[+-]?[0-9]+(\.[0-9]+)?$/', $value):
350
                return (float) $value;
351
            default:
352
                return $value;
353
        }
354
    }
355
356
    /**
357
     * Converts an PHP type to a Xml value.
358
     *
359
     * @param mixed $value
360
     *
361
     * @return string
362
     */
363
    protected function xmlize($value): string
364
    {
365
        switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^[+-]?[0-9]...+)?$/', (string)$value) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^0x[0-9a-f]++$/i', (string)$value) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
366
            case null === $value:
367
                return 'null';
368
            case isset($value[1]) && '-' === $value[0] && \ctype_digit(\substr($value, 1)):
369
                $raw  = $value;
370
                $cast = (int) $value;
371
372
                return (string) 0 == $value[1] ? \octdec($value) : (((string) $raw === (string) $cast) ? $cast : $raw);
373
            case true === $value:
374
                return 'true';
375
            case false === $value:
376
                return 'false';
377
            case isset($value[1]) && '0b' == $value[0] . $value[1] && \preg_match('/^0b[01]*$/', (string) $value):
378
                return (string) \bindec($value);
379
            case \preg_match('/^0x[0-9a-f]++$/i', (string) $value):
380
                return \hexdec($value);
381
            case \preg_match('/^[+-]?[0-9]+(\.[0-9]+)?$/', (string) $value):
382
                return (string) (float) $value;
383
            default:
384
                return (string) $value;
385
        }
386
    }
387
388
    protected function resolveClassObject($value)
389
    {
390
        if (\is_object($value) && !$value instanceof Closure) {
391
            $properties = [];
392
393
            foreach ((new ReflectionObject($value))->getProperties() as $property) {
394
                $property->setAccessible(true);
395
                $properties[$property->getName()] = $property->getValue($value);
396
            }
397
398
            return $properties;
399
        }
400
401
        if ($value instanceof stdClass) {
402
            return (array) $value;
403
        }
404
405
        return $value;
406
    }
407
408
    protected function getXmlErrors(bool $internalErrors)
409
    {
410
        $errors = [];
411
412
        foreach (\libxml_get_errors() as $error) {
413
            $errors[] = \sprintf(
414
                '[%s %s] %s (in %s - line %d, column %d)',
415
                \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
416
                $error->code,
417
                \trim($error->message),
418
                $error->file ?: 'n/a',
419
                $error->line,
420
                $error->column
421
            );
422
        }
423
424
        \libxml_clear_errors();
425
        \libxml_use_internal_errors($internalErrors);
426
427
        return $errors;
428
    }
429
}
430