Passed
Push — master ( bfc944...db8c15 )
by Esteban De La Fuente
05:46
created

Xml::getNodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
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\Package\Prime\Component\Xml\Entity;
26
27
use Derafu\Lib\Core\Helper\Selector;
28
use Derafu\Lib\Core\Helper\Str;
29
use Derafu\Lib\Core\Helper\Xml as XmlUtil;
30
use Derafu\Lib\Core\Package\Prime\Component\Xml\Contract\XmlInterface;
31
use Derafu\Lib\Core\Package\Prime\Component\Xml\Exception\XmlException;
32
use Derafu\Lib\Core\Support\Xml\XPathQuery;
33
use DOMDocument;
34
use DOMElement;
35
use DOMNode;
36
use DOMNodeList;
37
38
/**
39
 * Clase que representa un documento XML.
40
 */
41
class Xml extends DOMDocument implements XmlInterface
42
{
43
    /**
44
     * Instancia para facilitar el manejo de XML usando XPath.
45
     *
46
     * @var XPathQuery
47
     */
48
    private XPathQuery $xPathQuery;
49
50
    /**
51
     * Representación del XML como arreglo.
52
     *
53
     * @var array
54
     */
55
    private array $array;
56
57
    /**
58
     * Constructor del documento XML.
59
     *
60
     * @param string $version Versión del documento XML.
61
     * @param string $encoding Codificación del documento XML.
62
     */
63 78
    public function __construct(
64
        string $version = '1.0',
65
        string $encoding = 'ISO-8859-1'
66
    ) {
67 78
        parent::__construct($version, $encoding);
68
69 78
        $this->formatOutput = true;
70 78
        $this->preserveWhiteSpace = true;
71
    }
72
73
    /**
74
     * {@inheritDoc}
75
     */
76 1
    public function getName(): string
77
    {
78 1
        return $this->documentElement->tagName;
79
    }
80
81
    /**
82
     * {@inheritDoc}
83
     */
84 3
    public function getNamespace(): ?string
85
    {
86 3
        $namespace = $this->documentElement->getAttribute('xmlns');
87
88 3
        return $namespace !== '' ? $namespace : null;
89
    }
90
91
    /**
92
     * {@inheritDoc}
93
     */
94 2
    public function getSchema(): ?string
95
    {
96 2
        $schemaLocation = $this->documentElement->getAttribute(
97 2
            'xsi:schemaLocation'
98 2
        );
99
100 2
        if (!$schemaLocation || !str_contains($schemaLocation, ' ')) {
101 1
            return null;
102
        }
103
104 1
        return explode(' ', $schemaLocation)[1];
105
    }
106
107
    /**
108
     * {@inheritDoc}
109
     */
110 46
    public function loadXml(string $source, int $options = 0): bool
111
    {
112
        // Si no hay un string XML en el origen entonces se lanza excepción.
113 46
        if (empty($source)) {
114
            throw new XmlException(
115
                'El contenido del XML que se desea cargar está vacío.'
116
            );
117
        }
118
119
        // Convertir el XML si es necesario.
120 46
        preg_match(
121 46
            '/<\?xml\s+version="([^"]+)"\s+encoding="([^"]+)"\?>/',
122 46
            $source,
123 46
            $matches
124 46
        );
125
        //$version = $matches[1] ?? $this->xmlVersion;
126 46
        $encoding = strtoupper($matches[2] ?? $this->encoding);
127 46
        if ($encoding === 'UTF-8') {
128 4
            $source = Str::utf8decode($source);
129 4
            $source = str_replace(
130 4
                ' encoding="UTF-8"?>',
131 4
                ' encoding="ISO-8859-1"?>',
132 4
                $source
133 4
            );
134
        }
135
136
        // Obtener estado actual de libxml y cambiarlo antes de cargar el XML
137
        // para obtener los errores en una variable si falla algo.
138 46
        $useInternalErrors = libxml_use_internal_errors(true);
139
140
        // Cargar el XML.
141 46
        $status = parent::loadXml($source, $options);
142
143
        // Obtener errores, limpiarlos y restaurar estado de errores de libxml.
144 46
        $errors = libxml_get_errors();
145 46
        libxml_clear_errors();
146 46
        libxml_use_internal_errors($useInternalErrors);
147
148 46
        if (!$status) {
149 4
            throw new XmlException('Error al cargar el XML.', $errors);
150
        }
151
152
        // Retornar estado de la carga del XML.
153
        // Sólo retornará `true`, pues si falla lanza excepción.
154 42
        return true;
155
    }
156
157
    /**
158
     * {@inheritDoc}
159
     */
160 23
    public function saveXml(?DOMNode $node = null, int $options = 0): string
161
    {
162 23
        $xml = parent::saveXml($node, $options);
163
164 23
        return XmlUtil::fixEntities($xml);
165
    }
166
167
    /**
168
     * {@inheritDoc}
169
     */
170 3
    public function getXml(): string
171
    {
172 3
        $xml = $this->saveXml();
173 3
        $xml = preg_replace(
174 3
            '/<\?xml\s+version="1\.0"\s+encoding="[^"]+"\s*\?>/i',
175 3
            '',
176 3
            $xml
177 3
        );
178
179 3
        return trim($xml);
180
    }
181
182
    /**
183
     * {@inheritDoc}
184
     */
185 19
    public function C14NWithIsoEncoding(?string $xpath = null): string
186
    {
187
        // Si se proporciona XPath, filtrar los nodos.
188 19
        if ($xpath) {
189 8
            $node = $this->getNodes($xpath)->item(0);
190 8
            if (!$node) {
191 2
                throw new XmlException(sprintf(
192 2
                    'No fue posible obtener el nodo con el XPath %s.',
193 2
                    $xpath
194 2
                ));
195
            }
196 6
            $xml = $node->C14N();
197
        }
198
        // Usar C14N() para todo el documento si no se especifica XPath.
199
        else {
200 13
            $xml = $this->C14N();
201
        }
202
203
        // Corregir XML entities.
204 17
        $xml = XmlUtil::fixEntities($xml);
205
206
        // Convertir el XML aplanado de UTF-8 a ISO-8859-1.
207
        // Requerido porque C14N() siempre entrega los datos en UTF-8.
208 17
        $xml = Str::utf8decode($xml);
209
210
        // Entregar el XML canonicalizado.
211 17
        return $xml;
212
    }
213
214
    /**
215
     * {@inheritDoc}
216
     */
217 1
    public function C14NWithIsoEncodingFlattened(?string $xpath = null): string
218
    {
219
        // Obtener XML canonicalizado y codificado en ISO8859-1.
220 1
        $xml = $this->C14NWithIsoEncoding($xpath);
221
222
        // Eliminar los espacios entre tags.
223 1
        $xml = preg_replace("/>\s+</", '><', $xml);
224
225
        // Entregar el XML canonicalizado y aplanado.
226 1
        return $xml;
227
    }
228
229
    /**
230
     * {@inheritDoc}
231
     */
232
    public function getSignatureNodeXml(): ?string
233
    {
234
        $tag = $this->documentElement->tagName;
235
        $xpath = '/' . $tag . '/Signature';
236
        $signatureElement = $this->getNodes($xpath)->item(0);
237
238
        return $signatureElement?->C14N();
239
    }
240
241
    /**
242
     * {@inheritDoc}
243
     */
244
    public function query(string $query, array $params = []): string|array|null
245
    {
246
        if (!isset($this->xPathQuery)) {
247
            $this->xPathQuery = new XPathQuery($this);
248
        }
249
250
        return $this->xPathQuery->get($query, $params);
251
    }
252
253
    /**
254
     * {@inheritDoc}
255
     */
256 8
    public function getNodes(string $query, array $params = []): DOMNodeList
257
    {
258 8
        if (!isset($this->xPathQuery)) {
259 8
            $this->xPathQuery = new XPathQuery($this);
260
        }
261
262 8
        return $this->xPathQuery->getNodes($query, $params);
263
    }
264
265
    /**
266
     * {@inheritDoc}
267
     */
268
    public function get(string $selector): mixed
269
    {
270
        return Selector::get($this->toArray(), $selector);
271
    }
272
273
    /**
274
     * {@inheritDoc}
275
     */
276
    public function toArray(): array
277
    {
278
        if (!isset($this->array)) {
279
            $this->array = $this->query('/');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->query('/') can also be of type string. However, the property $array is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
280
        }
281
282
        return $this->array;
283
    }
284
285
    /**
286
     * {@inheritDoc}
287
     */
288 21
    public function getDocumentElement(): ?DOMElement
289
    {
290 21
        return $this->documentElement;
291
    }
292
}
293