Passed
Push — master ( c02ff8...8cf6c5 )
by Esteban De La Fuente
06:05
created

XmlConverter::arrayAddChilds()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 54
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 13.6578

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 33
c 1
b 0
f 0
nc 12
nop 4
dl 0
loc 54
ccs 24
cts 31
cp 0.7742
crap 13.6578
rs 6.9666

How to fix   Long Method    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
 * LibreDTE: Biblioteca PHP (Núcleo).
7
 * Copyright (C) LibreDTE <https://www.libredte.cl>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (a su elección) cualquier versión posterior de la misma.
13
 *
14
 * Este programa se distribuye con la esperanza de que sea útil, pero SIN
15
 * GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD
16
 * PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de
20
 * GNU junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace libredte\lib\Core\Xml;
26
27
use DOMElement;
28
use DOMNodeList;
29
use DOMText;
30
use InvalidArgumentException;
31
32
/**
33
 * Clase `XmlConverter` que proporciona métodos para convertir entre arreglos
34
 * PHP y documentos XML, permitiendo generar nodos XML a partir de datos
35
 * estructurados y viceversa.
36
 */
37
class XmlConverter
38
{
39
    /**
40
     * Reglas para convertir de arreglo de PHP a XML y viceversa.
41
     *
42
     * @var array
43
     */
44
    private static array $rules = [
45
        // ¿Cómo se deben procesar los valores de los nodos?.
46
        'node_values' => [
47
            // Valores que hacen que el nodo no se genere (se omite).
48
            'skip_generation' => [null, false, []],
49
            // Valores que generan un nodo vacío.
50
            'generate_empty' => ['', true],
51
        ],
52
    ];
53
54
    /*
55
    |--------------------------------------------------------------------------
56
    | Métodos para convertir un arreglo PHP a un documento XML.
57
    |--------------------------------------------------------------------------
58
    */
59
60
    /**
61
     * Convierte un arreglo PHP a un documento XML, generando los nodos y
62
     * respetando un espacio de nombres si se proporciona.
63
     *
64
     * @param array $data Arreglo con los datos que se usarán para generar XML.
65
     * @param array|null $namespace Espacio de nombres para el XML (URI y
66
     * prefijo).
67
     * @param DOMElement|null $parent Elemento padre para los nodos, o null
68
     * para que sea la raíz.
69
     * @param XmlDocument $doc El documento raíz del XML que se genera.
70
     * @return XmlDocument|DOMElement
71
     */
72 123
    public static function arrayToXml(
73
        array $data,
74
        ?array $namespace = null,
75
        ?DOMElement $parent = null,
76
        ?XmlDocument $doc = null
77
    ): XmlDocument|DOMElement {
78
        // Si no hay un documento XML completo (desde raíz, no vale un nodo),
79
        // entonces se crea, pues se necesitará para crear los futuros nodos.
80 123
        if ($doc === null) {
81 120
            $doc = new XmlDocument();
82
        }
83
84
        // Si no hay un elemento padre, entonces se está pidiendo crear el
85
        // documento XML desde 0 (desde el nodo raíz).
86 123
        if ($parent === null) {
87 123
            $parent = $doc;
88
        }
89
90
        // Iterar el primer nivel del arreglo para encontrar los tags que se
91
        // deben agregar al documento XML.
92 123
        foreach ($data as $key => $value) {
93
94
            // Si el índice es '@attributes' entonces el valor de este índice
95
            // es un arreglo donde la llave es el nombre del atributo del tag
96
            // de $parent y el valor es el valor del atributo.
97 119
            if ($key === '@attributes') {
98
                // Solo se agregan atributos si el valor es un arreglo.
99 96
                if (is_array($value)) {
100 96
                    self::nodeAddAttributes($parent, $value);
0 ignored issues
show
Bug introduced by
It seems like $parent can also be of type libredte\lib\Core\Xml\XmlDocument; however, parameter $node of libredte\lib\Core\Xml\Xm...er::nodeAddAttributes() does only seem to accept DOMElement, 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

100
                    self::nodeAddAttributes(/** @scrutinizer ignore-type */ $parent, $value);
Loading history...
101
                }
102
            }
103
104
            // Si el índice es '@value' entonces se debe asignar directamente
105
            // el valor al nodo, pues es un escalar (no un arreglo con nodos
106
            // hijos). Este caso normalmente se usa cuando se crea un nodo que
107
            // debe tener valor y atributos.
108 119
            elseif ($key === '@value') {
109 65
                if (!self::skipValue($value)) {
110 65
                    $parent->nodeValue = XmlUtils::sanitize($value);
111
                }
112
            }
113
114
            // Acá el índice es el nombre de un nodo. En este caso, el nodo es
115
            // un arreglo. Por lo que se procesará recursivamente para agregar
116
            // a este nodo los nodos hijos que están en el arreglo.
117 119
            elseif (is_array($value)) {
118
                // Solo se crea el nodo si tiene nodos hijos. El nodo no será
119
                // creado si se pasa un arreglo vacio (sin hijos).
120 119
                if (!empty($value)) {
121 119
                    self::nodeAddChilds($doc, $parent, $key, $value, $namespace);
122
                }
123
            }
124
125
            // El nodo es un escalar (no es un arreglo, no son nodos hijos).
126
            // Por lo que se crea el nodo y asigna el valor directamente.
127
            else {
128 113
                if (!self::skipValue($value)) {
129 113
                    self::nodeAddValue(
130 113
                        $doc,
131 113
                        $parent,
0 ignored issues
show
Bug introduced by
It seems like $parent can also be of type libredte\lib\Core\Xml\XmlDocument; however, parameter $parent of libredte\lib\Core\Xml\XmlConverter::nodeAddValue() does only seem to accept DOMElement, 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

131
                        /** @scrutinizer ignore-type */ $parent,
Loading history...
132 113
                        $key,
133 113
                        (string) $value,
134 113
                        $namespace
135 113
                    );
136
                }
137
            }
138
        }
139
140
        // Entregar el documento XML generado.
141 123
        return $parent;
142
    }
143
144
    /**
145
     * Agrega atributos a un nodo XML a partir de un arreglo.
146
     *
147
     * @param DOMElement $node Nodo al que se agregarán los atributos.
148
     * @param array $attributes Arreglo de atributos (clave => valor).
149
     * @return void
150
     * @throws InvalidArgumentException Si un valor de atributo es un arreglo.
151
     */
152 96
    private static function nodeAddAttributes(DOMElement $node, array $attributes): void
153
    {
154 96
        foreach ($attributes as $attribute => $value) {
155
            // Si el valor del atributo es un arreglo no se puede asignar.
156 96
            if (is_array($value)) {
157
                throw new InvalidArgumentException(sprintf(
158
                    'El tipo de dato del valor ingresado para el atributo "%s" del nodo "%s" es incorrecto (no puede ser un arreglo). El valor es: %s',
159
                    $attribute,
160
                    $node->tagName,
161
                    json_encode($value)
162
                ));
163
            }
164
            // Asignar el valor del atributo solo si no se debe omitir según
165
            // el tipo valor que se quiera asignar.
166 96
            if (!self::skipValue($value)) {
167 96
                $node->setAttribute($attribute, $value);
168
            }
169
        }
170
    }
171
172
    /**
173
     * Agrega nodos hijos a un nodo XML a partir de un arreglo.
174
     *
175
     * @param XmlDocument $doc Documento XML en el que se agregarán los nodos.
176
     * @param XmlDocument|DOMElement $parent Nodo padre al que se agregarán los
177
     * nodos hijos.
178
     * @param string $tagName Nombre del tag del nodo hijo.
179
     * @param array $childs Arreglo de datos de los nodos hijos.
180
     * @param array|null $namespace Espacio de nombres para el XML (URI y
181
     * prefijo).
182
     * @return void
183
     * @throws InvalidArgumentException Si un nodo hijo no es un arreglo.
184
     */
185 119
    private static function nodeAddChilds(
186
        XmlDocument $doc,
187
        XmlDocument|DOMElement $parent,
188
        string $tagName,
189
        array $childs,
190
        ?array $namespace = null,
191
    ): void {
192 119
        $keys = array_keys($childs);
193 119
        if (!is_int($keys[0])) {
194 119
            $childs = [$childs];
195
        }
196 119
        foreach ($childs as $child) {
197
            // Omitir valores que deben ser saltados.
198 119
            if (self::skipValue($child)) {
199
                continue;
200
            }
201
202
            // Si el hijo es un arreglo se crea un nodo para el hijo y se
203
            // agregan los elementos que están en el arreglo.
204 119
            if (is_array($child)) {
205
206
                // Si el arreglo no es asociativo (con nuevos nodos) error.
207 119
                if (isset($child[0])) {
208
                    throw new InvalidArgumentException(sprintf(
209
                        'El nodo "%s" permite incluir arreglos, pero deben ser arreglos con otros nodos. El valor actual es incorrecto: %s',
210
                        $tagName,
211
                        json_encode($child)
212
                    ));
213
                }
214
215
                // Agregar nodos hijos del nodo hijo (agregar
216
                // asociativo al nodo $tagName).
217 119
                $Node = $namespace
218
                    ? $doc->createElementNS(
219
                        $namespace[0],
220
                        $namespace[1] . ':' . $tagName
221
                    )
222 119
                    : $doc->createElement($tagName)
223 119
                ;
224 119
                $parent->appendChild($Node);
225 119
                self::arrayToXml($child, $namespace, $Node, $doc);
226
            }
227
            // Si el hijo no es un arreglo, es simplemente un nodo duplicado
228
            // que se debe agregar en el mismo nivel que el nodo padre.
229
            else {
230 3
                $value = XmlUtils::sanitize((string) $child);
231 3
                $Node = $namespace
232
                    ? $doc->createElementNS(
233
                        $namespace[0],
234
                        $namespace[1] . ':' . $tagName,
235
                        $value
236
                    )
237 3
                    : $doc->createElement($tagName, $value)
238 3
                ;
239 3
                $parent->appendChild($Node);
240
            }
241
        }
242
    }
243
244
    /**
245
     * Agrega un nodo XML con un valor escalar a un nodo padre.
246
     *
247
     * @param XmlDocument $doc Documento XML en el que se agregarán los nodos.
248
     * @param DOMElement $parent Nodo padre al que se agregará el nodo hijo.
249
     * @param string $tagName Nombre del tag del nodo hijo.
250
     * @param string $value Valor del nodo hijo.
251
     * @param array|null $namespace Espacio de nombres para el XML (URI y
252
     * prefijo).
253
     * @return void
254
     */
255 113
    private static function nodeAddValue(
256
        XmlDocument $doc,
257
        DOMElement $parent,
258
        string $tagName,
259
        string $value,
260
        ?array $namespace = null,
261
    ): void {
262 113
        $value = XmlUtils::sanitize($value);
263 113
        $Node = $namespace
264
            ? $doc->createElementNS(
265
                $namespace[0],
266
                $namespace[1] . ':' . $tagName,
267
                $value
268
            )
269 113
            : $doc->createElement($tagName, $value)
270 113
        ;
271 113
        $parent->appendChild($Node);
272
    }
273
274
    /**
275
     * Verifica si un valor debe omitirse al generar un nodo XML.
276
     *
277
     * @param mixed $value Valor a verificar.
278
     * @return bool `true` si el valor debe omitirse, `false` en caso contrario.
279
     */
280 119
    private static function skipValue(mixed $value): bool
281
    {
282 119
        return in_array($value, self::$rules['node_values']['skip_generation'], true);
283
    }
284
285
    /**
286
     * Verifica si un valor debe generar un nodo XML vacío.
287
     *
288
     * @param mixed $value Valor a verificar.
289
     * @return bool `true` si el valor debe generar un nodo vacío, `false` en
290
     * caso contrario.
291
     */
292
    // private static function createWithEmptyValue(mixed $value): bool
293
    // {
294
    //     return in_array($value, self::$rules['node_values']['generate_empty'], true);
295
    // }
296
297
    /*
298
    |--------------------------------------------------------------------------
299
    | Métodos para convertir un documento XML a un arreglo PHP.
300
    |--------------------------------------------------------------------------
301
    */
302
303
    /**
304
     * Convierte un documento XML a un arreglo PHP.
305
     *
306
     * @param XmlDocument|DOMElement $documentElement Documento XML que se
307
     * desea convertir a un arreglo de PHP o el elemento donde vamos a hacer la
308
     * conversión si no es el documento XML completo.
309
     * @param array|null $data Arreglo donde se almacenarán los resultados.
310
     * @param bool $twinsAsArray Indica si se deben tratar los nodos gemelos
311
     * como un arreglo.
312
     * @return array Arreglo con la representación del XML.
313
     */
314 89
    public static function xmlToArray(
315
        XmlDocument|DOMElement $documentElement,
316
        ?array &$data = null,
317
        bool $twinsAsArray = false
318
    ): array {
319
        // Si no viene un tagElement se busca uno, si no se obtiene se termina
320
        // la generación.
321 89
        $tagElement = $documentElement instanceof DOMElement
322 86
            ? $documentElement
323 89
            : $documentElement->documentElement
324 89
        ;
325 89
        if (!$tagElement) {
0 ignored issues
show
introduced by
$tagElement is of type DOMElement, thus it always evaluated to true.
Loading history...
326 1
            return [];
327
        }
328
329
        // Índice en el arreglo que representa al tag. Además es un nombre de
330
        // variable más corto :)
331 88
        $key = $tagElement->tagName;
332
333
        // Si no hay un arreglo de destino para los datos se crea un arreglo
334
        // con el índice del nodo principal con valor vacío.
335 88
        if ($data === null) {
336
            //$data = [$key => self::getEmptyValue()];
337 88
            $data = [$key => null];
338
        }
339
340
        // Si el tagElement tiene atributos se agregan al arreglo dentro del
341
        // índice especial '@attributes'.
342 88
        if ($tagElement->hasAttributes()) {
343 72
            $data[$key]['@attributes'] = [];
344 72
            foreach ($tagElement->attributes as $attribute) {
345 72
                $data[$key]['@attributes'][$attribute->name] = $attribute->value;
346
            }
347
        }
348
349
        // Si el tagElement tiene nodos hijos se agregan al valor del tag.
350 88
        if ($tagElement->hasChildNodes()) {
351 88
            self::arrayAddChilds(
352 88
                $data,
353 88
                $tagElement,
354 88
                $tagElement->childNodes,
355 88
                $twinsAsArray
356 88
            );
357
        }
358
359
        // Entregar los datos del documento XML como un arreglo.
360 88
        return $data;
361
    }
362
363
    /**
364
     * Agrega nodos hijos de un documento XML a un arreglo PHP.
365
     *
366
     * @param array &$data Arreglo donde se agregarán los nodos hijos.
367
     * @param DOMElement $tagElement Nodo padre del que se extraerán los nodos
368
     * hijos.
369
     * @param DOMNodeList $childs Lista de nodos hijos del nodo padre.
370
     * @param bool $twinsAsArray Indica si se deben tratar los nodos gemelos
371
     * como un arreglo.
372
     * @return void
373
     */
374 88
    private static function arrayAddChilds(
375
        array &$data,
376
        DOMElement $tagElement,
377
        DOMNodeList $childs,
378
        bool $twinsAsArray,
379
    ): void {
380 88
        $key = $tagElement->tagName;
381
        // Se recorre cada uno de los nodos hijos.
382 88
        foreach ($childs as $child) {
383 88
            if ($child instanceof DOMText) {
384 84
                $textContent = trim($child->textContent);
385 84
                if ($textContent !== '') {
386 84
                    if ($tagElement->hasAttributes()) {
387 66
                        $data[$key]['@value'] = $textContent;
388 82
                    } elseif ($childs->length === 1 && empty($data[$key])) {
389 82
                        $data[$key] = $textContent;
390
                    } else {
391 84
                        $array[$key]['@value'] = $textContent;
392
                    }
393
                }
394 88
            } elseif ($child instanceof DOMElement) {
395 88
                $n_twinsNodes = self::nodeCountTwins(
396 88
                    $tagElement,
397 88
                    $child->tagName
398 88
                );
399 88
                if ($n_twinsNodes === 1) {
400 86
                    if ($twinsAsArray) {
401
                        self::xmlToArray($child, $data);
402
                    } else {
403 86
                        self::xmlToArray($child, $data[$key]);
404
                    }
405
                } else {
406
                    // Se crea una lista para el nodo hijo, pues tiene varios
407
                    // nodos iguales el XML.
408 4
                    if (!isset($data[$key][$child->tagName])) {
409 4
                        $data[$key][$child->tagName] = [];
410
                    }
411
412
                    // Se revisa si el nodo hijo es escalar. Si lo es, se añade
413
                    // a la lista directamente.
414 4
                    $textContent = trim($child->textContent);
415 4
                    if ($textContent !== '') {
416 4
                        $data[$key][$child->tagName][] = $textContent;
417
                    }
418
                    // Si el nodo hijo es un escalar, sino que es una lista de
419
                    // nodos, se construye como si fuese un arreglo normal con
420
                    // la llamada a xmlToArray().
421
                    else {
422
                        $siguiente = count($data[$key][$child->tagName]);
423
                        $data[$key][$child->tagName][$siguiente] = [];
424
                        self::xmlToArray(
425
                            $child,
426
                            $data[$key][$child->tagName][$siguiente],
427
                            true
428
                        );
429
                    }
430
                }
431
            }
432
        }
433
    }
434
435
    /**
436
     * Cuenta los nodos con el mismo nombre hijos de un DOMElement.
437
     *
438
     * @param DOMElement $dom Elemento DOM donde se buscarán los nodos.
439
     * @param string $tagName Nombre del tag a contar.
440
     * @return int Cantidad de nodos hijos con el mismo nombre.
441
     */
442 88
    private static function nodeCountTwins(DOMElement $dom, string $tagName): int
443
    {
444 88
        $twins = 0;
445 88
        foreach ($dom->childNodes as $child) {
446 88
            if ($child instanceof DOMElement && $child->tagName === $tagName) {
447 88
                $twins++;
448
            }
449
        }
450 88
        return $twins;
451
    }
452
453
    /**
454
     * Obtiene el valor que indica que un nodo debe ser omitido.
455
     *
456
     * @return mixed El valor que indica que un nodo debe ser omitido.
457
     */
458
    // private static function getSkipValue(): mixed
459
    // {
460
    //     return self::$rules['node_values']['skip_generation'][0] ?? null;
461
    // }
462
463
    /**
464
     * Obtiene el valor que indica que un nodo debe ser generado como vacío.
465
     *
466
     * @return mixed El valor que indica que un nodo debe ser generado como
467
     * vacío.
468
     */
469
    // private static function getEmptyValue(): mixed
470
    // {
471
    //     return self::$rules['node_values']['generate_empty'][0] ?? '';
472
    // }
473
474
}
475