Passed
Branch master (0b4ab1)
by Esteban De La Fuente
74:02 queued 50:02
created

EncoderWorker::encode()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 80
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 12

Importance

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