Source for file Core.php
Documentation is available at Core.php
* FluentDOMCore implements the core and interface functions for FluentDOM
* @version $Id: Core.php 441 2010-04-16 18:24:08Z subjective $
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @copyright Copyright (c) 2009 Bastian Feder, Thomas Weinert
* @tutorial FluentDOM.pkg
* Include the external iterator class.
require_once(dirname(__FILE__ ). '/Iterator.php');
* Include the loader interface.
require_once(dirname(__FILE__ ). '/Loader.php');
* Include the handler class.
require_once(dirname(__FILE__ ). '/Handler.php');
* FluentDOMCore implements the core and interface functions for FluentDOM
* @property string $contentType Output type - text/xml or text/html
* @property-read integer $length The amount of elements found by selector.
* @property-read DOMDocument $document Internal DOMDocument object
* @property-read DOMXPath $xpath Internal XPath object
class FluentDOMCore implements IteratorAggregate, Countable, ArrayAccess {
* Associated DOMDocument object.
* @var DOMDocument $_document
* XPath object used to execute selectors
* List of namespaces to be registered for xpath expressions
* Use document context for expression (not selected nodes).
* @var boolean $_useDocumentContext
* Content type for output (xml, text/xml, html, text/html).
* @var string $_contentType
* Parent FluentDOM object (previous selection in chain).
* @var FluentDOM $_parent
* Seleted element and text nodes
* Load a $source. The type of the source depends on the loaders. If no explicit loaders are set
* FluentDOM will use a set of default loaders for xml/html and DOM.
* @param string $contentType optional, default value 'text/xml'
public function load($source, $contentType = 'text/xml') {
$this->_xpath = $source->_xpath;
if ($loaded = $loader->load($source, $this->_contentType)) {
if ($loaded instanceof DOMDocument) {
$loaded[0] instanceof DOMDocument &&
throw new InvalidArgumentException('Invalid source object.');
* Initialize default loaders if they are not already initialized
include_once($path. '/Loader/DOMNode.php');
include_once($path. '/Loader/DOMDocument.php');
include_once($path. '/Loader/StringXML.php');
include_once($path. '/Loader/FileXML.php');
include_once($path. '/Loader/StringHTML.php');
include_once($path. '/Loader/FileHTML.php');
* Define own loading handlers
* @example iniloader/iniToXML.php Usage Example: Own loader object
foreach ($loaders as $loader) {
throw new InvalidArgumentException('Array contains invalid loader object');
* Setter for FluentDOM::_contentType property
$newContentType = 'text/xml';
$newContentType = 'text/html';
throw new UnexpectedValueException('Invalid content type value');
$this->_parent->contentType = $newContentType;
* implement dynamic properties using magic methods
public function __get($name) {
* block changes of dynamic readonly property length
public function __set($name, $value) {
throw new BadMethodCallException('Can not set readonly value.');
* support isset for dynamic properties length and document
* Return the XML output of the internal dom document
* The item() method is used to access elements in the node list,
* @param integer $position
public function item($position) {
if (isset ($this->_array[$position])) {
return $this->_array[$position];
* Formats the current document, resets internal node array and other properties.
* The document is saved and reloaded, all variables with DOMNodes
* of this document will get invalid.
if (isset ($contentType)) {
$this->_document->preserveWhiteSpace = FALSE;
if (!empty($this->_document->documentElement)) {
* Interface - IteratorAggregate
* Get an iterator for this object.
* @example interfaces/Iterator.php Usage Example: Iterator Interface
* @example interfaces/RecursiveIterator.php Usage Example: Recursive Iterator Interface
* @return FluentDOMIterator
* Get element count (Countable interface)
* @example interfaces/Countable.php Usage Example: Countable Interface
public function count() {
* Interface - ArrayAccess
* If somebody tries to modify the internal array throw an exception.
* @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
throw new BadMethodCallException('List is read only');
* Check if index exists in internal array
* @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
return isset ($this->_array[$offset]);
* If somebody tries to remove an element from the internal array throw an exception.
* @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
throw new BadMethodCallException('List is read only');
* Get element from internal array
* @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
return isset ($this->_array[$offset]) ? $this->_array[$offset] : NULL;
* Create a new instance of the same class with $this as the parent. This is used for the chaining.
public function spawn() {
$result = new $className();
return $result->load($this);
* Push new element(s) an the internal element list
* @param DOMNode|DOMNodeList|FluentDOM$elements
* @param boolean $ignoreTextNodes ignore text nodes
public function push($elements, $ignoreTextNodes = FALSE) {
if ($this->_isNode($elements, $ignoreTextNodes)) {
$elements = array($elements);
foreach ($elements as $index => $node) {
if ($this->_isNode($node, $ignoreTextNodes)) {
if ($node->ownerDocument === $this->_document) {
throw new OutOfBoundsException(
'Node #%d is not a part of this document', $index
throw new InvalidArgumentException('Invalid elements variable.');
* Sorts an array of DOM nodes based on document position, in place, with the duplicates removed.
* Note that this only works on arrays of DOM nodes, not strings or numbers.
* @param array $array array of DOM nodes
public function unique(array $array) {
foreach ($array as $node) {
if (!($node instanceof DOMNode)) {
throw new InvalidArgumentException(
'Array must only contain dom nodes, found "%s".',
if (isset ($node->parentNode) ||
$node === $node->ownerDocument->documentElement) {
$position = (integer) $this->_xpath()->evaluate('count(preceding::node())', $node);
/* use the document position as index, ignore duplicates */
if (!isset ($sortable[$position])) {
$sortable[$position] = $node;
/* use the object hash as index, ignore duplicates */
if (!isset ($unsortable[$hash])) {
$unsortable[$hash] = $node;
ksort($sortable, SORT_NUMERIC);
$result = array_values($sortable);
array_splice($result, count($result), 0, array_values($unsortable));
* Sorts the selected nodes, with the duplicates removed.
* @uses FluentDOMCore::unique
* @param array $array array of DOM nodes
* Gives access to an xpath evaluate on the current document
* @param DOMNode $context
public function evaluate($expr, DOMNode $context = NULL) {
* Register namespaces and or get namespaces
* @param array $namespaces If this parameter is empty the current namespaces are returned
* @return array|FluentDOMCore
public function namespaces(array $namespaces = NULL) {
if (is_null($namespaces)) {
foreach ($namespaces as $prefix => $uri) {
$this->_xpath()->registerNamespace($prefix, $uri);
* Get a XPath object associated with the internal DOMDocument and register
* default namespaces from the document element if availiable.
$this->_xpath->registerNamespace($prefix, $uri);
$uri = $this->_document->documentElement->lookupnamespaceURI('_');
$uri = $this->_document->documentElement->lookupnamespaceURI(NULL);
$this->_xpath->registerNamespace('_', $uri);
* Match XPath expression agains context and return matched elements.
* @param DOMNode $context optional, default value NULL
protected function _match($expr, $context = NULL) {
return $this->_xpath()->query($expr, $context);
return $this->_xpath()->query($expr);
* Test xpath expression against context and return true/false
* @param DOMNode $context optional, default value NULL
protected function _test($expr, $context = NULL) {
if ($check instanceof DOMNodeList) {
return $check->length > 0;
* Check if object is already in internal list
protected function _inList($node) {
foreach ($this->_array as $compareNode) {
if ($compareNode === $node) {
* Validate string as qualified node name
throw new UnexpectedValueException('Invalid QName: QName is empty.');
} elseif (FALSE !== ($position = strpos($name, ':'))) {
* Validate string as qualified node name part (namespace or local name)
* @param string $name full QName
* @param integer $offset Offset of NCName part in QName
* @param integer $length Length of NCName part in QName
protected function _isNCName($name, $offset = 0, $length = 0) {
'\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}'.
'\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}'.
'\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}'.
'\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}';
'\\.\\d\\x{B7}\\x{300}-\\x{36F}\\x{203F}-\\x{2040}';
$namePart = substr($name, $offset, $length);
$namePart = substr($name, $offset);
throw new UnexpectedValueException(
'Invalid QName "'. $name. '": Missing QName part.'
} elseif (preg_match('([^'. $nameChar. '-])u', $namePart, $match, PREG_OFFSET_CAPTURE)) {
//invalid bytes and whitespaces
$position = (int) $match[0][1];
throw new UnexpectedValueException(
'Invalid QName "'. $name. '": Invalid character at index '. ($offset + $position). '.'
} elseif (preg_match('(^[^'. $nameStartChar. '])u', $namePart)) {
//first char is a little more limited
throw new UnexpectedValueException(
'Invalid QName "'. $name. '": Invalid character at index '. $offset. '.'
* Check if the DOMNode is DOMElement or DOMText with content
* @param boolean $ignoreTextNodes
protected function _isNode($node, $ignoreTextNodes = FALSE) {
if ($node instanceof DOMElement) {
} elseif ($node instanceof DOMText) {
!$node->isWhitespaceInElementContent()) {
* Check if $elements is a iterateable node list
* @param DOMNodeList|DOMDocumentFragment|Iterator|IteratorAggregate|array$list
if ($elements instanceof DOMNodeList ||
$elements instanceof DOMDocumentFragment ||
$elements instanceof Iterator ||
$elements instanceof IteratorAggregate ||
* check if parameter is a valid callback function
* @param callback $callback
* @param boolean $allowGlobalFunctions
* @param boolean $silent (no InvalidArgumentException)
protected function _isCallback($callback, $allowGlobalFunctions, $silent) {
if ($callback instanceof Closure) {
throw new InvalidArgumentException('Invalid callback argument');
* Convert a given content xml string into and array of nodes
* @param boolean $includeTextNodes
$fragment = $this->_document->createDocumentFragment();
if ($fragment->appendXML($content)) {
for ($i = $fragment->childNodes->length - 1; $i >= 0; $i-- ) {
$element = $fragment->childNodes->item($i);
if ($element instanceof DOMElement ||
($includeTextNodes && $this->_isNode($element))) {
$element->parentNode->removeChild($element);
if ($limit > 0 && count($result) >= $limit) {
throw new UnexpectedValueException('Invalid document fragment');
* Convert a given content into and array of nodes
* @param string|array|DOMElement|DOMText|Iterator$content
* @param boolean $includeTextNodes
protected function _getContentNodes($content, $includeTextNodes = TRUE, $limit = 0) {
if ($content instanceof DOMElement) {
$result = array($content);
} elseif ($includeTextNodes && $this->_isNode($content)) {
$result = array($content);
foreach ($content as $element) {
if ($element instanceof DOMElement ||
($includeTextNodes && $this->_isNode($element))) {
if ($limit > 0 && count($result) >= $limit) {
throw new InvalidArgumentException('Invalid content parameter');
throw new UnexpectedValueException('No element found');
//if a node is not in the current document import it
foreach ($result as $index => $node) {
if ($node->ownerDocument !== $this->_document) {
$result[$index] = $this->_document->importNode($node, TRUE);
* Convert $content to a DOMElement. If $content contains several elements use the first.
* @param string|array|DOMElement|DOMNodeList|Iterator$content
if ($content instanceof DOMElement) {
* Get the target nodes from a given $selector.
* A string will be used as XPath expression.
* @param string|array|DOMNode|DOMNodeList|Iterator$selector
return $this->_match($selector);
throw new InvalidArgumentException('Invalid selector');
* the context is the target of a selector or the current selection
* @param string|array|DOMNode|DOMNodeList|Iterator $selector
* Get the inner xml of a given node or in other words the xml of all children.
* @param DOMElement $node
if ($node instanceof DOMElement) {
foreach ($node->childNodes as $childNode) {
$result .= $this->_document->saveXML($childNode);
} elseif ($node instanceof DOMText) {
return $node->textContent;
* Remove nodes from document tree
* @param string|array|DOMNode|DOMNodeList|Iterator$selector
* @return array $result removed nodes
foreach ($targetNodes as $node) {
if ($node instanceof DOMNode &&
isset ($node->parentNode)) {
$result[] = $node->parentNode->removeChild($node);
* Get the class/object providing the handler functions
return 'FluentDOMHandler';
* Use a handler callback to apply a content argument to each node $targetNodes. The content
* argument can be an easy setter function
* @param array|DOMNodeList$targetNodes
* @param string|array|DOMElement|DOMText|DOMNodeList|Iterator|callback|Closure$content
* @param callback|Closure$handler
$isEasySetterFunction = $this->_isCallback($content, FALSE, TRUE);
if (!$isEasySetterFunction) {
foreach ($targetNodes as $index => $node) {
if ($isEasySetterFunction) {
if (!empty($contentNodes)) {
* Execute the easy setter function for a node and return the new elements
* @param callback|Closure$easySetter
if (!empty($contentData)) {
|