Passed
Push — master ( a8bdfa...0cad3e )
by Esteban De La Fuente
09:10
created

XmlEncoder   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 244
Duplicated Lines 0 %

Test Coverage

Coverage 68.28%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 75
c 2
b 0
f 0
dl 0
loc 244
ccs 56
cts 82
cp 0.6828
rs 10
wmc 27

5 Methods

Rating   Name   Duplication   Size   Complexity  
B nodeAddChilds() 0 55 8
A nodeAddValue() 0 17 2
C encode() 0 74 12
A nodeAddAttributes() 0 16 4
A skipValue() 0 3 1
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 DOMNode;
29
use InvalidArgumentException;
30
31
/**
32
 * Clase `XmlEncoder` que crea un documento XML a partir de un arreglo PHP.
33
 */
34
class XmlEncoder
35
{
36
    /**
37
     * Reglas para convertir de arreglo de PHP a XML y viceversa.
38
     *
39
     * @var array
40
     */
41
    private static array $rules = [
42
        // ¿Cómo se deben procesar los valores de los nodos?.
43
        'node_values' => [
44
            // Valores que hacen que el nodo no se genere (se omite).
45
            'skip_generation' => [null, false, []],
46
            // Valores que generan un nodo vacío.
47
            'generate_empty' => ['', true],
48
        ],
49
    ];
50
51
    /**
52
     * Convierte un arreglo PHP a un documento XML, generando los nodos y
53
     * respetando un espacio de nombres si se proporciona.
54
     *
55
     * @param array $data Arreglo con los datos que se usarán para generar XML.
56
     * @param array|null $namespace Espacio de nombres para el XML (URI y
57
     * prefijo).
58
     * @param DOMElement|null $parent Elemento padre para los nodos, o null
59
     * para que sea la raíz.
60
     * @param XmlDocument $doc El documento raíz del XML que se genera.
61
     * @return XmlDocument
62
     */
63 189
    public static function encode(
64
        array $data,
65
        ?array $namespace = null,
66
        ?DOMElement $parent = null,
67
        ?XmlDocument $doc = null
68
    ): XmlDocument {
69
        // Si no hay un documento XML completo (desde raíz, no vale un nodo),
70
        // entonces se crea, pues se necesitará para crear los futuros nodos.
71 189
        if ($doc === null) {
72 186
            $doc = new XmlDocument();
73
        }
74
75
        // Si no hay un elemento padre, entonces se está pidiendo crear el
76
        // documento XML desde 0 (desde el nodo raíz).
77 189
        if ($parent === null) {
78 189
            $parent = $doc;
79
        }
80
81
        // Iterar el primer nivel del arreglo para encontrar los tags que se
82
        // deben agregar al documento XML.
83 189
        foreach ($data as $key => $value) {
84
85
            // Si el índice es '@attributes' entonces el valor de este índice
86
            // es un arreglo donde la llave es el nombre del atributo del tag
87
            // de $parent y el valor es el valor del atributo.
88 185
            if ($key === '@attributes') {
89
                // Solo se agregan atributos si el valor es un arreglo.
90 162
                if (is_array($value)) {
91
                    // En la primera iteración de recursividad se debe revisar
92
                    // que $parent sea DOMElement. Y solo en ese caso seguir.
93 162
                    if ($parent instanceof DOMElement) {
94 162
                        self::nodeAddAttributes($parent, $value);
95
                    }
96
                }
97
            }
98
99
            // Si el índice es '@value' entonces se debe asignar directamente
100
            // el valor al nodo, pues es un escalar (no un arreglo con nodos
101
            // hijos). Este caso normalmente se usa cuando se crea un nodo que
102
            // debe tener valor y atributos.
103 185
            elseif ($key === '@value') {
104 109
                if (!self::skipValue($value)) {
105 109
                    $parent->nodeValue = XmlUtils::sanitize($value);
106
                }
107
            }
108
109
            // Acá el índice es el nombre de un nodo. En este caso, el nodo es
110
            // un arreglo. Por lo que se procesará recursivamente para agregar
111
            // a este nodo los nodos hijos que están en el arreglo.
112 185
            elseif (is_array($value)) {
113
                // Solo se crea el nodo si tiene nodos hijos. El nodo no será
114
                // creado si se pasa un arreglo vacio (sin hijos).
115 185
                if (!empty($value)) {
116 185
                    self::nodeAddChilds($doc, $parent, $key, $value, $namespace);
117
                }
118
            }
119
120
            // El nodo es un escalar (no es un arreglo, no son nodos hijos).
121
            // Por lo que se crea el nodo y asigna el valor directamente.
122
            else {
123 179
                if (!self::skipValue($value)) {
124 179
                    self::nodeAddValue(
125 179
                        $doc,
126 179
                        $parent,
127 179
                        $key,
128 179
                        (string) $value,
129 179
                        $namespace
130 179
                    );
131
                }
132
            }
133
        }
134
135
        // Entregar el documento XML generado.
136 189
        return $doc;
137
    }
138
139
    /**
140
     * Agrega atributos a un nodo XML a partir de un arreglo.
141
     *
142
     * @param DOMElement $node Nodo al que se agregarán los atributos.
143
     * @param array $attributes Arreglo de atributos (clave => valor).
144
     * @return void
145
     * @throws InvalidArgumentException Si un valor de atributo es un arreglo.
146
     */
147 162
    private static function nodeAddAttributes(DOMElement $node, array $attributes): void
148
    {
149 162
        foreach ($attributes as $attribute => $value) {
150
            // Si el valor del atributo es un arreglo no se puede asignar.
151 162
            if (is_array($value)) {
152
                throw new InvalidArgumentException(sprintf(
153
                    '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',
154
                    $attribute,
155
                    $node->tagName,
156
                    json_encode($value)
157
                ));
158
            }
159
            // Asignar el valor del atributo solo si no se debe omitir según
160
            // el tipo valor que se quiera asignar.
161 162
            if (!self::skipValue($value)) {
162 162
                $node->setAttribute($attribute, $value);
163
            }
164
        }
165
    }
166
167
    /**
168
     * Agrega nodos hijos a un nodo XML a partir de un arreglo.
169
     *
170
     * @param XmlDocument $doc Documento XML en el que se agregarán los nodos.
171
     * @param DOMNode $parent Nodo padre al que se agregarán los
172
     * nodos hijos.
173
     * @param string $tagName Nombre del tag del nodo hijo.
174
     * @param array $childs Arreglo de datos de los nodos hijos.
175
     * @param array|null $namespace Espacio de nombres para el XML (URI y
176
     * prefijo).
177
     * @return void
178
     * @throws InvalidArgumentException Si un nodo hijo no es un arreglo.
179
     */
180 185
    private static function nodeAddChilds(
181
        XmlDocument $doc,
182
        DOMNode $parent,
183
        string $tagName,
184
        array $childs,
185
        ?array $namespace = null,
186
    ): void {
187 185
        $keys = array_keys($childs);
188 185
        if (!is_int($keys[0])) {
189 185
            $childs = [$childs];
190
        }
191 185
        foreach ($childs as $child) {
192
            // Omitir valores que deben ser saltados.
193 185
            if (self::skipValue($child)) {
194
                continue;
195
            }
196
197
            // Si el hijo es un arreglo se crea un nodo para el hijo y se
198
            // agregan los elementos que están en el arreglo.
199 185
            if (is_array($child)) {
200
201
                // Si el arreglo no es asociativo (con nuevos nodos) error.
202 185
                if (isset($child[0])) {
203
                    throw new InvalidArgumentException(sprintf(
204
                        'El nodo "%s" permite incluir arreglos, pero deben ser arreglos con otros nodos. El valor actual es incorrecto: %s',
205
                        $tagName,
206
                        json_encode($child)
207
                    ));
208
                }
209
210
                // Agregar nodos hijos del nodo hijo (agregar
211
                // asociativo al nodo $tagName).
212 185
                $Node = $namespace
213
                    ? $doc->createElementNS(
214
                        $namespace[0],
215
                        $namespace[1] . ':' . $tagName
216
                    )
217 185
                    : $doc->createElement($tagName)
218 185
                ;
219 185
                $parent->appendChild($Node);
220 185
                self::encode($child, $namespace, $Node, $doc);
221
            }
222
            // Si el hijo no es un arreglo, es simplemente un nodo duplicado
223
            // que se debe agregar en el mismo nivel que el nodo padre.
224
            else {
225 3
                $value = XmlUtils::sanitize((string) $child);
226 3
                $Node = $namespace
227
                    ? $doc->createElementNS(
228
                        $namespace[0],
229
                        $namespace[1] . ':' . $tagName,
230
                        $value
231
                    )
232 3
                    : $doc->createElement($tagName, $value)
233 3
                ;
234 3
                $parent->appendChild($Node);
235
            }
236
        }
237
    }
238
239
    /**
240
     * Agrega un nodo XML con un valor escalar a un nodo padre.
241
     *
242
     * @param XmlDocument $doc Documento XML en el que se agregarán los nodos.
243
     * @param DOMNode $parent Nodo padre al que se agregará el nodo hijo.
244
     * @param string $tagName Nombre del tag del nodo hijo.
245
     * @param string $value Valor del nodo hijo.
246
     * @param array|null $namespace Espacio de nombres para el XML (URI y
247
     * prefijo).
248
     * @return void
249
     */
250 179
    private static function nodeAddValue(
251
        XmlDocument $doc,
252
        DOMNode $parent,
253
        string $tagName,
254
        string $value,
255
        ?array $namespace = null,
256
    ): void {
257 179
        $value = XmlUtils::sanitize($value);
258 179
        $Node = $namespace
259
            ? $doc->createElementNS(
260
                $namespace[0],
261
                $namespace[1] . ':' . $tagName,
262
                $value
263
            )
264 179
            : $doc->createElement($tagName, $value)
265 179
        ;
266 179
        $parent->appendChild($Node);
267
    }
268
269
    /**
270
     * Verifica si un valor debe omitirse al generar un nodo XML.
271
     *
272
     * @param mixed $value Valor a verificar.
273
     * @return bool `true` si el valor debe omitirse, `false` en caso contrario.
274
     */
275 185
    private static function skipValue(mixed $value): bool
276
    {
277 185
        return in_array($value, self::$rules['node_values']['skip_generation'], true);
278
    }
279
280
    /**
281
     * Verifica si un valor debe generar un nodo XML vacío.
282
     *
283
     * @param mixed $value Valor a verificar.
284
     * @return bool `true` si el valor debe generar un nodo vacío, `false` en
285
     * caso contrario.
286
     */
287
    // private static function createWithEmptyValue(mixed $value): bool
288
    // {
289
    //     return in_array($value, self::$rules['node_values']['generate_empty'], true);
290
    // }
291
}
292