Passed
Push — master ( 31c0b7...5e3680 )
by Esteban De La Fuente
04:40
created

Xml::toArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 7
ccs 0
cts 4
cp 0
crap 6
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\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
        // Si el XML que se cargará no inicia con el TAG que abre XML se agrega.
137
        // Esto es 100% necesario pues si viene en codificación diferente a
138
        // UTF-8 (lo más normal) y no viene este tag abriendo el XML al cargar
139
        // reclamará que falta la codificación.
140 46
        if (!str_starts_with($source, '<?xml')) {
141 18
            $source = '<?xml version="1.0" encoding="' . $encoding . '"?>'
142 18
                . "\n" . $source
143 18
            ;
144
        }
145
146
        // Obtener estado actual de libxml y cambiarlo antes de cargar el XML
147
        // para obtener los errores en una variable si falla algo.
148 46
        $useInternalErrors = libxml_use_internal_errors(true);
149
150
        // Cargar el XML.
151 46
        $status = parent::loadXml($source, $options);
152
153
        // Obtener errores, limpiarlos y restaurar estado de errores de libxml.
154 46
        $errors = libxml_get_errors();
155 46
        libxml_clear_errors();
156 46
        libxml_use_internal_errors($useInternalErrors);
157
158 46
        if (!$status) {
159 4
            throw new XmlException('Error al cargar el XML.', $errors);
160
        }
161
162
        // Retornar estado de la carga del XML.
163
        // Sólo retornará `true`, pues si falla lanza excepción.
164 42
        return true;
165
    }
166
167
    /**
168
     * {@inheritDoc}
169
     */
170 23
    public function saveXml(?DOMNode $node = null, int $options = 0): string
171
    {
172 23
        $xml = parent::saveXml($node, $options);
173
174 23
        return XmlUtil::fixEntities($xml);
175
    }
176
177
    /**
178
     * {@inheritDoc}
179
     */
180 3
    public function getXml(): string
181
    {
182 3
        $xml = $this->saveXml();
183 3
        $xml = preg_replace(
184 3
            '/<\?xml\s+version="1\.0"\s+encoding="[^"]+"\s*\?>/i',
185 3
            '',
186 3
            $xml
187 3
        );
188
189 3
        return trim($xml);
190
    }
191
192
    /**
193
     * {@inheritDoc}
194
     */
195 19
    public function C14NWithIsoEncoding(?string $xpath = null): string
196
    {
197
        // Si se proporciona XPath, filtrar los nodos.
198 19
        if ($xpath) {
199 8
            $node = $this->getNodes($xpath)->item(0);
200 8
            if (!$node) {
201 2
                throw new XmlException(sprintf(
202 2
                    'No fue posible obtener el nodo con el XPath %s.',
203 2
                    $xpath
204 2
                ));
205
            }
206 6
            $xml = $node->C14N();
207
        }
208
        // Usar C14N() para todo el documento si no se especifica XPath.
209
        else {
210 13
            $xml = $this->C14N();
211
        }
212
213
        // Corregir XML entities.
214 17
        $xml = XmlUtil::fixEntities($xml);
215
216
        // Convertir el XML aplanado de UTF-8 a ISO-8859-1.
217
        // Requerido porque C14N() siempre entrega los datos en UTF-8.
218 17
        $xml = Str::utf8decode($xml);
219
220
        // Entregar el XML canonicalizado.
221 17
        return $xml;
222
    }
223
224
    /**
225
     * {@inheritDoc}
226
     */
227 1
    public function C14NWithIsoEncodingFlattened(?string $xpath = null): string
228
    {
229
        // Obtener XML canonicalizado y codificado en ISO8859-1.
230 1
        $xml = $this->C14NWithIsoEncoding($xpath);
231
232
        // Eliminar los espacios entre tags.
233 1
        $xml = preg_replace("/>\s+</", '><', $xml);
234
235
        // Entregar el XML canonicalizado y aplanado.
236 1
        return $xml;
237
    }
238
239
    /**
240
     * {@inheritDoc}
241
     */
242
    public function getSignatureNodeXml(): ?string
243
    {
244
        $tag = $this->documentElement->tagName;
245
        $xpath = '/' . $tag . '/Signature';
246
        $signatureElement = $this->getNodes($xpath)->item(0);
247
248
        return $signatureElement?->C14N();
249
    }
250
251
    /**
252
     * {@inheritDoc}
253
     */
254
    public function query(string $query, array $params = []): string|array|null
255
    {
256
        if (!isset($this->xPathQuery)) {
257
            $this->xPathQuery = new XPathQuery($this);
258
        }
259
260
        return $this->xPathQuery->get($query, $params);
261
    }
262
263
    /**
264
     * {@inheritDoc}
265
     */
266 8
    public function getNodes(string $query, array $params = []): DOMNodeList
267
    {
268 8
        if (!isset($this->xPathQuery)) {
269 8
            $this->xPathQuery = new XPathQuery($this);
270
        }
271
272 8
        return $this->xPathQuery->getNodes($query, $params);
273
    }
274
275
    /**
276
     * {@inheritDoc}
277
     */
278
    public function get(string $selector): mixed
279
    {
280
        return Selector::get($this->toArray(), $selector);
281
    }
282
283
    /**
284
     * {@inheritDoc}
285
     */
286
    public function toArray(): array
287
    {
288
        if (!isset($this->array)) {
289
            $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...
290
        }
291
292
        return $this->array;
293
    }
294
295
    /**
296
     * {@inheritDoc}
297
     */
298 21
    public function getDocumentElement(): ?DOMElement
299
    {
300 21
        return $this->documentElement;
301
    }
302
}
303