EncoderWorker::skipValue()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 1
rs 10
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\Contract\XmlInterface;
31
use Derafu\Lib\Core\Package\Prime\Component\Xml\Entity\Xml;
32
use DOMElement;
33
use DOMNode;
34
use InvalidArgumentException;
35
36
/**
37
 * Clase que que crea un documento XML a partir de un arreglo PHP.
38
 */
39
class EncoderWorker extends AbstractWorker implements EncoderWorkerInterface
40
{
41
    /**
42
     * Reglas para convertir de arreglo de PHP a XML y viceversa.
43
     *
44
     * @var array
45
     */
46
    private array $rules = [
47
        // ¿Cómo se deben procesar los valores de los nodos?.
48
        'node_values' => [
49
            // Valores que hacen que el nodo no se genere (se omite).
50
            'skip_generation' => [null, false, []],
51
            // Valores que generan un nodo vacío.
52
            'generate_empty' => ['', true],
53
        ],
54
    ];
55
56
    /**
57
     * {@inheritDoc}
58
     */
59 35
    public function encode(
60
        array $data,
61
        ?array $namespace = null,
62
        ?DOMElement $parent = null,
63
        ?XmlInterface $doc = null
64
    ): XmlInterface {
65
        // Si no hay un documento XML completo (desde raíz, no vale un nodo),
66
        // entonces se crea, pues se necesitará para crear los futuros nodos.
67 35
        if ($doc === null) {
68 32
            $doc = new Xml();
69
        }
70
71
        // Si no hay un elemento padre, entonces se está pidiendo crear el
72
        // documento XML desde 0 (desde el nodo raíz).
73 35
        if ($parent === null) {
74 35
            $parent = $doc;
75
        }
76
77
        // Iterar el primer nivel del arreglo para encontrar los tags que se
78
        // deben agregar al documento XML.
79 35
        foreach ($data as $key => $value) {
80
81
            // Si el índice es '@attributes' entonces el valor de este índice
82
            // es un arreglo donde la llave es el nombre del atributo del tag
83
            // de $parent y el valor es el valor del atributo.
84 31
            if ($key === '@attributes') {
85
                // Solo se agregan atributos si el valor es un arreglo.
86 6
                if (is_array($value)) {
87
                    // En la primera iteración de recursividad se debe revisar
88
                    // que $parent sea DOMElement. Y solo en ese caso seguir.
89 6
                    if ($parent instanceof DOMElement) {
90 6
                        $this->nodeAddAttributes($parent, $value);
91
                    }
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 31
            elseif ($key === '@value') {
100 3
                if (!$this->skipValue($value)) {
101 3
                    $parent->nodeValue = XmlUtil::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 31
            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 31
                if (!empty($value)) {
112 31
                    $this->nodeAddChilds(
113 31
                        $doc,
114 31
                        $parent,
115 31
                        $key,
116 31
                        $value,
117 31
                        $namespace
118 31
                    );
119
                }
120
            }
121
122
            // El nodo es un escalar (no es un arreglo, no son nodos hijos).
123
            // Por lo que se crea el nodo y asigna el valor directamente.
124
            else {
125 25
                if (!$this->skipValue($value)) {
126 25
                    $this->nodeAddValue(
127 25
                        $doc,
128 25
                        $parent,
129 25
                        $key,
130 25
                        (string) $value,
131 25
                        $namespace
132 25
                    );
133
                }
134
            }
135
        }
136
137
        // Entregar el documento XML generado.
138 35
        return $doc;
139
    }
140
141
    /**
142
     * Agrega atributos a un nodo XML a partir de un arreglo.
143
     *
144
     * @param DOMElement $node Nodo al que se agregarán los atributos.
145
     * @param array $attributes Arreglo de atributos (clave => valor).
146
     * @return void
147
     * @throws InvalidArgumentException Si un valor de atributo es un arreglo.
148
     */
149 6
    private function nodeAddAttributes(DOMElement $node, array $attributes): void
150
    {
151 6
        foreach ($attributes as $attribute => $value) {
152
            // Si el valor del atributo es un arreglo no se puede asignar.
153 6
            if (is_array($value)) {
154
                throw new InvalidArgumentException(sprintf(
155
                    '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',
156
                    $attribute,
157
                    $node->tagName,
158
                    json_encode($value)
159
                ));
160
            }
161
162
            // Asignar el valor del atributo solo si no se debe omitir según
163
            // el tipo valor que se quiera asignar.
164 6
            if (!$this->skipValue($value)) {
165 6
                $node->setAttribute($attribute, $value);
166
            }
167
        }
168
    }
169
170
    /**
171
     * Agrega nodos hijos a un nodo XML a partir de un arreglo.
172
     *
173
     * @param XmlInterface $doc Documento XML en el que se agregarán los nodos.
174
     * @param DOMNode $parent Nodo padre al que se agregarán los
175
     * nodos hijos.
176
     * @param string $tagName Nombre del tag del nodo hijo.
177
     * @param array $childs Arreglo de datos de los nodos hijos.
178
     * @param array|null $namespace Espacio de nombres para el XML (URI y
179
     * prefijo).
180
     * @return void
181
     * @throws InvalidArgumentException Si un nodo hijo no es un arreglo.
182
     */
183 31
    private function nodeAddChilds(
184
        XmlInterface $doc,
185
        DOMNode $parent,
186
        string $tagName,
187
        array $childs,
188
        ?array $namespace = null,
189
    ): void {
190 31
        $keys = array_keys($childs);
191 31
        if (!is_int($keys[0])) {
192 31
            $childs = [$childs];
193
        }
194 31
        foreach ($childs as $child) {
195
            // Omitir valores que deben ser saltados.
196 31
            if ($this->skipValue($child)) {
197
                continue;
198
            }
199
200
            // Si el hijo es un arreglo se crea un nodo para el hijo y se
201
            // agregan los elementos que están en el arreglo.
202 31
            if (is_array($child)) {
203
204
                // Si el arreglo no es asociativo (con nuevos nodos) error.
205 31
                if (isset($child[0])) {
206
                    throw new InvalidArgumentException(sprintf(
207
                        'El nodo "%s" permite incluir arreglos, pero deben ser arreglos con otros nodos. El valor actual es incorrecto: %s',
208
                        $tagName,
209
                        json_encode($child)
210
                    ));
211
                }
212
213
                // Agregar nodos hijos del nodo hijo (agregar
214
                // asociativo al nodo $tagName).
215 31
                $Node = $namespace
216
                    ? $doc->createElementNS(
217
                        $namespace[0],
218
                        $namespace[1] . ':' . $tagName
219
                    )
220 31
                    : $doc->createElement($tagName)
221 31
                ;
222 31
                $parent->appendChild($Node);
0 ignored issues
show
Bug introduced by
It seems like $Node can also be of type false; however, parameter $node of DOMNode::appendChild() does only seem to accept DOMNode, 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

222
                $parent->appendChild(/** @scrutinizer ignore-type */ $Node);
Loading history...
223 31
                $this->encode($child, $namespace, $Node, $doc);
0 ignored issues
show
Bug introduced by
It seems like $Node can also be of type false; however, parameter $parent of Derafu\Lib\Core\Package\...EncoderWorker::encode() does only seem to accept DOMElement|null, 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

223
                $this->encode($child, $namespace, /** @scrutinizer ignore-type */ $Node, $doc);
Loading history...
224
            }
225
            // Si el hijo no es un arreglo, es simplemente un nodo duplicado
226
            // que se debe agregar en el mismo nivel que el nodo padre.
227
            else {
228 3
                $value = XmlUtil::sanitize((string) $child);
229 3
                $Node = $namespace
230
                    ? $doc->createElementNS(
231
                        $namespace[0],
232
                        $namespace[1] . ':' . $tagName,
233
                        $value
234
                    )
235 3
                    : $doc->createElement($tagName, $value)
236 3
                ;
237 3
                $parent->appendChild($Node);
238
            }
239
        }
240
    }
241
242
    /**
243
     * Agrega un nodo XML con un valor escalar a un nodo padre.
244
     *
245
     * @param XmlInterface $doc Documento XML en el que se agregarán los nodos.
246
     * @param DOMNode $parent Nodo padre al que se agregará el nodo hijo.
247
     * @param string $tagName Nombre del tag del nodo hijo.
248
     * @param string $value Valor del nodo hijo.
249
     * @param array|null $namespace Espacio de nombres para el XML (URI y
250
     * prefijo).
251
     * @return void
252
     */
253 25
    private function nodeAddValue(
254
        XmlInterface $doc,
255
        DOMNode $parent,
256
        string $tagName,
257
        string $value,
258
        ?array $namespace = null,
259
    ): void {
260 25
        $value = XmlUtil::sanitize($value);
261 25
        $Node = $namespace
262
            ? $doc->createElementNS(
263
                $namespace[0],
264
                $namespace[1] . ':' . $tagName,
265
                $value
266
            )
267 25
            : $doc->createElement($tagName, $value)
268 25
        ;
269 25
        $parent->appendChild($Node);
0 ignored issues
show
Bug introduced by
It seems like $Node can also be of type false; however, parameter $node of DOMNode::appendChild() does only seem to accept DOMNode, 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

269
        $parent->appendChild(/** @scrutinizer ignore-type */ $Node);
Loading history...
270
    }
271
272
    /**
273
     * Verifica si un valor debe omitirse al generar un nodo XML.
274
     *
275
     * @param mixed $value Valor a verificar.
276
     * @return bool `true` si el valor debe omitirse, `false` en caso contrario.
277
     */
278 31
    private function skipValue(mixed $value): bool
279
    {
280 31
        return in_array(
281 31
            $value,
282 31
            $this->rules['node_values']['skip_generation'],
283 31
            true
284 31
        );
285
    }
286
287
    /**
288
     * Verifica si un valor debe generar un nodo XML vacío.
289
     *
290
     * @param mixed $value Valor a verificar.
291
     * @return bool `true` si el valor debe generar un nodo vacío, `false` en
292
     * caso contrario.
293
     */
294
    // private function createWithEmptyValue(mixed $value): bool
295
    // {
296
    //     return in_array(
297
    //         $value,
298
    //         $this->rules['node_values']['generate_empty'],
299
    //         true
300
    //     );
301
    // }
302
}
303