blob: 5088aaccb6de83500c7ba33adca497a72722a3bd [file] [log] [blame]
<?php
/**
* File containing the ezcTree class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
* @version //autogentag//
* @filesource
* @package Tree
*/
/**
* ezcTree is an abstract class from which all the tree implementations
* inherit.
*
* Example:
* <code>
* <?php
* // Instantiating an existing tree, and creating a new tree is done through
* // the inherited classes
*
* // Creating a new in-memory tree
* $tree = ezcTreeMemory::create( new ezcTreeMemoryDataStore() );
*
* // Opening an existing tree in an XML file
* $tree = new ezcTreeXml( 'test.xml', new ezcTreeXmlInternalDataStore() );
*
* // Opening an existing tree from a database, using a nested set backend
* // - This retrieves data from the ezcTreeDbExternalTableDataStore store
* // using $this->dbh as database handle, $dataTable as table where to fetch
* // data from using 'id' as ID field.
* $store = new ezcTreeDbExternalTableDataStore( $this->dbh, $dataTable, 'id' );
* // - It uses the 'nested_set' table for keeping the tree structure
* $tree = new ezcTreeDbNestedSet( $this->dbh, 'nested_set', $store );
*
* // Fetching nodes and subtrees
* $node = $tree->fetchNodeById( 'He' );
* $nodeList = $tree->fetchSubtree( 'Pantherinae' );
*
* // Checking for relations between nodes
* $tree->isDescendantOf( 'Tiger', 'Panthera' );
* $tree->isSiblingOf( 'Lion', 'Leopard' );
* ?>
* </code>
*
* @property-read ezcTreeXmlDataStore $store
* The data store that is used for retrieving/storing data.
* @property string $nodeClassName
* Which class is used as tree node - this class *must* inherit
* the ezcTreeNode class.
* @property boolean $autoId
* When set to true, you can add nodes to the database without
* setting the ID. This only works with numeric keys however.
*
* @package Tree
* @version //autogentag//
*/
abstract class ezcTree implements ezcTreeVisitable
{
/**
* Holds the properties of this class.
*
* @var array(string=>mixed)
*/
protected $properties = array( 'nodeClassName' => 'ezcTreeNode' );
/**
* Contains whether a transaction has been started.
*
* @var bool
*/
protected $inTransaction = false;
/**
* Contains whether we are in a transaction commit stage.
*
* @var bool
*/
protected $inTransactionCommit = false;
/**
* Contains a list of transaction items.
*
* @var array(ezcTreeTransactionItem)
*/
private $transactionList = array();
/**
* Returns the value of the property $name.
*
* @throws ezcBasePropertyNotFoundException if the property does not exist.
* @param string $name
* @ignore
*/
public function __get( $name )
{
switch ( $name )
{
case 'autoId':
case 'store':
case 'nodeClassName':
return $this->properties[$name];
}
throw new ezcBasePropertyNotFoundException( $name );
}
/**
* Sets the property $name to $value.
*
* @throws ezcBasePropertyNotFoundException if the property does not exist.
* @throws ezcBasePropertyPermissionException if a read-only property is
* tried to be modified.
* @throws ezcBaseValueException if trying to assign a wrong value to the
* property
* @throws ezcBaseInvalidParentClassException
* if the class name passed as replacement for the ezcTreeNode
* classs does not inherit from the ezcTreeNode class.
* @param string $name
* @param mixed $value
* @ignore
*/
public function __set( $name, $value )
{
switch ( $name )
{
case 'store':
throw new ezcBasePropertyPermissionException( $name, ezcBasePropertyPermissionException::READ );
case 'autoId':
if ( !is_bool( $value ) )
{
throw new ezcBaseValueException( $name, $value, 'boolean' );
}
$this->properties[$name] = $value;
break;
case 'nodeClassName':
if ( !is_string( $value ) )
{
throw new ezcBaseValueException( $name, $value, 'string that contains a class name' );
}
// Check if the passed classname actually implements the
// correct parent class.
if ( 'ezcTreeNode' !== $value &&
!in_array( 'ezcTreeNode', class_parents( $value ) ) )
{
throw new ezcBaseInvalidParentClassException( 'ezcTreeNode', $value );
}
$this->properties[$name] = $value;
break;
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Returns true if the property $name is set, otherwise false.
*
* @param string $name
* @return bool
* @ignore
*/
public function __isset( $name )
{
switch ( $name )
{
case 'autoId':
case 'store':
case 'nodeClassName':
return isset( $this->properties[$name] );
default:
return false;
}
}
/**
* This method checks whether a node ID is valid to be used in a backend.
*
* @throws ezcTreeInvalidNodeIDException if the node is not valid.
*
* @param string $nodeId
*/
protected function checkNodeId( $nodeId )
{
/* The default implementation does not check anything */
}
/**
* This method generates the next node ID.
*
* @return integer
*/
abstract protected function generateNodeID();
/**
* Creates a new tree node with node ID $nodeId and $data.
*
* This method returns by default an object of the ezcTreeNode class,
* however if a replacement is configured through the nodeClassName property
* an object of that class is returned instead. This object is guaranteed
* to inherit from ezcTreeNode.
*
* @param string $nodeId
* @param mixed $data
* @return ezcTreeNode
*/
public function createNode( $nodeId, $data )
{
if ( $nodeId === null )
{
if ( $this->properties['autoId'] )
{
$nodeId = $this->generateNodeID();
}
else
{
throw new ezcTreeInvalidIdException( null, '' );
}
}
$this->checkNodeID( $nodeId );
$className = $this->properties['nodeClassName'];
return new $className( $this, $nodeId, $data );
}
/**
* Implements the accept method for visiting.
*
* @param ezcTreeVisitor $visitor
*/
public function accept( ezcTreeVisitor $visitor )
{
$visitor->visit( $this );
$root = $this->getRootNode();
if ( $root instanceof ezcTreeNode )
{
$root->accept( $visitor );
}
}
/**
* Returns whether the node with ID $nodeId exists.
*
* @param string $nodeId
* @return bool
*/
abstract public function nodeExists( $nodeId );
/**
* Returns the node identified by the ID $nodeId.
*
* @param string $nodeId
* @throws ezcTreeUnknownIdException if there is no node with ID $nodeId
* @return ezcTreeNode
*/
public function fetchNodeById( $nodeId )
{
if ( !$this->nodeExists( $nodeId ) )
{
throw new ezcTreeUnknownIdException( $nodeId );
}
$className = $this->properties['nodeClassName'];
$node = new $className( $this, $nodeId );
return $node;
}
/**
* Returns all the children of the node with ID $nodeId.
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
abstract public function fetchChildren( $nodeId );
/**
* Returns the parent node of the node with ID $nodeId.
*
* @param string $nodeId
* @return ezcTreeNode
*/
abstract public function fetchParent( $nodeId );
/**
* Returns all the nodes in the path from the root node to the node with ID
* $nodeId, including those two nodes.
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
abstract public function fetchPath( $nodeId );
/**
* Alias for fetchSubtreeDepthFirst().
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
abstract public function fetchSubtree( $nodeId );
/**
* Returns the node with ID $nodeId and all its children, sorted according to
* the {@link http://en.wikipedia.org/wiki/Breadth-first_search Breadth-first sorting}
* algorithm.
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
abstract public function fetchSubtreeBreadthFirst( $nodeId );
/**
* Returns the node with ID $nodeId and all its children, sorted according to
* the {@link http://en.wikipedia.org/wiki/Depth-first_search Depth-first sorting}
* algorithm.
*
* @param string $nodeId
* @return ezcTreeNodeList
*/
abstract public function fetchSubtreeDepthFirst( $nodeId );
/**
* Returns the number of direct children of the node with ID $nodeId.
*
* @param string $nodeId
* @return int
*/
abstract public function getChildCount( $nodeId );
/**
* Returns the number of children of the node with ID $nodeId, recursively.
*
* @param string $nodeId
* @return int
*/
abstract public function getChildCountRecursive( $nodeId );
/**
* Returns the distance from the root node to the node with ID $nodeId.
*
* @param string $nodeId
* @return int
*/
abstract public function getPathLength( $nodeId );
/**
* Returns whether the node with ID $nodeId has children.
*
* @param string $nodeId
* @return bool
*/
abstract public function hasChildNodes( $nodeId );
/**
* Returns whether the node with ID $childId is a direct child of the node
* with ID $parentId.
*
* @param string $childId
* @param string $parentId
* @return bool
*/
abstract public function isChildOf( $childId, $parentId );
/**
* Returns whether the node with ID $childId is a direct or indirect child
* of the node with ID $parentId.
*
* @param string $childId
* @param string $parentId
* @return bool
*/
abstract public function isDescendantOf( $childId, $parentId );
/**
* Returns whether the nodes with IDs $child1Id and $child2Id are siblings
* (ie, they share the same parent).
*
* @param string $child1Id
* @param string $child2Id
* @return bool
*/
abstract public function isSiblingOf( $child1Id, $child2Id );
/**
* Sets a new node as root node, this also wipes out the whole tree.
*
* @param ezcTreeNode $node
*/
abstract public function setRootNode( ezcTreeNode $node );
/**
* Returns the root node.
*
* @return ezcTreeNode
*/
abstract public function getRootNode();
/**
* Adds the node $childNode as child of the node with ID $parentId.
*
* @param string $parentId
* @param ezcTreeNode $childNode
*/
abstract public function addChild( $parentId, ezcTreeNode $childNode );
/**
* Deletes the node with ID $nodeId from the tree, including all its children.
*
* @param string $nodeId
*/
abstract public function delete( $nodeId );
/**
* Moves the node with ID $nodeId as child to the node with ID $targetParentId.
*
* @param string $nodeId
* @param string $targetParentId
*/
abstract public function move( $nodeId, $targetParentId );
/**
* Copies all the children of node $fromNode to node $toNode recursively.
*
* This method copies all children recursively from $fromNode to $toNode.
* The $fromNode belongs to the $from tree and the $toNode to the $to tree.
* Data associated with the nodes is copied as well from the store
* associated with the $from tree to the $to tree.
*
* @param ezcTree $from
* @param ezcTree $to
* @param ezcTreeNode $fromNode
* @param ezcTreeNode $toNode
*/
private static function copyChildren( ezcTree $from, ezcTree $to, ezcTreeNode $fromNode, ezcTreeNode $toNode )
{
$children = $fromNode->fetchChildren();
foreach ( new ezcTreeNodeListIterator( $from, $children, true ) as $childNodeKey => $childNodeData )
{
$fromChildNode = $from->fetchNodeById( $childNodeKey );
$toChildNode = new ezcTreeNode( $to, $childNodeKey, $childNodeData );
$toNode->addChild( $toChildNode );
self::copyChildren( $from, $to, $fromChildNode, $toChildNode );
}
}
/**
* Copies the tree in $from to the empty tree in $to.
*
* This method copies all the nodes, including associated data from the
* used data store, from the tree $from to the tree $to. Because this
* function uses internally setRootNode() the target tree will be cleared
* out automatically. The method will not check whether the $from and $to
* trees share the same database table or data store, so make sure they are
* different to prevent unexpected behavior.
*
* @param ezcTree $from
* @param ezcTree $to
*/
public static function copy( ezcTree $from, ezcTree $to )
{
$fromRootNode = $from->getRootNode();
$to->setRootNode( new ezcTreeNode( $to, $fromRootNode->id, $fromRootNode->data ) );
$toRootNode = $to->getRootNode();
self::copyChildren( $from, $to, $fromRootNode, $toRootNode );
}
/**
* Returns whether we are currently in a transaction or not
*
* @return bool
*/
public function inTransaction()
{
return $this->inTransaction;
}
/**
* Returns whether we are currently in a transaction commit state or not
*
* @return bool
*/
public function inTransactionCommit()
{
return $this->inTransactionCommit;
}
/**
* Starts an transaction in which all tree modifications are queued until
* the transaction is committed with the commit() method.
*/
public function beginTransaction()
{
if ( $this->inTransaction )
{
throw new ezcTreeTransactionAlreadyStartedException;
}
$this->inTransaction = true;
$this->transactionList = array();
}
/**
* Commits the transaction by running the stored instructions to modify
* the tree structure.
*/
public function commit()
{
if ( !$this->inTransaction )
{
throw new ezcTreeTransactionNotStartedException;
}
$this->inTransaction = false;
$this->inTransactionCommit = true;
foreach ( $this->transactionList as $transactionItem )
{
switch ( $transactionItem->type )
{
case ezcTreeTransactionItem::ADD:
$this->addChild( $transactionItem->parentId, $transactionItem->node );
break;
case ezcTreeTransactionItem::DELETE:
$this->delete( $transactionItem->nodeId );
break;
case ezcTreeTransactionItem::MOVE:
$this->move( $transactionItem->nodeId, $transactionItem->parentId );
break;
}
}
$this->inTransactionCommit = false;
$this->fixateTransaction();
}
/**
* Adds a new transaction item to the list.
*
* @param ezcTreeTransactionItem $item
*/
protected function addTransactionItem( ezcTreeTransactionItem $item )
{
$this->transactionList[] = $item;
}
/**
* Rolls back the transaction by clearing all stored instructions.
*/
public function rollback()
{
if ( !$this->inTransaction )
{
throw new ezcTreeTransactionNotStartedException;
}
$this->inTransaction = false;
$this->transactionList = array();
}
}
?>