Passed
Push — master ( a08a48...8426a1 )
by Esteban De La Fuente
05:58
created

XmlEncoder   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Test Coverage

Coverage 67.9%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 74
c 1
b 0
f 0
dl 0
loc 240
ccs 55
cts 81
cp 0.679
rs 10
wmc 26

5 Methods

Rating   Name   Duplication   Size   Complexity  
B nodeAddChilds() 0 55 8
A nodeAddValue() 0 17 2
B encode() 0 70 11
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 123
    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 123
        if ($doc === null) {
72 120
            $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 123
        if ($parent === null) {
78 123
            $parent = $doc;
79
        }
80
81
        // Iterar el primer nivel del arreglo para encontrar los tags que se
82
        // deben agregar al documento XML.
83 123
        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 119
            if ($key === '@attributes') {
89
                // Solo se agregan atributos si el valor es un arreglo.
90 96
                if (is_array($value)) {
91 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

91
                    self::nodeAddAttributes(/** @scrutinizer ignore-type */ $parent, $value);
Loading history...
92
                }
93
            }
94
95
            // Si el índice es '@value' entonces se debe asignar directamente
96
            // el valor al nodo, pues es un escalar (no un arreglo con nodos
97
            // hijos). Este caso normalmente se usa cuando se crea un nodo que
98
            // debe tener valor y atributos.
99 119
            elseif ($key === '@value') {
100 65
                if (!self::skipValue($value)) {
101 65
                    $parent->nodeValue = XmlUtils::sanitize($value);
102
                }
103
            }
104
105
            // Acá el índice es el nombre de un nodo. En este caso, el nodo es
106
            // un arreglo. Por lo que se procesará recursivamente para agregar
107
            // a este nodo los nodos hijos que están en el arreglo.
108 119
            elseif (is_array($value)) {
109
                // Solo se crea el nodo si tiene nodos hijos. El nodo no será
110
                // creado si se pasa un arreglo vacio (sin hijos).
111 119
                if (!empty($value)) {
112 119
                    self::nodeAddChilds($doc, $parent, $key, $value, $namespace);
113
                }
114
            }
115
116
            // El nodo es un escalar (no es un arreglo, no son nodos hijos).
117
            // Por lo que se crea el nodo y asigna el valor directamente.
118
            else {
119 113
                if (!self::skipValue($value)) {
120 113
                    self::nodeAddValue(
121 113
                        $doc,
122 113
                        $parent,
123 113
                        $key,
124 113
                        (string) $value,
125 113
                        $namespace
126 113
                    );
127
                }
128
            }
129
        }
130
131
        // Entregar el documento XML generado.
132 123
        return $doc;
133
    }
134
135
    /**
136
     * Agrega atributos a un nodo XML a partir de un arreglo.
137
     *
138
     * @param DOMElement $node Nodo al que se agregarán los atributos.
139
     * @param array $attributes Arreglo de atributos (clave => valor).
140
     * @return void
141
     * @throws InvalidArgumentException Si un valor de atributo es un arreglo.
142
     */
143 96
    private static function nodeAddAttributes(DOMElement $node, array $attributes): void
144
    {
145 96
        foreach ($attributes as $attribute => $value) {
146
            // Si el valor del atributo es un arreglo no se puede asignar.
147 96
            if (is_array($value)) {
148
                throw new InvalidArgumentException(sprintf(
149
                    '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',
150
                    $attribute,
151
                    $node->tagName,
152
                    json_encode($value)
153
                ));
154
            }
155
            // Asignar el valor del atributo solo si no se debe omitir según
156
            // el tipo valor que se quiera asignar.
157 96
            if (!self::skipValue($value)) {
158 96
                $node->setAttribute($attribute, $value);
159
            }
160
        }
161
    }
162
163
    /**
164
     * Agrega nodos hijos a un nodo XML a partir de un arreglo.
165
     *
166
     * @param XmlDocument $doc Documento XML en el que se agregarán los nodos.
167
     * @param DOMNode $parent Nodo padre al que se agregarán los
168
     * nodos hijos.
169
     * @param string $tagName Nombre del tag del nodo hijo.
170
     * @param array $childs Arreglo de datos de los nodos hijos.
171
     * @param array|null $namespace Espacio de nombres para el XML (URI y
172
     * prefijo).
173
     * @return void
174
     * @throws InvalidArgumentException Si un nodo hijo no es un arreglo.
175
     */
