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

XmlEncoder::nodeAddChilds()   B

Complexity

Conditions 8
Paths 14

Size

Total Lines 55
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 14.0087

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 28
c 1
b 0
f 0
nc 14
nop 5
dl 0
loc 55
ccs 18
cts 33
cp 0.5455
crap 14.0087
rs 8.4444

How to fix   Long Method   

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 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