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();