Passed
Push — master ( ae22ab...660904 )
by Esteban De La Fuente
03:20
created

XPathQuery::processNode()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.0488

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 7
nop 1
dl 0
loc 16
ccs 7
cts 8
cp 0.875
crap 5.0488
rs 9.6111
c 0
b 0
f 0
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\Support\Xml;
26
27
use DOMDocument;
28
use DOMNode;
29
use DOMNodeList;
30
use DOMXPath;
31
use InvalidArgumentException;
32
33
/**
34
 * Clase para facilitar el manejo de XML usando XPath.
35
 */
36
class XPathQuery
37
{
38
    /**
39
     * Instancia del documento XML.
40
     *
41
     * @var DOMDocument
42
     */
43
    private readonly DOMDocument $dom;
44
45
    /**
46
     * Instancia que representa el buscador con XPath.
47
     *
48
     * @var DOMXPath
49
     */
50
    private readonly DOMXPath $xpath;
51
52
    /**
53
     * Constructor que recibe el documento XML y prepara XPath.
54
     *
55
     * @param string|DOMDocument $xml Documento XML.
56
     */
57 14
    public function __construct(string|DOMDocument $xml)
58
    {
59 14
        if ($xml instanceof DOMDocument) {
60
            $this->dom = $xml;
0 ignored issues
show
Bug introduced by
The property dom is declared read-only in Derafu\Lib\Core\Support\Xml\XPathQuery.
Loading history...
61
        } else {
62 14
            $this->dom = new DOMDocument();
63 14
            $this->loadXml($xml);
64
        }
65
66 13
        $this->xpath = new DOMXPath($this->dom);
0 ignored issues
show
Bug introduced by
The property xpath is declared read-only in Derafu\Lib\Core\Support\Xml\XPathQuery.
Loading history...
67
    }
68
69
    /**
70
     * Carga un string XML en el atributo $dom.
71
     *
72
     * @param string $xml
73
     * @return static
74
     */
75 14
    private function loadXml(string $xml): static
76
    {
77 14
        $use_errors = libxml_use_internal_errors(true);
78
79 14
        $this->dom->loadXml($xml);
80
81 14
        if ($error = libxml_get_last_error()) {
82 1
            throw new InvalidArgumentException(sprintf(
83 1
                'El XML proporcionado no es válido: %s.',
84 1
                $error->message
85 1
            ));
86
        }
87
88 13
        libxml_clear_errors();
89 13
        libxml_use_internal_errors($use_errors);
90
91 13
        return $this;
92
    }
93
94
    /**
95
     * Devuelve el DOMDocument usado internamente.
96
     *
97
     * @return DOMDocument
98
     */
99 1
    public function getDomDocument(): DOMDocument
100
    {
101 1
        return $this->dom;
102
    }
103
104
    /**
105
     * Ejecuta una consulta XPath y devuelve el resultado procesado.
106
     *
107
     * El resultado dependerá de o que se encuentre:
108
     *
109
     *   - `null`: si no hubo coincidencias.
110
     *   - string: si hubo una coincidencia.
111
     *   - string[]: si hubo más de una coincidencia.
112
     *
113
     * Si el nodo tiene hijos, devuelve un arreglo recursivo representando
114
     * toda la estructura de los nodos.
115
     *
116
     * @param string $query Consulta XPath.
117
     * @return string|string[]|null El valor procesado: string, arreglo o null.
118
     */
119 8
    public function get(string $query): string|array|null
120
    {
121 8
        $nodes = $this->getNodes($query);
122
123
        // Sin coincidencias.
124 7
        if ($nodes->length === 0) {
125 2
            return null;
126
        }
127
128
        // Un solo nodo.
129 5
        if ($nodes->length === 1) {
130 3
            return $this->processNode($nodes->item(0));
131
        }
132
133
        // Varios nodos.
134 2
        $results = [];
135 2
        foreach ($nodes as $node) {
136 2
            $results[] = $this->processNode($node);
137
        }
138
139 2
        return $results;
140
    }
141
142
    /**
143
     * Procesa un nodo DOM y sus hijos recursivamente.
144
     *
145
     * @param DOMNode $node Nodo DOM a procesar.
146
     * @return string|array Valor del nodo o estructura de hijos como arreglo.
147
     */
148 5
    private function processNode(DOMNode $node): string|array
149
    {
150 5
        if ($node->hasChildNodes()) {
151 5
            $children = [];
152 5
            foreach ($node->childNodes as $child) {
153 5
                if ($child->nodeType === XML_ELEMENT_NODE) {
154 1
                    $children[$child->nodeName] = $this->processNode($child);
155
                }
156
            }
157
158
            // Si tiene hijos procesados, devolver la estructura.
159 5
            return count($children) > 0 ? $children : $node->nodeValue;
160
        }
161
162
        // Si no tiene hijos, devolver el valor.
163
        return $node->nodeValue;
164
    }
165
166
167
    /*public function get(string $query): string|array|null
168
    {
169
        // Ejecutar consulta.
170
        $results = $this->getValues($query);
171
172
        // Si no hay resultados null.
173
        if (!isset($results[0])) {
174
            return null;
175
        }
176
177
        // Si solo hay un resultado un string.
178
        if (!isset($results[1])) {
179
            return $results[0];
180
        }
181
182
        // Más de un resultado, arreglo.
183
        return $results;
184
    }*/
185
186
    /**
187
     * Ejecuta una consulta XPath y devuelve un arreglo de valores.
188
     *
189
     * @param string $query Consulta XPath.
190
     * @return string[] Arreglo de valores encontrados.
191
     */
192 1
    public function getValues(string $query): array
193
    {
194 1
        $nodes = $this->getNodes($query);
195
196 1
        $results = [];
197 1
        foreach ($nodes as $node) {
198 1
            $results[] = $node->nodeValue;
199
        }
200
201 1
        return $results;
202
    }
203
204
    /**
205
     * Ejecuta una consulta XPath y devuelve el primer resultado como string.
206
     *
207
     * @param string $query Consulta XPath.
208
     * @return string|null El valor del nodo, o `null` si no existe.
209
     */
210 1
    public function getValue(string $query): ?string
211
    {
212 1
        $nodes = $this->getNodes($query);
213
214 1
        return $nodes->length > 0
215 1
            ? $nodes->item(0)->nodeValue
216 1
            : null
217 1
        ;
218
    }
219
220
    /**
221
     * Ejecuta una consulta XPath y devuelve los nodos resultantes.
222
     *
223
     * @param string $query Consulta XPath.
224
     * @return DOMNodeList Nodos resultantes de la consulta XPath.
225
     */
226 12
    public function getNodes(string $query): DOMNodeList
227
    {
228 12
        $use_errors = libxml_use_internal_errors(true);
229
230 12
        $nodes = $this->xpath->query($query);
231
232 12
        if ($nodes === false || $error = libxml_get_last_error()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $error is dead and can be removed.
Loading history...
233 2
            throw new InvalidArgumentException(sprintf(
234 2
                'Ocurrió un error al ejecutar la expresión XPath: %s.',
235 2
                $query
236 2
            ));
237
        }
238
239 10
        libxml_clear_errors();
240 10
        libxml_use_internal_errors($use_errors);
241
242 10
        return $nodes;
243
    }
244
}
245