176 119
    private static function nodeAddChilds(
177
        XmlDocument $doc,
178
        DOMNode $parent,
179
        string $tagName,
180
        array $childs,
181
        ?array $namespace = null,
182
    ): void {
183 119
        $keys = array_keys($childs);
184 119
        if (!is_int($keys[0])) {
185 119
            $childs = [$childs];
186
        }
187 119
        foreach ($childs as $child) {
188
            // Omitir valores que deben ser saltados.
189 119
            if (self::skipValue($child)) {
190
                continue;
191
            }
192
193
            // Si el hijo es un arreglo se crea un nodo para el hijo y se
194
            // agregan los elementos que están en el arreglo.
195 119
            if (is_array($child)) {
196
197
                // Si el arreglo no es asociativo (con nuevos nodos) error.
198 119
                if (isset($child[0])) {
199
                    throw new InvalidArgumentException(sprintf(
200
                        'El nodo "%s" permite incluir arreglos, pero deben ser arreglos con otros nodos. El valor actual es incorrecto: %s',
201
                        $tagName,
202
                        json_encode($child)
203
                    ));
204
                }
205
206
                // Agregar nodos hijos del nodo hijo (agregar
207
                // asociativo al nodo $tagName).
208 119
                $Node = $namespace
209
                    ? $doc->createElementNS(
210
                        $namespace[0],
211
                        $namespace[1] . ':' . $tagName
212
                    )
213 119
                    : $doc->createElement($tagName)
214 119
                ;
215 119
                $parent->appendChild($Node);
216 119
                self::encode($child, $namespace, $Node, $doc);
217
            }
218
            // Si el hijo no es un arreglo, es simplemente un nodo duplicado
219
            // que se debe agregar en el mismo nivel que el nodo padre.
220
            else {
221 3
                $value = XmlUtils::sanitize((string) $child);
222 3
                $Node = $namespace
223
                    ? $doc->createElementNS(
224
                        $namespace[0],
225
                        $namespace[1] . ':' . $tagName,
226
                        $value
227
                    )
228 3
                    : $doc->createElement($tagName, $value)
229 3
                ;
230 3
                $parent->appendChild($Node);
231
            }
232
        }
233
    }
234
235
    /**
236
     * Agrega un nodo XML con un valor escalar a un nodo padre.
237
     *
238
     * @param XmlDocument $doc Documento XML en el que se agregarán los nodos.
239
     * @param DOMNode $parent Nodo padre al que se agregará el nodo hijo.
240
     * @param string $tagName Nombre del tag del nodo hijo.
241
     * @param string $value Valor del nodo hijo.
242
     * @param array|null $namespace Espacio de nombres para el XML (URI y
243
     * prefijo).
244
     * @return void
245
     */
246 113
    private static function nodeAddValue(
247
        XmlDocument $doc,
248
        DOMNode $parent,
249
        string $tagName,
250
        string $value,
251
        ?array $namespace = null,
252
    ): void {
253 113
        $value = XmlUtils::sanitize($value);
254 113
        $Node = $namespace
255
            ? $doc->createElementNS(
256
                $namespace[0],
257
                $namespace[1] . ':' . $tagName,
258
                $value
259
            )
260 113
            : $doc->createElement($tagName, $value)
261 113
        ;
262 113
        $parent->appendChild($Node);
263
    }
264
265
    /**
266
     * Verifica si un valor debe omitirse al generar un nodo XML.
267
     *
268
     * @param mixed $value Valor a verificar.
269
     * @return bool `true` si el valor debe omitirse, `false` en caso contrario.
270
     */
271 119
    private static function skipValue(mixed $value): bool
272
    {
273 119
        return in_array($value, self::$rules['node_values']['skip_generation'], true);
274
    }
275
276
    /**
277
     * Verifica si un valor debe generar un nodo XML vacío.
278
     *
279
     * @param mixed $value Valor a verificar.
280
     * @return bool `true` si el valor debe generar un nodo vacío, `false` en
281
     * caso contrario.
282
     */
283
    // private static function createWithEmptyValue(mixed $value): bool
284
    // {
285
    //     return in_array($value, self::$rules['node_values']['generate_empty'], true);
286
    // }
287
}
288