Skip to content

Commit

Permalink
Merge pull request #95 from eclipxe13/node-value
Browse files Browse the repository at this point in the history
Node can have simple text content (Version 2.21.0)
  • Loading branch information
eclipxe13 authored May 5, 2022
2 parents be90639 + 4ed8f0c commit 7c501be
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 16 deletions.
6 changes: 6 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@
- Remove deprecated constant `CfdiUtils\Cfdi::CFDI_NAMESPACE`.
- Remove `CfdiUtils\Validate\Cfdi33\Xml\XmlFollowSchema`.
- Remove classes `CfdiUtils\Elements\Cfdi33\Helpers\SumasConceptosWriter` and `CfdiUtils\Elements\Cfdi40\Helpers\SumasConceptosWriter`.
- Merge methods from `\CfdiUtils\Nodes\NodeHasValueInterface` into `\CfdiUtils\Nodes\NodeInterface`.

## Version 2.21.0 2022-04-29

- Introduce `\CfdiUtils\Nodes\NodeHasValueInterface` to work with nodes simple text content.
- The class `\CfdiUtils\Nodes\Node` implements `\CfdiUtils\Nodes\NodeHasValueInterface`.
- The XML node importers and exporters now can read and write simple text content.

## Version 2.20.2 2022-04-05

Expand Down
37 changes: 27 additions & 10 deletions docs/componentes/nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ donde cada uno tiene una colección de atributos. Los nodos no tienen referencia
## Objeto `CfdiUtils\Nodes\Node`

Esta es la estructura básica. Un nodo debe tener un nombre y esta propiedad no se puede cambiar.
Su contructor admite tres parámetros:
Su constructor admite tres parámetros:

- `string $name`: Nombre del nodo, se eliminan espacios en blanco al inicio y al final, no permite vacíos.
- `string[] $attributes`: arreglo de elementos clave/valor que serán importados como atributos
Expand All @@ -16,7 +16,7 @@ Su contructor admite tres parámetros:

### Atributos de nodos `attributes(): CfdiUtils\Nodes\Attributes`

Se accesa a sus atributos utilizando la forma de arreglos de php siguiendo estas reglas básicas:
Se accede a sus atributos utilizando la forma de arreglos de php siguiendo estas reglas básicas:

- La lectura de un nodo siempre devuelve una cadena de caracteres aunque el atributo no exista.
- La escritura de un nodo es siempre con una cadena de caracteres, también puede ser un objeto
Expand Down Expand Up @@ -56,7 +56,7 @@ Cuanto se itera el objeto en realidad se está iterando sobre la colección de n

La clase `Node` tiene estos métodos de ayuda que sirven para trabajar directamente sobre la colección Nodes:

- iterador: el `foreach` se realiza sobre la colección de nodos.
- iterador: el ciclo `foreach` se realiza sobre la colección de nodos.
- `addChild(Node $node)`: agrega un nodo en la colección de nodos.


Expand Down Expand Up @@ -101,7 +101,7 @@ Se pueden hacer las operaciones básicas como:
`exists(Node $node)`,
`get(int $index)`.

Adicionalmente se pueden usar los métodos:
Adicionalmente, se pueden usar los métodos:
`firstNodeWithName(string name): Node|null`,
`getNodesByName(string $nodeName): Nodes` y
`importFromArray(Nodes[] $nodes)`
Expand All @@ -110,9 +110,9 @@ Adicionalmente se pueden usar los métodos:
## Clase CfdiUtils\Nodes\Attributes

Esta clase representa una colección de atributos identificados por nombre.
Al iterar en el objeto se devolverá cada uno de los attributos en forma de clave/valor.
Al iterar en el objeto se devolverá cada uno de los atributos en forma de clave/valor.

Adicionalmente esta clase permite el uso de acceso como arreglo, por lo que permite:
Adicionalmente, esta clase permite el uso de acceso como arreglo, por lo que permite:

- `$attributes[$name]` como equivalente de `$attributes->get($name)`
- `$attributes[$name] = $value` como equivalente de `$attributes->set($name, $value)`
Expand All @@ -133,10 +133,27 @@ Se pueden hacer las operaciones básicas como:
Esta es una clase de utilerías que contiene métodos estáticos que permiten crear estructuras de nodos desde XML
y generar XML a partir de los nodos. Recuerde que los nodos solo pueden almacenar atributos y nodos hijos.

Actualmente permite exportar e importar a/desde: `DOMDocument`, `DOMElement`, `SimpleXmlElement` y `string` (con contenido válido).
Actualmente, permite exportar e importar a/desde: `DOMDocument`, `DOMElement`, `SimpleXmlElement` y `string` (con contenido válido).

**Advertencias:**

