Test Failed
Push — master ( 4516e7...6ee9d0 )
by Esteban De La Fuente
04:23
created

Xml::loadXml()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 45
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4.9102

Importance

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