. */ namespace SP\Storage\File; use DOMDocument; use DOMElement; use DOMNode; use DOMNodeList; use DOMXPath; use ReflectionObject; use RuntimeException; /** * Class XmlHandler para manejo básico de documentos XML * * @package SP\Storage\File; */ final class XmlHandler implements XmlFileStorageInterface { /** * @var mixed */ protected $items; /** * @var DOMDocument */ private $Dom; /** * @var DOMElement */ private $root; /** * @var FileHandler */ private $fileHandler; /** * XmlHandler constructor. * * @param FileHandler $fileHandler */ public function __construct(FileHandler $fileHandler) { $this->fileHandler = $fileHandler; } /** * Cargar un archivo XML * * @param string $node * * @return XmlFileStorageInterface * @throws FileException * @throws RuntimeException */ public function load(string $node = 'root'): XmlFileStorageInterface { $this->fileHandler->checkIsReadable(); $this->fileHandler->getFileSize(true); $this->items = []; $this->setDOM(); $this->Dom->load($this->fileHandler->getFile()); $nodes = $this->Dom->getElementsByTagName($node); if ($nodes->length === 0) { throw new RuntimeException(__u('XML node does not exist')); } $this->items = $this->readChildNodes($nodes->item(0)->childNodes); return $this; } /** * Crear un nuevo documento XML */ private function setDOM() { $this->Dom = new DOMDocument('1.0', 'utf-8'); } /** * Leer de forma recursiva los nodos hijos y devolver un array multidimensional * * @param DOMNodeList $nodeList * * @return array */ protected function readChildNodes(DOMNodeList $nodeList): array { $nodes = []; /** @var DOMElement $node */ foreach ($nodeList as $node) { if ($node->nodeType === XML_ELEMENT_NODE) { if (is_object($node->childNodes) && $node->childNodes->length > 1 ) { if ($node->hasAttribute('multiple') && (int)$node->getAttribute('multiple') === 1 ) { $nodes[] = $this->readChildNodes($node->childNodes); } elseif ($node->hasAttribute('class')) { $nodes[$node->nodeName] = $this->readChildNodes($node->childNodes); $nodes[$node->nodeName]['__class__'] = (string)$node->getAttribute('class');; } else { $nodes[$node->nodeName] = $this->readChildNodes($node->childNodes); } } else { $val = null; if (is_numeric($node->nodeValue) && strpos($node->nodeValue, '.') === false) { $val = (int)$node->nodeValue; } else if (!empty($node->nodeValue)) { $val = $node->nodeValue; } if ($node->nodeName === 'item') { $nodes[] = $val; } else { $nodes[$node->nodeName] = $val; } } } } return $nodes; } /** * Obtener un elemento del array * * @param $id * * @return mixed */ public function __get($id) { return $this->items[$id]; } /** * Guardar el archivo XML * * @param mixed $data Data to be saved * @param string $node * * @return XmlFileStorageInterface * @throws FileException * @throws RuntimeException */ public function save($data, string $node = 'root'): XmlFileStorageInterface { $this->fileHandler->checkIsWritable(); if (null === $data) { throw new RuntimeException(__u('There aren\'t any items to save')); } $this->setDOM(); $this->Dom->formatOutput = true; $this->root = $this->Dom->createElement($node); $this->Dom->appendChild($this->root); $this->writeChildNodes($data, $this->root); $this->fileHandler->save($this->Dom->saveXML()); return $this; } /** * Crear los nodos hijos recursivamente a partir de un array multidimensional * * @param mixed $items * @param DOMNode $node * @param null $type */ protected function writeChildNodes($items, DOMNode $node, $type = null) { foreach ($this->analyzeItems($items) as $key => $value) { if (is_int($key)) { $newNode = $this->Dom->createElement('item'); $newNode->setAttribute('type', $type); } else { $newNode = $this->Dom->createElement($key); } if (is_array($value)) { $this->writeChildNodes($value, $newNode, $key); } else if (is_object($value)) { $newNode->setAttribute('class', get_class($value)); $this->writeChildNodes($value, $newNode, $key); // $newNode->appendChild($this->Dom->createTextNode(base64_encode(serialize($value)))); } else { $newNode->appendChild($this->Dom->createTextNode(trim($value))); } $node->appendChild($newNode); } } /** * Analizar el tipo de elementos * * @param mixed $items * @param bool $serialize * * @return array|string */ protected function analyzeItems($items, $serialize = false) { if (is_array($items)) { ksort($items); return $items; } if (is_object($items)) { return $serialize ? serialize($items) : $this->analyzeObject($items); } return []; } /** * Analizar un elemento del tipo objeto * * @param $object * * @return array */ protected function analyzeObject($object) { $items = []; $reflection = new ReflectionObject($object); foreach ($reflection->getProperties() as $property) { $property->setAccessible(true); $value = $property->getValue($object); if (is_bool($value)) { $items[$property->getName()] = (int)$value; } elseif (is_numeric($value) && strpos($value, '.') === false ) { $items[$property->getName()] = (int)$value; } else { $items[$property->getName()] = $value; } $property->setAccessible(false); } ksort($items); return $items; } /** * Devolver los elementos cargados * * @return mixed */ public function getItems() { return $this->items; } /** * Establecer los elementos * * @param $items * * @return XmlHandler */ public function setItems($items) { $this->items = $items; return $this; } /** * @param $path * * @return string * @throws FileException */ public function getPathValue(string $path):string { $this->fileHandler->checkIsReadable(); $this->fileHandler->getFileSize(true); $dom = new DOMDocument('1.0', 'utf-8'); $dom->load($this->fileHandler->getFile()); $query = (new DOMXPath($dom))->query($path); if ($query->length === 0) { throw new RuntimeException(__u('XML node does not exist')); } return $query->item(0)->nodeValue; } /** * @return FileHandler */ public function getFileHandler(): FileHandler { return $this->fileHandler; } }