- Los nodos no tienen campo de contenido y no son una reescritura fiel de DOM.
- Los nodos solo contienen atributos e hijos.
- Importar XML que no siga la estructura de atributos/hijos exclusivamente puede resultar en pérdida de datos.
- Los nodos no son una reescritura fiel de DOM.
- Los nodos solo contienen atributos, hijos y contenido textual simple.
- Importar XML que no siga la estructura de atributos, hijos y contenido textual simple exclusivamente puede resultar en pérdida de datos.

## Contenido de texto

Tradicionalmente, los CFDI Regulares, CFDI de Retenciones e Información de Pagos, así como sus complementos,
siguen la estructura de elementos con valores en los atributos y sin texto.

Sin embargo, el SAT —en su infinita consistencia— tiene el *Complemento de facturas del sector de ventas al detalle*
disponible en <https://www.sat.gob.mx/consulta/76197/complemento-para-factura-electronica> donde, en lugar de poner
los valores en atributos, pone los valores en el contenido textual del elemento, además de otros cambios como usar
nombres de nodos en inglés.

Por lo anterior, se introdujo la interfaz `NodeHasValueInterface` que contiene los métodos `value(): string` y
`setValue(string $string): void` con lo que se puede escribir y leer este contenido simple.

Los objetos de tipo `Node` ya implementan esta interfaz, sin embargo, por compatibilidad con la versión `2.x`,
los métodos que retornan y obtienen parámetros de tipo `NodeInterface` no se han alterado.
Esto cambiará en la versión `3.x` donde `NodeHasValueInterface` y `NodeInterface` se fusionarán en una sola interfaz.
18 changes: 16 additions & 2 deletions src/CfdiUtils/Nodes/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use CfdiUtils\Utils\Xml;
use Traversable;

class Node implements NodeInterface
class Node implements NodeInterface, NodeHasValueInterface
{
/** @var string */
private $name;
Expand All @@ -16,20 +16,24 @@ class Node implements NodeInterface
/** @var Nodes|NodeInterface[] */
private $children;

/** @var string */
private $value;

/**
* Node constructor.
* @param string $name
* @param array $attributes
* @param NodeInterface[] $children
*/
public function __construct(string $name, array $attributes = [], array $children = [])
public function __construct(string $name, array $attributes = [], array $children = [], string $value = '')
{
if (! Xml::isValidXmlName($name)) {
throw new \UnexpectedValueException(sprintf('Cannot create a node with an invalid xml name: "%s"', $name));
}
$this->name = $name;
$this->attributes = new Attributes($attributes);
$this->children = new Nodes($children);
$this->value = $value;
}

public function name(): string
Expand Down Expand Up @@ -70,6 +74,16 @@ public function addAttributes(array $attributes)
$this->attributes->importArray($attributes);
}

public function value(): string
{
return $this->value;
}

public function setValue(string $value): void
{
$this->value = $value;
}

/*
* Search methods
*/
Expand Down
10 changes: 10 additions & 0 deletions src/CfdiUtils/Nodes/NodeHasValueInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace CfdiUtils\Nodes;

interface NodeHasValueInterface
{
public function value(): string;

public function setValue(string $value): void;
}
4 changes: 4 additions & 0 deletions src/CfdiUtils/Nodes/XmlNodeExporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ private function exportRecursive(DOMDocument $document, NodeInterface $node): DO
$element->appendChild($childElement);
}

if ($node instanceof NodeHasValueInterface && '' !== $node->value()) {
$element->appendChild($document->createTextNode($node->value()));
}

return $element;
}
}
24 changes: 21 additions & 3 deletions src/CfdiUtils/Nodes/XmlNodeImporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@

namespace CfdiUtils\Nodes;

use \DOMElement;
use DOMElement;
use DOMNode;
use DOMText;

class XmlNodeImporter
{
/**
* Local record for registered namespaces to avoid set the namespace declaration in every children
* Local record for registered namespaces to avoid set the namespace declaration in every child
* @var string[]
*/
private $registeredNamespaces = [];

public function import(DOMElement $element): NodeInterface
{
$node = new Node($element->tagName);

$node->setValue($this->extractValue($element));

if ('' !== $element->prefix) {
$this->registerNamespace($node, 'xmlns:' . $element->prefix, $element->namespaceURI);
$this->registerNamespace($node, 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
}

/** @var \DOMNode $attribute */
/** @var DOMNode $attribute */
foreach ($element->attributes as $attribute) {
$node[$attribute->nodeName] = $attribute->nodeValue;
}
Expand Down Expand Up @@ -49,4 +54,17 @@ private function registerNamespace(Node $node, string $prefix, string $uri)
$this->registeredNamespaces[$prefix] = $uri;
$node[$prefix] = $uri;
}

private function extractValue(DOMElement $element): string
{
$values = [];
foreach ($element->childNodes as $childElement) {
if (! $childElement instanceof DOMText) {
continue;
}
$values[] = $childElement->wholeText;
}

return implode('', $values);
}
}
16 changes: 15 additions & 1 deletion tests/CfdiUtilsTests/Nodes/NodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ public function testConstructWithoutArguments()
$this->assertSame('name', $node->name());
$this->assertCount(0, $node->attributes());
$this->assertCount(0, $node->children());
$this->assertSame('', $node->value());
}

