diff --git a/source/FluidXml.php b/source/FluidXml.php index 6a0bd03..a159f20 100644 --- a/source/FluidXml.php +++ b/source/FluidXml.php @@ -43,6 +43,7 @@ use \FluidXml\Core\FluidInterface; use \FluidXml\Core\FluidDocument; +use \FluidXml\Core\FluidInsertionHandler; use \FluidXml\Core\FluidContext; use \FluidXml\Core\NewableTrait; use \FluidXml\Core\ReservedCallTrait; @@ -142,6 +143,7 @@ class FluidXml implements FluidInterface const ROOT_NODE = 'doc'; private $document; + private $handler; public static function load($document) { @@ -167,9 +169,6 @@ public static function load($document) public function __construct($root = null, $options = []) { - $this->document = new FluidDocument(); - $doc = $this->document; - $defaults = [ 'root' => self::ROOT_NODE, 'version' => '1.0', 'encoding' => 'UTF-8', @@ -187,11 +186,16 @@ public function __construct($root = null, $options = []) $opts = \array_merge($defaults, $options); + $this->document = new FluidDocument(); + $doc = $this->document; + $doc->dom = new \DOMDocument($opts['version'], $opts['encoding']); $doc->dom->formatOutput = true; $doc->dom->preserveWhiteSpace = false; - $doc->xpath = new \DOMXPath($doc->dom); + $doc->xpath = new \DOMXPath($doc->dom); + + $this->handler = new FluidInsertionHandler($doc); if (! empty($opts['root'])) { $this->appendSibling($opts['root']); @@ -412,7 +416,7 @@ protected function context() // a root node yet. Whether there is not a root node, the DOMDocument // is promoted as root node. if ($this->context === null) { - $this->context = new FluidContext($this->document, $this->document->dom); + $this->context = new FluidContext($this->document, $this->handler, $this->document->dom); } return $this->context; @@ -421,7 +425,7 @@ protected function context() if ($this->contextEl !== $this->document->dom->documentElement) { // The user can prepend a root node to the current root node. // In this case we have to update the context with the new first root node. - $this->context = new FluidContext($this->document, $this->document->dom->documentElement); + $this->context = new FluidContext($this->document, $this->handler, $this->document->dom->documentElement); $this->contextEl = $this->document->dom->documentElement; } @@ -631,17 +635,20 @@ class FluidDocument public $dom; public $xpath; public $namespaces = []; + public $handler; } class FluidRepeater { private $document; + private $handler; private $context; private $times; - public function __construct($document, $context, $times) + public function __construct($document, $handler, $context, $times) { $this->document = $document; + $this->handler = $handler; $this->context = $context; $this->times = $times; } @@ -657,791 +664,813 @@ public function __call($method, $arguments) } if ($new_context !== $this->context) { - return new FluidContext($this->document, $nodes); + return new FluidContext($this->document, $this->handler, $nodes); } return $this->context; } } -class FluidContext implements FluidInterface, \ArrayAccess, \Iterator +class FluidInsertionHandler { - use NewableTrait, - ReservedCallTrait, // For compatibility with PHP 5.6. - ReservedCallStaticTrait; // For compatibility with PHP 5.6. - private $document; - private $nodes = []; - private $seek = 0; + private $dom; + private $namespaces; - public function __construct($document, $context) + public function __construct($document) { - $this->document = $document; + $this->document = $document; + $this->dom = $document->dom; + $this->namespaces =& $document->namespaces; + } - if (! \is_array($context) && ! $context instanceof \Traversable) { - // DOMDocument, DOMElement and DOMNode are not iterable. - // DOMNodeList and FluidContext are iterable. - $context = [ $context ]; - } + public function insertElement(&$nodes, $element, &$optionals, $fn, $orig_context) + { + list($element, $attributes, $switch_context) = $this->handleOptionals($element, $optionals); - foreach ($context as $n) { - if (! $n instanceof \DOMNode) { - throw new \Exception('Node type not recognized.'); - } + $new_nodes = []; - $this->nodes[] = $n; + foreach ($nodes as $n) { + foreach ($element as $k => $v) { + // I give up, it's a too complex job for only one method like me. + $cx = $this->handleInsertion($n, $k, $v, $fn, $optionals); + $new_nodes = \array_merge($new_nodes, $cx); + } } - } - public function asArray() - { - return $this->nodes; - } + $new_context = $this->newContext($new_nodes); - // \ArrayAccess interface. - public function offsetSet($offset, $value) - { - // if (\is_null($offset)) { - // $this->nodes[] = $value; - // } else { - // $this->nodes[$offset] = $value; - // } - throw new \Exception('Setting a context element is not allowed.'); - } + // Setting the attributes is an help that the appendChild method + // offers to the user and is the same of: + // 1. appending a child switching the context + // 2. setting the attributes over the new context. + if (! empty($attributes)) { + $new_context->setAttribute($attributes); + } - // \ArrayAccess interface. - public function offsetExists($offset) - { - return isset($this->nodes[$offset]); + if ($switch_context) { + return $new_context; + } + + return $orig_context; } - // \ArrayAccess interface. - public function offsetUnset($offset) + protected function newContext(&$context) { - // unset($this->nodes[$offset]); - \array_splice($this->nodes, $offset, 1); + return new FluidContext($this->document, $this, $context); } - // \ArrayAccess interface. - public function offsetGet($offset) + protected function handleOptionals($element, &$optionals) { - if (isset($this->nodes[$offset])) { - return $this->nodes[$offset]; + if (! \is_array($element)) { + $element = [ $element ]; } - return null; - } + $switch_context = false; + $attributes = []; - // \Iterator interface. - public function rewind() - { - $this->seek = 0; - } + foreach ($optionals as $opt) { + if (\is_array($opt)) { + $attributes = $opt; - // \Iterator interface. - public function current() - { - return $this->nodes[$this->seek]; - } + } else if (\is_bool($opt)) { + $switch_context = $opt; - // \Iterator interface. - public function key() - { - return $this->seek; - } + } else if (\is_string($opt)) { + $e = \array_pop($element); - // \Iterator interface. - public function next() - { - ++$this->seek; - } + $element[$e] = $opt; - // \Iterator interface. - public function valid() - { - return isset($this->nodes[$this->seek]); - } + } else { + throw new \Exception("Optional argument '$opt' not recognized."); + } + } - public function length() - { - return \count($this->nodes); + return [ $element, $attributes, $switch_context ]; } - public function query(...$xpath) + + protected function handleInsertion($parent, $k, $v, $fn, &$optionals) { - if (\is_array($xpath[0])) { - $xpath = $xpath[0]; - } + // This is an highly optimized method. + // Good code design would split this method in many different handlers + // each one with its own checks. But it is too much expensive in terms + // of performances for a core method like this, so this implementation + // is prefered to collapse many identical checks to one. - $results = []; + ////////////////////// + // Key is a string. // + ////////////////////// - $xp = $this->document->xpath; + /////////////////////////////////////////////////////// + $k_is_string = \is_string($k); + $v_is_string = \is_string($v); + $v_is_xml = $v_is_string && is_an_xml_string($v); + $k_is_special = $k_is_string && $k[0] === '@'; + $k_isnt_special = ! $k_is_special; + $v_isnt_string = ! $v_is_string; + $v_isnt_xml = ! $v_is_xml; + /////////////////////////////////////////////////////// - foreach ($this->nodes as $n) { - foreach ($xpath as $x) { - // Returns a DOMNodeList. - $res = $xp->query($x, $n); + if ($k_is_string && $k_isnt_special && $v_is_string && $v_isnt_xml) { + return $this->insertStringString($parent, $k, $v, $fn, $optionals); + } - // Algorithm 1: - // $results = \array_merge($results, \iterator_to_array($res)); + if ($k_is_string && $k_isnt_special && $v_is_string && $v_is_xml) { + // TODO + } - // Algorithm 2: - // It is faster than \iterator_to_array and a lot faster - // than \iterator_to_array + \array_merge. - foreach ($res as $r) { - $results[] = $r; - } + ////////////////////////////////////////////// + $k_is_special_c = $k_is_special && $k === '@'; + ////////////////////////////////////////////// - // Algorithm 3: - // for ($i = 0, $l = $res->length; $i < $l; ++$i) { - // $results[] = $res->item($i); - // } - } + if ($k_is_special_c && $v_is_string) { + return $this->insertSpecialContent($parent, $k, $v, $fn, $optionals); } - // Performing over multiple sibling nodes a query that ascends - // the xpath, relative (../..) or absolute (//), returns identical - // matching results that must be collapsed in an unique result - // otherwise a subsequent operation is performed multiple times. - $results = $this->filterQueryResults($results); + ///////////////////////////////////////////////////// + $k_is_special_a = $k_is_special && ! $k_is_special_c; + ///////////////////////////////////////////////////// - return $this->newContext($results); - } + if ($k_is_special_a && $v_is_string) { + return $this->insertSpecialAttribute($parent, $k, $v, $fn, $optionals); + } - public function times($times, callable $fn = null) - { - if ($fn === null) { - return new FluidRepeater($this->document, $this, $times); + if ($k_is_string && $v_isnt_string) { + return $this->insertStringMixed($parent, $k, $v, $fn, $optionals); } - for ($i = 0; $i < $times; ++$i) { - $args = [$this, $i]; + //////////////////////// + // Key is an integer. // + //////////////////////// - if ($fn instanceof \Closure) { - $fn = $fn->bindTo($this); + //////////////////////////////// + $k_is_integer = \is_integer($k); + $v_is_array = \is_array($v); + //////////////////////////////// - \array_shift($args); + if ($k_is_integer && $v_is_array) { + return $this->insertIntegerArray($parent, $k, $v, $fn, $optionals); + } - // It is faster than \call_user_func. - $fn(...$args); - } else { - \call_user_func($fn, ...$args); - } + if ($k_is_integer && $v_is_string && $v_isnt_xml) { + return $this->insertIntegerString($parent, $k, $v, $fn, $optionals); } - return $this; - } + if ($k_is_integer && $v_is_string && $v_is_xml) { + return $this->insertIntegerXml($parent, $k, $v, $fn, $optionals); + } - public function each(callable $fn) - { - foreach ($this->nodes as $i => $n) { - $cx = $this->newContext($n); - $args = [$cx, $i, $n]; + ////////////////////////////////////////// + $v_is_domdoc = $v instanceof \DOMDocument; + ////////////////////////////////////////// - if ($fn instanceof \Closure) { - $fn = $fn->bindTo($cx); + if ($k_is_integer && $v_is_domdoc) { + return $this->insertIntegerDomdocument($parent, $k, $v, $fn, $optionals); + } - \array_shift($args); + /////////////////////////////////////////////// + $v_is_domnodelist = $v instanceof \DOMNodeList; + /////////////////////////////////////////////// - // It is faster than \call_user_func. - $fn(...$args); - } else { - \call_user_func($fn, ...$args); - } + if ($k_is_integer && $v_is_domnodelist) { + return $this->insertIntegerDomnodelist($parent, $k, $v, $fn, $optionals); } - return $this; - } + /////////////////////////////////////// + $v_is_domnode = $v instanceof \DOMNode; + /////////////////////////////////////// - // appendChild($child, $value?, $attributes? = [], $switchContext? = false) - public function appendChild($child, ...$optionals) - { - return $this->insertElement($child, $optionals, function($parent, $element) { - return $parent->appendChild($element); - }); - } + if ($k_is_integer && ! $v_is_domdoc && $v_is_domnode) { + return $this->insertIntegerDomnode($parent, $k, $v, $fn, $optionals); + } - // Alias of appendChild(). - public function add($child, ...$optionals) - { - return $this->appendChild($child, ...$optionals); - } + ////////////////////////////////////////////////// + $v_is_simplexml = $v instanceof \SimpleXMLElement; + ////////////////////////////////////////////////// - public function prependSibling($sibling, ...$optionals) - { - return $this->insertElement($sibling, $optionals, function($sibling, $element) { - return $sibling->parentNode->insertBefore($element, $sibling); - }); - } + if ($k_is_integer && $v_is_simplexml) { + return $this->insertIntegerSimplexml($parent, $k, $v, $fn, $optionals); + } - // Alias of prependSibling(). - public function prepend($sibling, ...$optionals) - { - return $this->prependSibling($sibling, ...$optionals); - } + //////////////////////////////////////// + $v_is_fluidxml = $v instanceof FluidXml; + //////////////////////////////////////// - // Alias of prependSibling(). - public function insertSiblingBefore($sibling, ...$optionals) - { - return $this->prependSibling($sibling, ...$optionals); - } + if ($k_is_integer && $v_is_fluidxml) { + return $this->insertIntegerFluidxml($parent, $k, $v, $fn, $optionals); + } - public function appendSibling($sibling, ...$optionals) - { - return $this->insertElement($sibling, $optionals, function($sibling, $element) { - // If ->nextSibling is null, $element is simply appended as last sibling. - return $sibling->parentNode->insertBefore($element, $sibling->nextSibling); - }); - } + /////////////////////////////////////////// + $v_is_fluidcx = $v instanceof FluidContext; + /////////////////////////////////////////// - // Alias of appendSibling(). - public function append($sibling, ...$optionals) - { - return $this->appendSibling($sibling, ...$optionals); - } + if ($k_is_integer && $v_is_fluidcx) { + return $this->insertIntegerFluidcontext($parent, $k, $v, $fn, $optionals); + } - // Alias of appendSibling(). - public function insertSiblingAfter($sibling, ...$optionals) - { - return $this->appendSibling($sibling, ...$optionals); + throw new \Exception('XML document not supported.'); } - // Arguments can be in the form of: - // setAttribute($name, $value) - // setAttribute(['name' => 'value', ...]) - public function setAttribute(...$arguments) + protected function createElement($name, $value = null) { - // Default case is: - // [ 'name' => 'value', ... ] - $attrs = $arguments[0]; + // The DOMElement instance must be different for every node, + // otherwise only one element is attached to the DOM. - // If the first argument is not an array, - // the user has passed two arguments: - // 1. is the attribute name - // 2. is the attribute value - if (! \is_array($arguments[0])) { - $attrs = [$arguments[0] => $arguments[1]]; - } + $id = null; + $uri = null; - foreach ($this->nodes as $n) { - foreach ($attrs as $k => $v) { - // Algorithm 1: - $n->setAttribute($k, $v); + // The node name can contain the namespace id prefix. + // Example: xsl:template + $colon_pos = \strpos($name, ':'); - // Algorithm 2: - // $n->setAttributeNode(new \DOMAttr($k, $v)); + if ($colon_pos !== false) { + $id = \substr($name, 0, $colon_pos); + $name = \substr($name, $colon_pos + 1); + } - // Algorithm 3: - // $n->appendChild(new \DOMAttr($k, $v)); + if ($id !== null) { + $ns = $this->namespaces[$id]; + $uri = $ns->uri(); - // Algorithm 2 and 3 have a different behaviour - // from Algorithm 1. - // The attribute is still created or setted, but - // changing the value of an existing attribute - // changes even the order of that attribute - // in the attribute list. + if ($ns->mode() === FluidNamespace::MODE_EXPLICIT) { + $name = "{$id}:{$name}"; } } - return $this; - } + // Algorithm 1: + $el = new \DOMElement($name, $value, $uri); - // Alias of setAttribute(). - public function attr(...$arguments) - { - return $this->setAttribute(...$arguments); + // Algorithm 2: + // $el = $dom->createElement($name, $value); + + return $el; } - public function appendText($text) + protected function attachNodes($parent, $nodes, $fn) { - foreach ($this->nodes as $n) { - $n->appendChild(new \DOMText($text)); + if (! \is_array($nodes) && ! $nodes instanceof \Traversable) { + $nodes = [ $nodes ]; } - return $this; - } + $context = []; - public function appendCdata($text) - { - foreach ($this->nodes as $n) { - $n->appendChild(new \DOMCDATASection($text)); + foreach ($nodes as $el) { + $el = $this->dom->importNode($el, true); + $context[] = $fn($parent, $el); } - return $this; + return $context; } - public function setText($text) + protected function insertSpecialContent($parent, $k, $v) { - foreach ($this->nodes as $n) { - // Algorithm 1: - $n->nodeValue = $text; + // The user has passed an element text content: + // [ '@' => 'Element content.' ] - // Algorithm 2: - // foreach ($n->childNodes as $c) { - // $n->removeChild($c); - // } - // $n->appendChild(new \DOMText($text)); + // Algorithm 1: + $this->newContext($parent)->appendText($v); - // Algorithm 3: - // foreach ($n->childNodes as $c) { - // $n->replaceChild(new \DOMText($text), $c); - // } - } + // Algorithm 2: + // $this->setText($v); - return $this; - } + // The user can specify multiple '@' special elements + // so Algorithm 1 is the right choice. - // Alias of setText(). - public function text($text) - { - return $this->setText($text); + return []; } - public function setCdata($text) + protected function insertSpecialAttribute($parent, $k, $v) { - foreach ($this->nodes as $n) { - $n->nodeValue = ''; - $n->appendChild(new \DOMCDATASection($text)); - } + // The user has passed an attribute name and an attribute value: + // [ '@attribute' => 'Attribute content' ] - return $this; - } + $attr = \substr($k, 1); + $this->newContext($parent)->setAttribute($attr, $v); - // Alias of setCdata(). - public function cdata($text) - { - return $this->setCdata($text); + return []; } - public function remove(...$xpath) + protected function insertStringString($parent, $k, $v, $fn) { - // Arguments can be empty, a string or an array of strings. - - if (empty($xpath)) { - // The user has requested to remove the nodes of this context. - $targets = $this->nodes; - } else { - $targets = $this->query(...$xpath); - } + // The user has passed an element name and an element value: + // [ 'element' => 'Element content' ] - foreach ($targets as $t) { - $t->parentNode->removeChild($t); - } + $el = $this->createElement($k, $v); + $el = $fn($parent, $el); - return $this; + return [ $el ]; } - public function xml($strip = false) + protected function insertStringMixed($parent, $k, $v, $fn, &$optionals) { - return domnodes_to_string($this->nodes); - } + // The user has passed one of these two cases: + // - [ 'element' => [...] ] + // - [ 'element' => DOMNode|SimpleXMLElement|FluidXml ] - protected function newContext(&$context) - { - return new FluidContext($this->document, $context); + $el = $this->createElement($k); + $el = $fn($parent, $el); + + // The new children elements must be created in the order + // they are supplied, so 'appendChild' is the perfect operation. + $this->newContext($el)->appendChild($v, ...$optionals); + + return [ $el ]; } - protected function filterQueryResults(&$results) + protected function insertIntegerArray($parent, $k, $v, $fn, &$optionals) { - $set = []; + // The user has passed a wrapper array: + // [ [...], ... ] - foreach ($results as $r) { - $found = false; + $context = []; - foreach ($set as $u) { - if ($r === $u) { - $found = true; - } - } + foreach ($v as $kk => $vv) { + $cx = $this->handleInsertion($parent, $kk, $vv, $fn, $optionals); - if (! $found) { - $set[] = $r; - } + $context = \array_merge($context, $cx); } - return $set; + return $context; } - protected function handleOptionals($element, &$optionals) + protected function insertIntegerString($parent, $k, $v, $fn) { - if (! \is_array($element)) { - $element = [ $element ]; - } + // The user has passed a node name without a node value: + // [ 'element', ... ] - $switch_context = false; - $attributes = []; + $el = $this->createElement($v); + $el = $fn($parent, $el); - foreach ($optionals as $opt) { - if (\is_array($opt)) { - $attributes = $opt; + return [ $el ]; + } - } else if (\is_bool($opt)) { - $switch_context = $opt; + protected function insertIntegerXml($parent, $k, $v, $fn) + { + // The user has passed an XML document instance: + // [ '', DOMNode, SimpleXMLElement, FluidXml ] - } else if (\is_string($opt)) { - $e = \array_pop($element); + $wrapper = new \DOMDocument(); + $wrapper->formatOutput = true; + $wrapper->preserveWhiteSpace = false; - $element[$e] = $opt; + $v = \ltrim($v); - } else { - throw new \Exception("Optional argument '$opt' not recognized."); - } - } - - return [ $element, $attributes, $switch_context ]; - } - - protected function insertElement($element, &$optionals, $fn) - { - list($element, $attributes, $switch_context) = $this->handleOptionals($element, $optionals); - - $nodes = []; - - foreach ($this->nodes as $n) { - foreach ($element as $k => $v) { - // I give up, it's a too complex job for only one method like me. - $cx = $this->handleInsertion($n, $k, $v, $fn, $optionals); - - $nodes = \array_merge($nodes, $cx); - } - } - - $new_context = $this->newContext($nodes); + if ($v[1] === '?') { + $wrapper->loadXML($v); + $nodes = $wrapper->childNodes; + } else { + // A way to import strings with multiple root nodes. + $wrapper->loadXML("$v"); - // Setting the attributes is an help that the appendChild method - // offers to the user and is the same of: - // 1. appending a child switching the context - // 2. setting the attributes over the new context. - if (! empty($attributes)) { - $new_context->setAttribute($attributes); - } + // Algorithm 1: + $nodes = $wrapper->documentElement->childNodes; - if ($switch_context) { - return $new_context; + // Algorithm 2: + // $dom_xp = new \DOMXPath($dom); + // $nodes = $dom_xp->query('/root/*'); } - return $this; + return $this->attachNodes($parent, $nodes, $fn); } - protected function handleInsertion($parent, $k, $v, $fn, &$optionals) + protected function insertIntegerDomdocument($parent, $k, $v, $fn) { - // This is an highly optimized method. - // Good code design would split this method in many different handlers - // each one with its own checks. But it is too much expensive in terms - // of performances for a core method like this, so this implementation - // is prefered to collapse many identical checks to one. - - ////////////////////// - // Key is a string. // - ////////////////////// + // A DOMDocument can have multiple root nodes. - /////////////////////////////////////////////////////// - $k_is_string = \is_string($k); - $v_is_string = \is_string($v); - $v_is_xml = $v_is_string && is_an_xml_string($v); - $k_is_special = $k_is_string && $k[0] === '@'; - $k_isnt_special = ! $k_is_special; - $v_isnt_string = ! $v_is_string; - $v_isnt_xml = ! $v_is_xml; - /////////////////////////////////////////////////////// + // Algorithm 1: + return $this->attachNodes($parent, $v->childNodes, $fn); - if ($k_is_string && $k_isnt_special && $v_is_string && $v_isnt_xml) { - return $this->insertStringString($parent, $k, $v, $fn, $optionals); - } + // Algorithm 2: + // return $this->attachNodes($parent, $v->documentElement, $fn); + } - if ($k_is_string && $k_isnt_special && $v_is_string && $v_is_xml) { - // TODO - } + protected function insertIntegerDomnodelist($parent, $k, $v, $fn) + { + return $this->attachNodes($parent, $v, $fn); + } - ////////////////////////////////////////////// - $k_is_special_c = $k_is_special && $k === '@'; - ////////////////////////////////////////////// + protected function insertIntegerDomnode($parent, $k, $v, $fn) + { + return $this->attachNodes($parent, $v, $fn); + } - if ($k_is_special_c && $v_is_string) { - return $this->insertSpecialContent($parent, $k, $v, $fn, $optionals); - } + protected function insertIntegerSimplexml($parent, $k, $v, $fn) + { + return $this->attachNodes($parent, \dom_import_simplexml($v), $fn); + } - ///////////////////////////////////////////////////// - $k_is_special_a = $k_is_special && ! $k_is_special_c; - ///////////////////////////////////////////////////// + protected function insertIntegerFluidxml($parent, $k, $v, $fn) + { + return $this->attachNodes($parent, $v->dom()->documentElement, $fn); + } - if ($k_is_special_a && $v_is_string) { - return $this->insertSpecialAttribute($parent, $k, $v, $fn, $optionals); - } + protected function insertIntegerFluidcontext($parent, $k, $v, $fn) + { + return $this->attachNodes($parent, $v->asArray(), $fn); + } +} - if ($k_is_string && $v_isnt_string) { - return $this->insertStringMixed($parent, $k, $v, $fn, $optionals); - } +class FluidContext implements FluidInterface, \ArrayAccess, \Iterator +{ + use NewableTrait, + ReservedCallTrait, // For compatibility with PHP 5.6. + ReservedCallStaticTrait; // For compatibility with PHP 5.6. - //////////////////////// - // Key is an integer. // - //////////////////////// + private $document; + private $handler; + private $nodes = []; + private $seek = 0; - //////////////////////////////// - $k_is_integer = \is_integer($k); - $v_is_array = \is_array($v); - //////////////////////////////// + public function __construct($document, $handler, $context) + { + $this->document = $document; + $this->handler = $handler; - if ($k_is_integer && $v_is_array) { - return $this->insertIntegerArray($parent, $k, $v, $fn, $optionals); + if (! \is_array($context) && ! $context instanceof \Traversable) { + // DOMDocument, DOMElement and DOMNode are not iterable. + // DOMNodeList and FluidContext are iterable. + $context = [ $context ]; } - if ($k_is_integer && $v_is_string && $v_isnt_xml) { - return $this->insertIntegerString($parent, $k, $v, $fn, $optionals); - } + foreach ($context as $n) { + if (! $n instanceof \DOMNode) { + throw new \Exception('Node type not recognized.'); + } - if ($k_is_integer && $v_is_string && $v_is_xml) { - return $this->insertIntegerXml($parent, $k, $v, $fn, $optionals); + $this->nodes[] = $n; } + } - ////////////////////////////////////////// - $v_is_domdoc = $v instanceof \DOMDocument; - ////////////////////////////////////////// - - if ($k_is_integer && $v_is_domdoc) { - return $this->insertIntegerDomdocument($parent, $k, $v, $fn, $optionals); - } + public function asArray() + { + return $this->nodes; + } - /////////////////////////////////////////////// - $v_is_domnodelist = $v instanceof \DOMNodeList; - /////////////////////////////////////////////// + // \ArrayAccess interface. + public function offsetSet($offset, $value) + { + // if (\is_null($offset)) { + // $this->nodes[] = $value; + // } else { + // $this->nodes[$offset] = $value; + // } + throw new \Exception('Setting a context element is not allowed.'); + } - if ($k_is_integer && $v_is_domnodelist) { - return $this->insertIntegerDomnodelist($parent, $k, $v, $fn, $optionals); - } + // \ArrayAccess interface. + public function offsetExists($offset) + { + return isset($this->nodes[$offset]); + } - /////////////////////////////////////// - $v_is_domnode = $v instanceof \DOMNode; - /////////////////////////////////////// + // \ArrayAccess interface. + public function offsetUnset($offset) + { + // unset($this->nodes[$offset]); + \array_splice($this->nodes, $offset, 1); + } - if ($k_is_integer && ! $v_is_domdoc && $v_is_domnode) { - return $this->insertIntegerDomnode($parent, $k, $v, $fn, $optionals); + // \ArrayAccess interface. + public function offsetGet($offset) + { + if (isset($this->nodes[$offset])) { + return $this->nodes[$offset]; } - ////////////////////////////////////////////////// - $v_is_simplexml = $v instanceof \SimpleXMLElement; - ////////////////////////////////////////////////// + return null; + } - if ($k_is_integer && $v_is_simplexml) { - return $this->insertIntegerSimplexml($parent, $k, $v, $fn, $optionals); - } + // \Iterator interface. + public function rewind() + { + $this->seek = 0; + } - //////////////////////////////////////// - $v_is_fluidxml = $v instanceof FluidXml; - //////////////////////////////////////// + // \Iterator interface. + public function current() + { + return $this->nodes[$this->seek]; + } - if ($k_is_integer && $v_is_fluidxml) { - return $this->insertIntegerFluidxml($parent, $k, $v, $fn, $optionals); - } + // \Iterator interface. + public function key() + { + return $this->seek; + } - /////////////////////////////////////////// - $v_is_fluidcx = $v instanceof FluidContext; - /////////////////////////////////////////// + // \Iterator interface. + public function next() + { + ++$this->seek; + } - if ($k_is_integer && $v_is_fluidcx) { - return $this->insertIntegerFluidcontext($parent, $k, $v, $fn, $optionals); - } + // \Iterator interface. + public function valid() + { + return isset($this->nodes[$this->seek]); + } - throw new \Exception('XML document not supported.'); + public function length() + { + return \count($this->nodes); } - protected function createElement($name, $value = null) + public function query(...$xpath) { - // The DOMElement instance must be different for every node, - // otherwise only one element is attached to the DOM. + if (\is_array($xpath[0])) { + $xpath = $xpath[0]; + } - $id = null; - $uri = null; + $results = []; - // The node name can contain the namespace id prefix. - // Example: xsl:template - $colon_pos = \strpos($name, ':'); + $xp = $this->document->xpath; - if ($colon_pos !== false) { - $id = \substr($name, 0, $colon_pos); - $name = \substr($name, $colon_pos + 1); - } + foreach ($this->nodes as $n) { + foreach ($xpath as $x) { + // Returns a DOMNodeList. + $res = $xp->query($x, $n); - if ($id !== null) { - $ns = $this->document->namespaces[$id]; - $uri = $ns->uri(); + // Algorithm 1: + // $results = \array_merge($results, \iterator_to_array($res)); - if ($ns->mode() === FluidNamespace::MODE_EXPLICIT) { - $name = "{$id}:{$name}"; + // Algorithm 2: + // It is faster than \iterator_to_array and a lot faster + // than \iterator_to_array + \array_merge. + foreach ($res as $r) { + $results[] = $r; + } + + // Algorithm 3: + // for ($i = 0, $l = $res->length; $i < $l; ++$i) { + // $results[] = $res->item($i); + // } } } - // Algorithm 1: - $el = new \DOMElement($name, $value, $uri); - - // Algorithm 2: - // $el = $dom->createElement($name, $value); + // Performing over multiple sibling nodes a query that ascends + // the xpath, relative (../..) or absolute (//), returns identical + // matching results that must be collapsed in an unique result + // otherwise a subsequent operation is performed multiple times. + $results = $this->filterQueryResults($results); - return $el; + return $this->newContext($results); } - protected function attachNodes($parent, $nodes, $fn) + public function times($times, callable $fn = null) { - if (! \is_array($nodes) && ! $nodes instanceof \Traversable) { - $nodes = [ $nodes ]; + if ($fn === null) { + return new FluidRepeater($this->document, $this->handler, $this, $times); } - $context = []; + for ($i = 0; $i < $times; ++$i) { + $args = [$this, $i]; + + if ($fn instanceof \Closure) { + $fn = $fn->bindTo($this); + + \array_shift($args); - foreach ($nodes as $el) { - $el = $this->document->dom->importNode($el, true); - $context[] = $fn($parent, $el); + // It is faster than \call_user_func. + $fn(...$args); + } else { + \call_user_func($fn, ...$args); + } } - return $context; + return $this; } - protected function insertSpecialContent($parent, $k, $v) + public function each(callable $fn) { - // The user has passed an element text content: - // [ '@' => 'Element content.' ] + foreach ($this->nodes as $i => $n) { + $cx = $this->newContext($n); + $args = [$cx, $i, $n]; - // Algorithm 1: - $this->newContext($parent)->appendText($v); + if ($fn instanceof \Closure) { + $fn = $fn->bindTo($cx); - // Algorithm 2: - // $this->setText($v); + \array_shift($args); - // The user can specify multiple '@' special elements - // so Algorithm 1 is the right choice. + // It is faster than \call_user_func. + $fn(...$args); + } else { + \call_user_func($fn, ...$args); + } + } - return []; + return $this; } - protected function insertSpecialAttribute($parent, $k, $v) + // appendChild($child, $value?, $attributes? = [], $switchContext? = false) + public function appendChild($child, ...$optionals) { - // The user has passed an attribute name and an attribute value: - // [ '@attribute' => 'Attribute content' ] - - $attr = \substr($k, 1); - $this->newContext($parent)->setAttribute($attr, $v); - - return []; + return $this->handler->insertElement($this->nodes, $child, $optionals, function($parent, $element) { + return $parent->appendChild($element); + }, $this); } - protected function insertStringString($parent, $k, $v, $fn) + // Alias of appendChild(). + public function add($child, ...$optionals) { - // The user has passed an element name and an element value: - // [ 'element' => 'Element content' ] + return $this->appendChild($child, ...$optionals); + } - $el = $this->createElement($k, $v); - $el = $fn($parent, $el); + public function prependSibling($sibling, ...$optionals) + { + return $this->handler->insertElement($this->nodes, $sibling, $optionals, function($sibling, $element) { + return $sibling->parentNode->insertBefore($element, $sibling); + }, $this); + } - return [ $el ]; + // Alias of prependSibling(). + public function prepend($sibling, ...$optionals) + { + return $this->prependSibling($sibling, ...$optionals); } - protected function insertStringMixed($parent, $k, $v, $fn, &$optionals) + // Alias of prependSibling(). + public function insertSiblingBefore($sibling, ...$optionals) { - // The user has passed one of these two cases: - // - [ 'element' => [...] ] - // - [ 'element' => DOMNode|SimpleXMLElement|FluidXml ] + return $this->prependSibling($sibling, ...$optionals); + } - $el = $this->createElement($k); - $el = $fn($parent, $el); + public function appendSibling($sibling, ...$optionals) + { + return $this->handler->insertElement($this->nodes, $sibling, $optionals, function($sibling, $element) { + // If ->nextSibling is null, $element is simply appended as last sibling. + return $sibling->parentNode->insertBefore($element, $sibling->nextSibling); + }, $this); + } - // The new children elements must be created in the order - // they are supplied, so 'appendChild' is the perfect operation. - $this->newContext($el)->appendChild($v, ...$optionals); + // Alias of appendSibling(). + public function append($sibling, ...$optionals) + { + return $this->appendSibling($sibling, ...$optionals); + } - return [ $el ]; + // Alias of appendSibling(). + public function insertSiblingAfter($sibling, ...$optionals) + { + return $this->appendSibling($sibling, ...$optionals); } - protected function insertIntegerArray($parent, $k, $v, $fn, &$optionals) + // Arguments can be in the form of: + // setAttribute($name, $value) + // setAttribute(['name' => 'value', ...]) + public function setAttribute(...$arguments) { - // The user has passed a wrapper array: - // [ [...], ... ] + // Default case is: + // [ 'name' => 'value', ... ] + $attrs = $arguments[0]; - $context = []; + // If the first argument is not an array, + // the user has passed two arguments: + // 1. is the attribute name + // 2. is the attribute value + if (! \is_array($arguments[0])) { + $attrs = [$arguments[0] => $arguments[1]]; + } - foreach ($v as $kk => $vv) { - $cx = $this->handleInsertion($parent, $kk, $vv, $fn, $optionals); + foreach ($this->nodes as $n) { + foreach ($attrs as $k => $v) { + // Algorithm 1: + $n->setAttribute($k, $v); - $context = \array_merge($context, $cx); + // Algorithm 2: + // $n->setAttributeNode(new \DOMAttr($k, $v)); + + // Algorithm 3: + // $n->appendChild(new \DOMAttr($k, $v)); + + // Algorithm 2 and 3 have a different behaviour + // from Algorithm 1. + // The attribute is still created or setted, but + // changing the value of an existing attribute + // changes even the order of that attribute + // in the attribute list. + } } - return $context; + return $this; } - protected function insertIntegerString($parent, $k, $v, $fn) + // Alias of setAttribute(). + public function attr(...$arguments) { - // The user has passed a node name without a node value: - // [ 'element', ... ] - - $el = $this->createElement($v); - $el = $fn($parent, $el); - - return [ $el ]; + return $this->setAttribute(...$arguments); } - protected function insertIntegerXml($parent, $k, $v, $fn) + public function appendText($text) { - // The user has passed an XML document instance: - // [ '', DOMNode, SimpleXMLElement, FluidXml ] + foreach ($this->nodes as $n) { + $n->appendChild(new \DOMText($text)); + } - $wrapper = new \DOMDocument(); - $wrapper->formatOutput = true; - $wrapper->preserveWhiteSpace = false; + return $this; + } - $v = \ltrim($v); + public function appendCdata($text) + { + foreach ($this->nodes as $n) { + $n->appendChild(new \DOMCDATASection($text)); + } - if ($v[1] === '?') { - $wrapper->loadXML($v); - $nodes = $wrapper->childNodes; - } else { - // A way to import strings with multiple root nodes. - $wrapper->loadXML("$v"); + return $this; + } + public function setText($text) + { + foreach ($this->nodes as $n) { // Algorithm 1: - $nodes = $wrapper->documentElement->childNodes; + $n->nodeValue = $text; // Algorithm 2: - // $dom_xp = new \DOMXPath($dom); - // $nodes = $dom_xp->query('/root/*'); + // foreach ($n->childNodes as $c) { + // $n->removeChild($c); + // } + // $n->appendChild(new \DOMText($text)); + + // Algorithm 3: + // foreach ($n->childNodes as $c) { + // $n->replaceChild(new \DOMText($text), $c); + // } } - return $this->attachNodes($parent, $nodes, $fn); + return $this; } - protected function insertIntegerDomdocument($parent, $k, $v, $fn) + // Alias of setText(). + public function text($text) { - // A DOMDocument can have multiple root nodes. + return $this->setText($text); + } - // Algorithm 1: - return $this->attachNodes($parent, $v->childNodes, $fn); + public function setCdata($text) + { + foreach ($this->nodes as $n) { + $n->nodeValue = ''; + $n->appendChild(new \DOMCDATASection($text)); + } - // Algorithm 2: - // return $this->attachNodes($parent, $v->documentElement, $fn); + return $this; } - protected function insertIntegerDomnodelist($parent, $k, $v, $fn) + // Alias of setCdata(). + public function cdata($text) { - return $this->attachNodes($parent, $v, $fn); + return $this->setCdata($text); } - protected function insertIntegerDomnode($parent, $k, $v, $fn) + public function remove(...$xpath) { - return $this->attachNodes($parent, $v, $fn); + // Arguments can be empty, a string or an array of strings. + + if (empty($xpath)) { + // The user has requested to remove the nodes of this context. + $targets = $this->nodes; + } else { + $targets = $this->query(...$xpath); + } + + foreach ($targets as $t) { + $t->parentNode->removeChild($t); + } + + return $this; } - protected function insertIntegerSimplexml($parent, $k, $v, $fn) + public function xml($strip = false) { - return $this->attachNodes($parent, \dom_import_simplexml($v), $fn); + return domnodes_to_string($this->nodes); } - protected function insertIntegerFluidxml($parent, $k, $v, $fn) + protected function newContext(&$context) { - return $this->attachNodes($parent, $v->dom()->documentElement, $fn); + return new FluidContext($this->document, $this->handler, $context); } - protected function insertIntegerFluidcontext($parent, $k, $v, $fn) + protected function filterQueryResults(&$results) { - return $this->attachNodes($parent, $v->asArray(), $fn); + $set = []; + + foreach ($results as $r) { + $found = false; + + foreach ($set as $u) { + if ($r === $u) { + $found = true; + } + } + + if (! $found) { + $set[] = $r; + } + } + + return $set; } + } } // END OF NAMESPACE FluidXml\Core diff --git a/specs/FluidXml.php b/specs/FluidXml.php index 098d9c1..9247e72 100644 --- a/specs/FluidXml.php +++ b/specs/FluidXml.php @@ -5,6 +5,7 @@ use \FluidXml\FluidXml; use \FluidXml\FluidNamespace; use \FluidXml\Core\FluidContext; +use \FluidXml\Core\FluidInsertionHandler; use \FluidXml\Core\FluidDocument; use \FluidXml\Core\FluidRepeater; use function \FluidXml\fluidxml; @@ -1843,7 +1844,9 @@ function addchild($parent, $i) it('should accept a DOMDocument', function() { $xml = new FluidXml(); - $new_cx = new FluidContext(new FluidDocument(), $xml->dom()); + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $new_cx = new FluidContext($doc, $handler, $xml->dom()); $actual = $new_cx[0]; $expected = $xml->dom(); @@ -1854,7 +1857,9 @@ function addchild($parent, $i) $xml = new FluidXml(); $cx = $xml->appendChild(['head'], true); - $new_cx = new FluidContext(new FluidDocument(), $cx[0]); + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $new_cx = new FluidContext($doc, $handler, $cx[0]); $actual = $new_cx->asArray(); $expected = $cx->asArray(); @@ -1865,7 +1870,9 @@ function addchild($parent, $i) $xml = new FluidXml(); $cx = $xml->appendChild(['head', 'body'], true); - $new_cx = new FluidContext(new FluidDocument(), $cx->asArray()); + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $new_cx = new FluidContext($doc, $handler, $cx->asArray()); $actual = $new_cx->asArray(); $expected = $cx->asArray(); @@ -1880,7 +1887,9 @@ function addchild($parent, $i) $domxp = new \DOMXPath($dom); $nodes = $domxp->query('/doc/*'); - $new_cx = new FluidContext(new FluidDocument(), $nodes); + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $new_cx = new FluidContext($doc, $handler, $nodes); $actual = $new_cx->asArray(); $expected = $cx->asArray(); @@ -1891,7 +1900,9 @@ function addchild($parent, $i) $xml = new FluidXml(); $cx = $xml->appendChild(['head', 'body'], true); - $new_cx = new FluidContext(new FluidDocument(), $cx); + $doc = new FluidDocument(); + $handler = new FluidInsertionHandler($doc); + $new_cx = new FluidContext($doc, $handler, $cx); $actual = $new_cx->asArray(); $expected = $cx->asArray();