Skip to content

Commit 5e914e8

Browse files
committed
Se refactoriza para dejar una biblioteca estándar de XML, sin hacks por codificación.
1 parent 394a302 commit 5e914e8

12 files changed

Lines changed: 416 additions & 67 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Derafu: XML - Library for XML manipulation.
7+
*
8+
* Copyright (c) 2026 Esteban De La Fuente Rubio / Derafu <https://www.derafu.dev>
9+
* Licensed under the MIT License.
10+
* See LICENSE file for more details.
11+
*/
12+
13+
namespace Derafu\Xml\Exception;
14+
15+
/**
16+
* Exception thrown when a PHP array cannot be encoded into XML.
17+
*
18+
* Covers two cases:
19+
* - An attribute value is an array (attributes must be scalar).
20+
* - A child node contains a non-associative array where an associative
21+
* one is required.
22+
*/
23+
class XmlEncoderException extends XmlException
24+
{
25+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Derafu: XML - Library for XML manipulation.
7+
*
8+
* Copyright (c) 2026 Esteban De La Fuente Rubio / Derafu <https://www.derafu.dev>
9+
* Licensed under the MIT License.
10+
* See LICENSE file for more details.
11+
*/
12+
13+
namespace Derafu\Xml\Exception;
14+
15+
/**
16+
* Exception thrown when an XML string cannot be parsed.
17+
*
18+
* Covers two cases:
19+
* - The XML string is empty.
20+
* - The XML string is malformed (libxml parse error).
21+
*/
22+
class XmlParseException extends XmlException
23+
{
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Derafu: XML - Library for XML manipulation.
7+
*
8+
* Copyright (c) 2026 Esteban De La Fuente Rubio / Derafu <https://www.derafu.dev>
9+
* Licensed under the MIT License.
10+
* See LICENSE file for more details.
11+
*/
12+
13+
namespace Derafu\Xml\Exception;
14+
15+
/**
16+
* Exception thrown when an XPath query fails.
17+
*
18+
* Covers two cases:
19+
* - The XPath expression is syntactically invalid.
20+
* - The XPath expression points to a node that does not exist (when the
21+
* node is required).
22+
*/
23+
class XmlQueryException extends XmlException
24+
{
25+
}

src/Service/XmlEncoder.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
use Derafu\Xml\Contract\XmlDocumentInterface;
1616
use Derafu\Xml\Contract\XmlEncoderInterface;
17+
use Derafu\Xml\Exception\XmlEncoderException;
1718
use Derafu\Xml\XmlDocument;
1819
use Derafu\Xml\XmlHelper;
1920
use DOMElement;
2021
use DOMNode;
21-
use InvalidArgumentException;
2222

2323
/**
2424
* Class that creates an XML document from a PHP array.
@@ -132,14 +132,14 @@ public function encode(
132132
* @param DOMElement $node Node to which the attributes will be added.
133133
* @param array $attributes Array of attributes (key => value).
134134
* @return void
135-
* @throws InvalidArgumentException If an attribute value is an array.
135+
* @throws XmlEncoderException If an attribute value is an array.
136136
*/
137137
private function nodeAddAttributes(DOMElement $node, array $attributes): void
138138
{
139139
foreach ($attributes as $attribute => $value) {
140140
// If the attribute value is an array, it cannot be assigned.
141141
if (is_array($value)) {
142-
throw new InvalidArgumentException(sprintf(
142+
throw new XmlEncoderException(sprintf(
143143
'The type of data of the value entered for the attribute "%s" of the node "%s" is incorrect (cannot be an array). The value is: %s',
144144
$attribute,
145145
$node->tagName,
@@ -164,7 +164,7 @@ private function nodeAddAttributes(DOMElement $node, array $attributes): void
164164
* @param array $childs Array of data of the child nodes.
165165
* @param array|null $namespace XML namespace (URI and prefix).
166166
* @return void
167-
* @throws InvalidArgumentException If a child node is not an array.
167+
* @throws XmlEncoderException If a child node is not an array.
168168
*/
169169
private function nodeAddChilds(
170170
XmlDocumentInterface $doc,
@@ -189,7 +189,7 @@ private function nodeAddChilds(
189189

190190
// If the array is not associative (with new nodes) error.
191191
if (isset($child[0])) {
192-
throw new InvalidArgumentException(sprintf(
192+
throw new XmlEncoderException(sprintf(
193193
'The node "%s" allows including arrays, but they must be arrays with other nodes. The current value is incorrect: %s',
194194
$tagName,
195195
json_encode($child)

src/XPathQuery.php

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212

1313
namespace Derafu\Xml;
1414

15+
use Derafu\Xml\Contract\XmlDocumentInterface;
16+
use Derafu\Xml\Exception\XmlParseException;
17+
use Derafu\Xml\Exception\XmlQueryException;
1518
use DOMDocument;
1619
use DOMNode;
1720
use DOMNodeList;
1821
use DOMXPath;
19-
use InvalidArgumentException;
20-
use LogicException;
2122

2223
/**
2324
* Class to facilitate XML handling using XPath.
@@ -48,18 +49,18 @@ final class XPathQuery
4849
/**
4950
* Constructor that receives the XML document and prepares XPath.
5051
*
51-
* @param string|DOMDocument $xml XML document.
52+
* @param string|XmlDocumentInterface|DOMDocument $xml XML document.
5253
* @param array $namespaces Associative array with prefix and URI.
5354
*/
5455
public function __construct(
55-
string|DOMDocument $xml,
56+
string|XmlDocumentInterface|DOMDocument $xml,
5657
array $namespaces = []
5758
) {
5859
// Assign the DOM document instance.
59-
if ($xml instanceof DOMDocument) {
60+
if ($xml instanceof XmlDocumentInterface || $xml instanceof DOMDocument) {
6061
$this->dom = $xml;
6162
} else {
62-
$this->dom = new DOMDocument();
63+
$this->dom = new XmlDocument();
6364
$this->loadXml($xml);
6465
}
6566

@@ -194,8 +195,8 @@ public function getNodes(
194195
$contextNode,
195196
$this->registerNodeNS
196197
));
197-
} catch (LogicException $e) {
198-
throw new InvalidArgumentException(sprintf(
198+
} catch (XmlQueryException $e) {
199+
throw new XmlQueryException(sprintf(
199200
'An error occurred while executing the XPath expression: %s. %s',
200201
$query,
201202
$e->getMessage()
@@ -213,14 +214,9 @@ public function getNodes(
213214
*/
214215
private function loadXml(string $xml): static
215216
{
216-
try {
217-
$this->execute(fn () => $this->dom->loadXml($xml));
218-
} catch (LogicException $e) {
219-
throw new InvalidArgumentException(sprintf(
220-
'The provided XML is not valid: %s',
221-
$e->getMessage()
222-
));
223-
}
217+
// XmlDocument::loadXml() throws XmlParseException if the XML is
218+
// empty or malformed, propagating it directly to the caller.
219+
$this->dom->loadXml($xml);
224220

225221
return $this;
226222
}
@@ -346,7 +342,7 @@ private function execute(callable $function): mixed
346342
libxml_use_internal_errors($use_errors);
347343

348344
$message = $error ?: 'Ocurrió un error en XPathQuery.';
349-
throw new LogicException($message);
345+
throw new XmlQueryException($message);
350346
}
351347

352348
libxml_clear_errors();

src/XmlDocument.php

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
namespace Derafu\Xml;
1414

1515
use Derafu\Xml\Contract\XmlDocumentInterface;
16-
use Derafu\Xml\Exception\XmlException;
16+
use Derafu\Xml\Exception\XmlParseException;
17+
use Derafu\Xml\Exception\XmlQueryException;
1718
use DOMDocument;
1819
use DOMElement;
1920
use DOMNode;
@@ -95,39 +96,11 @@ public function loadXml(string $source, int $options = 0): bool
9596
{
9697
// If there is no XML string in the source, then an exception is thrown.
9798
if (empty($source)) {
98-
throw new XmlException(
99+
throw new XmlParseException(
99100
'The XML content that you want to load is empty.'
100101
);
101102
}
102103

103-
// Convert the XML if necessary.
104-
preg_match(
105-
'/<\?xml\s+version="([^"]+)"\s+encoding="([^"]+)"\?>/',
106-
$source,
107-
$matches
108-
);
109-
//$version = $matches[1] ?? $this->xmlVersion;
110-
$encoding = strtoupper($matches[2] ?? $this->encoding);
111-
if ($encoding === 'UTF-8' && $this->encoding === 'ISO-8859-1') {
112-
$source = mb_convert_encoding($source, 'ISO-8859-1', 'UTF-8');
113-
$source = str_replace(
114-
' encoding="UTF-8"?>',
115-
' encoding="ISO-8859-1"?>',
116-
$source
117-
);
118-
}
119-
120-
// If the XML that will be loaded does not start with the XML tag, it
121-
// is added. This is 100% necessary because if it comes in a different
122-
// encoding to UTF-8 (the most normal) and does not come with this tag
123-
// opening the XML, it will complain that the encoding is missing when
124-
// loading.
125-
if (!str_starts_with($source, '<?xml')) {
126-
$source = '<?xml version="1.0" encoding="' . $encoding . '"?>'
127-
. "\n" . $source
128-
;
129-
}
130-
131104
// Get the current state of libxml and change it before loading the XML
132105
// to get the errors in a variable if something fails.
133106
$useInternalErrors = libxml_use_internal_errors(true);
@@ -141,7 +114,7 @@ public function loadXml(string $source, int $options = 0): bool
141114
libxml_use_internal_errors($useInternalErrors);
142115

143116
if (!$status) {
144-
throw new XmlException('Error al cargar el XML.', $errors);
117+
throw new XmlParseException('Error al cargar el XML.', $errors);
145118
}
146119

147120
// Return the status of the XML loading.
@@ -183,7 +156,7 @@ public function C14NWithIso88591Encoding(?string $xpath = null): string
183156
if ($xpath) {
184157
$node = $this->getNodes($xpath)->item(0);
185158
if (!$node) {
186-
throw new XmlException(sprintf(
159+
throw new XmlQueryException(sprintf(
187160
'No fue posible obtener el nodo con el XPath %s.',
188161
$xpath
189162
));

src/XmlHelper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212

1313
namespace Derafu\Xml;
1414

15+
use Derafu\Xml\Exception\XmlQueryException;
1516
use DOMDocument;
1617
use DOMNodeList;
1718
use DOMXPath;
18-
use InvalidArgumentException;
1919

2020
/**
2121
* Class with utilities to work with XML strings.
@@ -44,7 +44,7 @@ public static function xpath(
4444
$result = @$xpath->query($expression);
4545

4646
if ($result === false) {
47-
throw new InvalidArgumentException(sprintf(
47+
throw new XmlQueryException(sprintf(
4848
'Invalid XPath expression: %s',
4949
$expression
5050
));

tests/fixtures/encode_and_decode.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,9 +354,8 @@
354354
// XML en UTF-8 con caracteres especiales (ej. tildes, ñ).
355355
'utf8_characters' => [
356356
'xmlContent' => '<?xml version="1.0" encoding="UTF-8"?><root><element>Árbol</element></root>',
357-
'expected' => '<?xml version="1.0" encoding="ISO-8859-1"?>' . "\n" . '<root>'
358-
. "\n" . ' <element>' . mb_convert_encoding('Árbol', 'ISO-8859-1', 'UTF-8')
359-
. '</element>' . "\n" . '</root>' . "\n",
357+
'expected' => '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . '<root>'
358+
. "\n" . ' <element>Árbol</element>' . "\n" . '</root>' . "\n",
360359
'expectedException' => null,
361360
],
362361
// XML en ISO-8859-1 con caracteres especiales (ej. tildes, ñ).
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Derafu: XML - Library for XML manipulation.
7+
*
8+
* Copyright (c) 2026 Esteban De La Fuente Rubio / Derafu <https://www.derafu.dev>
9+
* Licensed under the MIT License.
10+
* See LICENSE file for more details.
11+
*/
12+
13+
/**
14+
* Returns an XML string with a comprehensive set of ISO-8859-1 characters,
15+
* encoded as actual ISO-8859-1 bytes (not UTF-8, not XML entities).
16+
*
17+
* The content is defined here as UTF-8 string literals (the natural encoding
18+
* of PHP source files) and converted to ISO-8859-1 before being returned.
19+
*
20+
* Characters covered:
21+
* - Lowercase accented vowels : á é í ó ú à è ì ò ù
22+
* - Uppercase accented vowels : Á É Í Ó Ú À È Ì Ò Ù
23+
* - Spanish ñ/Ñ
24+
* - Umlaut vowels : ü Ü ö Ö
25+
* - Spanish punctuation : ¿ ¡
26+
* - Common symbols : © ® ° ½ ¼ ¾
27+
* - A representative sentence : Fabricación de Ñoños en Güemes
28+
*
29+
* @return string XML content encoded in ISO-8859-1.
30+
*/
31+
32+
$xml = '<?xml version="1.0" encoding="ISO-8859-1"?>' . "\n"
33+
. '<documento>' . "\n"
34+
. ' <vocales_min>á é í ó ú à è ì ò ù</vocales_min>' . "\n"
35+
. ' <vocales_may>Á É Í Ó Ú À È Ì Ò Ù</vocales_may>' . "\n"
36+
. ' <enie>ñ Ñ</enie>' . "\n"
37+
. ' <dieresis>ü Ü ö Ö</dieresis>' . "\n"
38+
. ' <puntuacion>¿Hola? ¡Mundo!</puntuacion>' . "\n"
39+
. ' <simbolos>© ® ° ½ ¼ ¾</simbolos>' . "\n"
40+
. ' <frase>Fabricación de Ñoños en Güemes</frase>' . "\n"
41+
. '</documento>' . "\n"
42+
;
43+
44+
return mb_convert_encoding($xml, 'ISO-8859-1', 'UTF-8');

0 commit comments

Comments
 (0)