public function testConstructWithArguments()
{
$dummyNode = new Node('dummy');
$attributes = ['foo' => 'bar'];
$children = [$dummyNode];
$node = new Node('name', $attributes, $children);
$value = 'xee';
$node = new Node('name', $attributes, $children, $value);
$this->assertSame('bar', $node->attributes()->get('foo'));
$this->assertSame($dummyNode, $node->children()->firstNodeWithName('dummy'));
$this->assertSame($value, $node->value());
}

public function testConstructWithEmptyName()
Expand Down Expand Up @@ -116,4 +119,15 @@ public function testArrayAccessToAttributes()
$this->assertFalse(isset($node['id']));
$this->assertSame('', $node['id']);
}

public function testValueProperty()
{
$node = new Node('x');

$node->setValue('first');
$this->assertSame('first', $node->value());

$node->setValue('second');
$this->assertSame('second', $node->value());
}
}
53 changes: 53 additions & 0 deletions tests/CfdiUtilsTests/Nodes/XmlNodeUtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace CfdiUtilsTests\Nodes;

use CfdiUtils\Nodes\Node;
use CfdiUtils\Nodes\NodeHasValueInterface;
use CfdiUtils\Nodes\NodeInterface;
use CfdiUtils\Nodes\XmlNodeUtils;
use CfdiUtils\Utils\Xml;
use CfdiUtilsTests\TestCase;
Expand All @@ -13,6 +15,7 @@ public function providerToNodeFromNode(): array
{
return [
'simple-xml' => [$this->utilAsset('nodes/sample.xml')],
'with-texts-xml' => [$this->utilAsset('nodes/sample-with-texts.xml')],
'cfdi' => [$this->utilAsset('cfdi33-valid.xml')],
];
}
Expand Down Expand Up @@ -81,4 +84,54 @@ public function testImportXmlWithNamespaceWithoutPrefix()
}
$this->assertSame('http://external.com/inner', $inspected['xmlns']);
}

public function testXmlWithValueWithSpecialChars()
{
$expectedValue = 'ampersand: &';
$content = '<root>ampersand: &amp;</root>';

/** @var NodeInterface&NodeHasValueInterface $node */
$node = XmlNodeUtils::nodeFromXmlString($content);

$this->assertSame($expectedValue, $node->value());
$this->assertSame($content, XmlNodeUtils::nodeToXmlString($node));
}

public function testXmlWithValueWithInnerComment()
{
$expectedValue = 'ampersand: &';
$content = '<root>ampersand: <!-- comment -->&amp;</root>';
$expectedContent = '<root>ampersand: &amp;</root>';

/** @var NodeInterface&NodeHasValueInterface $node */
$node = XmlNodeUtils::nodeFromXmlString($content);

$this->assertSame($expectedValue, $node->value());
$this->assertSame($expectedContent, XmlNodeUtils::nodeToXmlString($node));
}

public function testXmlWithValueWithInnerWhiteSpace()
{
$expectedValue = "\n\nfirst line\n\tsecond line\n\t third line \t\nfourth line\n\n";
$content = "<root>$expectedValue</root>";

/** @var NodeInterface&NodeHasValueInterface $node */
$node = XmlNodeUtils::nodeFromXmlString($content);

$this->assertSame($expectedValue, $node->value());
$this->assertSame($content, XmlNodeUtils::nodeToXmlString($node));
}

public function testXmlWithValueWithInnerElement()
{
$expectedValue = 'ampersand: &';
$content = '<root>ampersand: <inner/>&amp;</root>';
$expectedContent = '<root><inner/>ampersand: &amp;</root>';

/** @var NodeInterface&NodeHasValueInterface $node */
$node = XmlNodeUtils::nodeFromXmlString($content);

$this->assertSame($expectedValue, $node->value());
$this->assertSame($expectedContent, XmlNodeUtils::nodeToXmlString($node));
}
}
12 changes: 12 additions & 0 deletions tests/assets/nodes/sample-with-texts.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<root id="root">
<level-one id="1">
<level-two id="1.1">value 1.1</level-two>
<level-two id="1.2">value 1.2</level-two>
<level-two id="1.3">value 1.3</level-two>
</level-one>
<level-one id="2">
<level-two id="2.1">value 2.1</level-two>
<level-two id="2.2">value 2.2</level-two>
</level-one>
<level-one id="empty"/>
</root>

0 comments on commit 7c501be

Please sign in to comment.