<?php
// +-----------------------------------------------------------------------+
// | Copyright (c) 2002-2003, Richard Heyes |
// | All rights reserved. |
// | |
// | Redistribution and use in source and binary forms, with or without |
// | modification, are permitted provided that the following conditions |
// | are met: |
// | |
// | o Redistributions of source code must retain the above copyright |
// | notice, this list of conditions and the following disclaimer. |
// | o Redistributions in binary form must reproduce the above copyright |
// | notice, this list of conditions and the following disclaimer in the |
// | documentation and/or other materials provided with the distribution.|
// | o The names of the authors may not be used to endorse or promote |
// | products derived from this software without specific prior written |
// | permission. |
// | |
// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
// | |
// +-----------------------------------------------------------------------+
// | Author: Richard Heyes <http://www.phpguru.org> |
// +-----------------------------------------------------------------------+
/**
* A class to represent a collection of child nodes
*
* Public methods for Tree_NodeCollection class:
* &add(Tree_Node node) Adds a node to the collection
* &firstNode() Retreives a reference to the first node in the collection
* &lastNode() Retreives a reference to the last node in the collection
* &removeNodeAt(int index) Removes the node at the specified index (nodes are re-ordered)
* removeNode(Tree_Node node [, boolean search]) Removes the given node (nodes are re-ordered)
* indexOf(Tree_Node node) Retreives the index of the given node
* getNodeCount([boolean recurse]) Retreives the number of nodes in the collection, optionally recursing
* getFlatList() Retrieves an indexed array of the nodes from top to bottom, left to right
* traverse(callback function) Traverses the tree supply each node to the callback function
* search(mixed searchData [, bool strict]) Basic search function for searching the Trees' "tag" data
* moveTo() Moves the nodes in this collection to the given node/tree
* copyTo() Copies the nodes in this collection to the given node/tree
* usortNodes(callback comparer) Sorts the nodes in the collection based on a user defined comparison function.
*/
class Tree_NodeCollection implements ArrayAccess, IteratorAggregate
{
/**
* An array of child nodes
* @var array
*/
public $nodes;
/**
* The containing node/tree object
* @var object
*/
private $container;
/**
* Constructor
*/
function __construct($container)
{
$this->nodes = array();
$this->container = $container;
}
/**
* Sorts the nodes in the collection based on a user defined comparison function.
* The function is deliberately not recursive so that you can sort
* any of the node collections within the tree in different ways.
*
* To sort the entire tree uniformly, call the Tree function of the same name.
* eg:
* $myTree->usortNodes('cmp');
*
* where 'cmp' is the comparison function.
*
* Not part of the original package, added by Bruce Anwyl on Jan 04, 2008.
*
* @param callback $comparer Name of user defined comparison function
* @return bool Result of usort
*/
public function usortNodes($comparer)
{
return usort($this->nodes, $comparer);
}
/**
* Implementation of IteratorAggregate::getIterator()
*
* @return object Iterator object for looping over this collections
* immediate nodes.
*/
public function getIterator()
{
return new ArrayIterator($this->nodes);
}
/**
* Implementation of ArrayAccess:offsetSet()
*
* @param mixed $key Key to set value for
* @param mixed $value Value to set
*/
public function offsetSet($key, $value)
{
$this->nodes[$key] = $value;
}
/**
* Implementation of ArrayAccess:offsetGet()
*
* @param mixed $key Key to retrieve value of
* @return mixed Value of given key
*/
public function offsetGet($key)
{
return $this->nodes[$key];
}
/**
* Implementation of ArrayAccess:offsetUnset()
*
* @param mixed $key Key to unset
*/
public function offsetUnset($key)
{
unset($this->nodes[$key]);
}
/**
* Implementation of ArrayAccess:offsetExists()
*
* @param mixed $key Key to check for
* @return bool Whether it's set or not
*/
public function offsetExists($key)
{
return isset($this->nodes[$key]);
}
/**
* Adds a node to this node
*
* @param object $node The Tree_Node object
* @return object A reference to the new node inside the tree
*/
public function add(Tree_Node $node)
{
$node->setParent($this->container);
// Set the Tree for the node
if ($this->container->getTree() instanceof Tree) {
$node->setTree($this->container->getTree());
}
$this->nodes[] = $node;
return $node;
}
/**
* Returns the first node in this particular collection
*
* @return object The first node. NULL if no nodes.
*/
public function firstNode()
{
return !empty($this->nodes) ? $this->nodes[0] : null;
}
/**
* Returns the last node in this particular collection
*
* @return object The last node. NULL if no nodes.
*/
public function lastNode()
{
return !empty($this->nodes) ? $this->nodes[count($this->nodes) - 1] : null;
}
/**
* Removes a node from the child nodes array at the
* specified (zero based) index.
*
* @param integer $index The index to remove
* @return object The node that was removed, or null
* if this index did not exist
*/
public function removeNodeAt($index)
{
$node = null;
if (!empty($this->nodes[$index])) {
$node = $this->nodes[$index]->remove();
$this->nodes = array_values($this->nodes);
}
return $node;
}
/**
* Removes a node from the child nodes array by using
* reference comparison.
*
* @param Tree_Node $node The node to remove
* @param bool $search Whether to search child nodes
* @return bool True/False
*/
public function remove(Tree_Node $node, $search = false)
{
foreach ($this->nodes as $index => $_node) {
if ($_node === $node) {
// Unset parent, tree
$node->setParent(null);
$node->setTree(null);
unset($this->nodes[$index]);
$this->nodes = array_values($this->nodes);
return true;
} else if ($search AND $_node->hasChildren() ) {
$searchNodes[] = $_node;
}
}
// Go thru searching those nodes that have children
if (!empty($searchNodes)) {
foreach ($searchNodes as $_node) {
if ($_node->nodes->remove($node, true)) {
return true;
}
}
}
return false;
}
/**
* Returns the index in the nodes array at which
* the given node resides. Used in the prev/next Sibling
* methods.
*
* @param object $node The node to return the index of
* @return integer The index of the node or null if
* not found.
*/
public function indexOf($node)
{
foreach ($this->nodes as $index => $_node) {
if ($node === $_node) {
return $index;
}
}
return null;
}
/**
* Returns node at given index. Note that this class also implements the
* ArrayAccess interface so you could do this:
* $tree->nodes[3] instead of $tree->nodes->indexOf(3)
*
* @param integer $index Index of node to retrieve
* @return Tree_Node Tree_Node object or null if it doesn't exist
*/
function nodeAt($index)
{
return isset($this->nodes[$index]) ? $this->nodes[$index] : null;
}
/**
* Returns the number of child nodes in this node/tree.
* Optionally searches the tree and returns the cumulative count.
* Works also as an implementation of the Countable::count() interface (PHP5.1+)
*
*
* @param bool $search Search tree for nodecount too
* @return integer The number of nodes found
*/
public function count($search = false)
{
if ($search) {
$count = count($this->nodes);
foreach ($this->nodes as $node) {
$count += $node->nodes->count(true);
}
return $count;
} else {
return count($this->nodes);
}
}
/**
* Returns a flat list of the node collection. This array contains references
* to the nodes.
*
* @return array Flat list of the nodes from top to bottom, left to right.
*/
public function getFlatList()
{
$return = array();
foreach ($this->nodes as $node) {
$return[] = $node;
// Recurse
if ($node->hasChildren()) {
$return = array_merge($return, $node->nodes->getFlatList());
}
}
return $return;
}
/**
* Traverses the node collection applying a function to each and every node.
* The function name given (though this can be anything you can supply to
* call_user_func(), not just a name) should take two arguments which are the
* node object (Tree_Node class) and any extra data you pass via the $data argument
* to traverse(). You can then access the nodes data by using
* the getTag() method. The traversal goes from top to bottom, left to right
* (ie same order as what you get from getFlatList()).
*
* ** The node is passed by reference to the function! **
*
* @param callback $function The callback function to use
* @param array $data Any data to pass on to the callback function.
* Probably most useful as an array of "stuff".
*/
public function traverse($function, $data = null)
{
foreach ($this->nodes as $node) {
call_user_func($function, $node, $data);
// Recurse
if ($node->hasChildren()) {
$node->nodes->traverse($function, $data);
}
}
}
/**
* Searches the node collection for a node with a tag matching
* what you supply. This is a simple "tag == your data" comparison, (=== if strict option is applied)
* and more advanced comparisons can be made using the traverse() method.
* This function returns an array of matching Tree_Node objects.
*
* @param mixed $data Data to try to find and match
* @param bool $strict Whether to use === or simply == to compare
* @return array Array of resulting nodes matched.
*/
public function search($data, $strict = false)
{
$results = array();
$nodeList = $this->getFlatList();
foreach ($nodeList as $node) {
$comparison = $strict ? ($node->getTag() === $data) : ($node->getTag() == $data);
if ($comparison) {
$results[] = $node;
}
}
return $results;
}
/**
* Moves the nodes in this collection (not the collection itself)
* to the given new parent.
*
* @param object $newParent The new parent Tree_Node or Tree object
*/
public function moveTo($newParent)
{
foreach ($this->nodes as $node) {
$node->moveTo($newParent);
}
}
/**
* Copies the nodes in this collection (not the collection itself)
* to the given new parent.
*
* @param object $newParent The new parent Tree_Node or Tree object
*/
public function copyTo($newParent)
{
foreach ($this->nodes as $node) {
$node->copyTo($newParent);
}
}
/**
* Sets the master Tree object for Nodes in this
* collection.
*
* @param Tree $tree The Tree object to set
*/
public function setTree($tree)
{
foreach ($this->nodes as $node) {
$node->setTree($tree);
}
}
/**
* Returns the Tree object
*
* @return Tree The Tree object containing this NodeCollection
*/
function getTree()
{
return $this->container->getTree();
}
}
?>