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

XmlConverter::nodeCountTwins()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 5
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 4
rs 10
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