Buen momento del día!
El alcance del formato XML es bastante extenso. Junto con CSV, JSON y otros, XML es una de las formas más comunes de presentar datos para el intercambio entre diferentes servicios, programas y sitios. Como ejemplo, podemos citar el formato CommerceML para intercambiar bienes y pedidos entre 1C "Trade Management" y una tienda en línea.
Por lo tanto, casi todos los que crean servicios web de vez en cuando tienen que lidiar con la necesidad de analizar documentos XML. En mi publicación, propongo uno de los métodos sobre cómo hacer esto de la manera más clara y transparente posible usando XMLReader.
PHP ofrece varias formas de trabajar con el formato XML. Sin entrar en detalles, diré que en principio se pueden dividir en dos grupos:
- Cargar todo el documento XML en la memoria como un objeto y trabajar con este objeto
- Lectura paso a paso de una cadena XML a nivel de etiquetas, atributos y contenido de texto.
La primera forma es más intuitiva, el código se ve más transparente. Este método funciona bien para archivos pequeños.
El segundo método es un enfoque de nivel inferior, que nos brinda una serie de ventajas y, al mismo tiempo, eclipsa un poco la vida. Detengámonos en ello con más detalle. Pros:
- Velocidad de análisis. Puedes leer con más detalle aquí .
- Consumo de menos RAM. No almacenamos todos los datos en forma de un objeto que es muy costoso en la memoria.
Pero: sacrificamos la legibilidad del código. Si el objetivo de nuestro análisis es, por ejemplo, calcular la suma de los valores en ciertos lugares dentro de XML con una estructura simple, entonces no hay problemas.
Sin embargo, si la estructura del archivo es compleja, incluso trabajar con datos depende de la ruta completa a estos datos, y el resultado debe incluir muchos parámetros, entonces aquí llegamos a un código bastante caótico.
Entonces escribí una clase que posteriormente me hizo la vida más fácil. Su uso simplifica la escritura de reglas y mejora enormemente la legibilidad de los programas, su tamaño se vuelve muchas veces más pequeño y el código es más hermoso.
La idea principal es esta: almacenaremos tanto el esquema de nuestro XML como cómo trabajar con él en una única matriz que repita la jerarquía de solo las etiquetas que necesitamos. Además, para cualquiera de las etiquetas en la misma matriz, podremos registrar las funciones que necesitamos que son controladores para abrir una etiqueta, cerrarla, leer atributos o leer texto, o todos juntos. Por lo tanto, almacenamos nuestra estructura XML y controladores en un solo lugar. Una sola mirada a nuestra estructura de procesamiento será suficiente para comprender lo que estamos haciendo con nuestro archivo XML. Haré una reserva de que en tareas simples (como en los ejemplos a continuación) la ventaja en la legibilidad es pequeña, pero será obvio cuando trabaje con archivos de una estructura relativamente compleja, por ejemplo, un formato de intercambio con 1C.
Ahora los detalles. Aquí está nuestra clase:
Versión de depuración (con el parámetro $ debug):
Clase XMLReaderStruct: haga clic para expandirclass XMLReaderStruct extends XMLReader { public function xmlStruct($xml, $structure, $encoding = null, $options = 0, $debug = false) { $this->xml($xml, $encoding, $options); $stack = array(); $node = &$structure; $skipToDepth = false; while ($this->read()) { switch ($this->nodeType) { case self::ELEMENT: if ($skipToDepth === false) {
Versión de lanzamiento (sin el parámetro $ debug y los comentarios):
Clase XMLReaderStruct: haga clic para expandir class XMLReaderStruct extends XMLReader { public function xmlStruct($xml, $structure, $encoding = null, $options = 0) { $this->xml($xml, $encoding, $options); $stack = array(); $node = &$structure; $skipToDepth = false; while ($this->read()) { switch ($this->nodeType) { case self::ELEMENT: if ($skipToDepth === false) { if (isset($node[$this->name])) { $stack[$this->depth] = &$node; $node = &$node[$this->name]; if (isset($node["__open"]) && (false === $node["__open"]())) return false; if (isset($node["__attrs"])) { $attrs = array(); if ($this->hasAttributes) while ($this->moveToNextAttribute()) $attrs[$this->name] = $this->value; if (false === $node["__attrs"]($attrs)) return false; } if ($this->isEmptyElement) { if (isset($node["__close"]) && (false === $node["__close"]())) return false; $node = &$stack[$this->depth]; } } else { $skipToDepth = $this->depth; } } break; case self::TEXT: if ($skipToDepth === false) { if (isset($node["__text"]) && (false === $node["__text"]($this->value))) return false; } break; case self::END_ELEMENT: if ($skipToDepth === false) { if (isset($node["__close"]) && (false === $node["__close"]())) return false; $node = &$stack[$this->depth]; } elseif ($this->depth === $skipToDepth) { $skipToDepth = false; } break; } } return true; } }
Como puede ver, nuestra clase extiende las capacidades de la clase XMLReader estándar, a la cual agregamos un método:
xmlStruct($xml, $structure, $encoding = null, $options = 0, $debug = false)
Parámetros:
- $ xml, $ encoding, $ options : como en XMLReader :: xml ()
- $ estructura : una matriz asociativa que describe completamente cómo debemos trabajar con nuestro archivo. Se entiende que su apariencia se conoce de antemano, y sabemos exactamente qué etiquetas y qué debemos hacer.
- $ debug : (solo para la versión Debug) si se debe generar información de depuración (de forma predeterminada, desactivada).
El argumento
$ estructura .
Esta es una matriz asociativa, cuya estructura repite la jerarquía de etiquetas en el archivo XML, además, si es necesario, cada uno de los elementos de la estructura puede tener funciones de controlador (definidas como campos con la clave correspondiente):
- "__open" - función al abrir una etiqueta - function ()
- "__attrs" - función para procesar atributos de etiqueta (si existe) - función ($ assocArray)
- "__text" - función en presencia del valor de texto de la etiqueta - función ($ text)
- "__close" - función al cerrar una etiqueta - función ()
Si alguno de los controladores devuelve falso, el análisis se cancelará y la función xmlStruct () devolverá falso. Los siguientes ejemplos muestran cómo construir el argumento $ structure:
Ejemplo 1 que muestra el orden en que se llaman los manejadoresSupongamos que hay un archivo XML:
<?xml version="1.0" encoding="UTF-8"?> <root> <a attr_1="123" attr_2="456">Abc</a> <b> <x>This is node x inside b</x> </b> <c></c> <d> <x>This is node x inside d</x> </d> <e></e> </root>
$structure = array( 'root' => array( 'a' => array( "__attrs" => function($array) { echo "ATTR ARRAY IS ",json_encode($array),"\r\n"; }, "__text" => function($text) use (&$a) { echo "TEXT a {$text}\r\n"; } ), 'b' => array( "__open" => function() { echo "OPEN b\r\n"; }, "__close" => function() { echo "CLOSE b\r\n"; }, 'x' => array( "__open" => function() { echo "OPEN x\r\n"; }, "__text" => function($text) { echo "TEXT x {$text}\r\n"; }, "__close" => function() { echo "CLOSE x\r\n"; } ) ) ) ); $xmlReaderStruct->xmlStruct($xml, $structure);
Los manejadores serán llamados (en orden cronológico):
atributos raíz-> a
campo de texto raíz-> a
raíz de apertura-> b
raíz de apertura-> b-> x
raíz de texto-> b-> x
raíz de cierre-> b-> x
raíz de cierre-> b
Los campos restantes no serán procesados (incluyendo root-> d-> x serán ignorados, porque está fuera de la estructura)
Ejemplo 2 que ilustra una tarea práctica simpleSupongamos que hay un archivo XML:
<?xml version="1.0" encoding="UTF-8"?> <shop> <record> <id>0</id> <type>product</type> <name>Some product name. ID:0</name> <qty>0</qty> <price>0</price> </record> <record> <id>1</id> <type>service</type> <name>Some product name. ID:1</name> <qty>1</qty> <price>15</price> </record> <record> <id>2</id> <type>product</type> <name>Some product name. ID:2</name> <qty>2</qty> <price>30</price> </record> <record> <id>3</id> <type>service</type> <name>Some product name. ID:3</name> <qty>3</qty> <price>45</price> </record> <record> <id>4</id> <type>product</type> <name>Some product name. ID:4</name> <qty>4</qty> <price>60</price> </record> <record> <id>5</id> <type>service</type> <name>Some product name. ID:5</name> <qty>5</qty> <price>75</price> </record> </shop>
Este es un cheque de caja con bienes y servicios.
Cada registro de verificación contiene el identificador de registro, tipo (producto "producto" o servicio "servicio"), nombre, cantidad y precio.
Tarea: calcule el monto del cheque, pero por separado para bienes y servicios.
include_once "xmlreaderstruct.class.php"; $x = new XMLReaderStruct(); $productsSum = 0; $servicesSum = 0; $structure = array( 'shop' => array( 'record' => array( 'type' => array( "__text" => function($text) use (&$currentRecord) { $currentRecord['isService'] = $text === 'service'; } ), 'qty' => array( "__text" => function($text) use (&$currentRecord) { $currentRecord['qty'] = (int)$text; } ), 'price' => array( "__text" => function($text) use (&$currentRecord) { $currentRecord['price'] = (int)$text; } ), '__open' => function() use (&$currentRecord) { $currentRecord = array(); }, '__close' => function() use (&$currentRecord, &$productsSum, &$servicesSum) { $money = $currentRecord['qty'] * $currentRecord['price']; if ($currentRecord['isService']) $servicesSum += $money; else $productsSum += $money; } ) ) ); $x->xmlStruct(file_get_contents('example.xml'), $structure); echo 'Overal products price: ', $productsSum, ', Overal services price: ', $servicesSum;