Commit 7ad2c77a authored by romanb's avatar romanb

- Updated nested set documentation

- Removed the reserved alias "base" from the nested set. It's no longer necessary to use that. Use whatever alias you want.
parent 121d9ad9
......@@ -79,8 +79,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
*/
public function getPrevSibling()
{
$baseAlias = $this->_tree->getBaseAlias();
$q = $this->_tree->getBaseQuery();
$q = $q->addWhere('base.rgt = ?', $this->getLeftValue() - 1);
$q = $q->addWhere("$baseAlias.rgt = ?", $this->getLeftValue() - 1);
$q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
$result = $q->execute();
......@@ -104,8 +105,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
*/
public function getNextSibling()
{
$baseAlias = $this->_tree->getBaseAlias();
$q = $this->_tree->getBaseQuery();
$q = $q->addWhere('base.lft = ?', $this->getRightValue() + 1);
$q = $q->addWhere("$baseAlias.lft = ?", $this->getRightValue() + 1);
$q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
$result = $q->execute();
......@@ -149,8 +151,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
*/
public function getFirstChild()
{
$baseAlias = $this->_tree->getBaseAlias();
$q = $this->_tree->getBaseQuery();
$q->addWhere('base.lft = ?', $this->getLeftValue() + 1);
$q->addWhere("$baseAlias.lft = ?", $this->getLeftValue() + 1);
$this->_tree->returnQueryWithRootId($q, $this->getRootValue());
$result = $q->execute();
......@@ -174,8 +177,9 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
*/
public function getLastChild()
{
$baseAlias = $this->_tree->getBaseAlias();
$q = $this->_tree->getBaseQuery();
$q->addWhere('base.rgt = ?', $this->getRightValue() - 1);
$q->addWhere("$baseAlias.rgt = ?", $this->getRightValue() - 1);
$this->_tree->returnQueryWithRootId($q, $this->getRootValue());
$result = $q->execute();
......@@ -211,17 +215,18 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
*/
public function getDescendants($depth = null, $includeNode = false)
{
$baseAlias = $this->_tree->getBaseAlias();
$q = $this->_tree->getBaseQuery();
$params = array($this->record->get('lft'), $this->record->get('rgt'));
if ($includeNode) {
$q->addWhere("base.lft >= ? AND base.rgt <= ?", $params)->addOrderBy("base.lft asc");
$q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", $params)->addOrderBy("$baseAlias.lft asc");
} else {
$q->addWhere("base.lft > ? AND base.rgt < ?", $params)->addOrderBy("base.lft asc");
$q->addWhere("$baseAlias.lft > ? AND $baseAlias.rgt < ?", $params)->addOrderBy("$baseAlias.lft asc");
}
if ($depth !== null) {
$q->addWhere("base.level <= ?", $this->record['level'] + $depth);
$q->addWhere("$baseAlias.level <= ?", $this->record['level'] + $depth);
}
$q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
......@@ -241,9 +246,10 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
*/
public function getParent()
{
$baseAlias = $this->_tree->getBaseAlias();
$q = $this->_tree->getBaseQuery();
$q->addWhere("base.lft < ? AND base.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
->addOrderBy("base.rgt asc");
$q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
->addOrderBy("$baseAlias.rgt asc");
$q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
$result = $q->execute();
......@@ -269,11 +275,12 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
*/
public function getAncestors($depth = null)
{
$baseAlias = $this->_tree->getBaseAlias();
$q = $this->_tree->getBaseQuery();
$q->addWhere("base.lft < ? AND base.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
->addOrderBy("base.lft asc");
$q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()))
->addOrderBy("$baseAlias.lft asc");
if ($depth !== null) {
$q->addWhere("base.level >= ?", $this->record['level'] - $depth);
$q->addWhere("$baseAlias.level >= ?", $this->record['level'] - $depth);
}
$q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
$ancestors = $q->execute();
......@@ -765,9 +772,10 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
$oldRoot = $this->getRootValue();
$q = $this->_tree->getBaseQuery();
$baseAlias = $this->_tree->getBaseAlias();
$componentName = $this->_tree->getBaseComponent();
$q = $q->addWhere('base.lft >= ? AND base.rgt <= ?', array($this->getLeftValue(), $this->getRightValue()));
$q = $q->addWhere("$baseAlias.lft >= ? AND $baseAlias.rgt <= ?", array($this->getLeftValue(), $this->getRightValue()));
$q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $oldRoot);
......@@ -950,9 +958,10 @@ class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Int
public function getLevel()
{
if (!isset($this->record['level'])) {
$baseAlias = $this->_tree->getBaseAlias();
$componentName = $this->_tree->getBaseComponent();
$q = $this->_tree->getBaseQuery();
$q = $q->addWhere('base.lft < ? AND base.rgt > ?', array($this->getLeftValue(), $this->getRightValue()));
$q = $q->addWhere("$baseAlias.lft < ? AND $baseAlias.rgt > ?", array($this->getLeftValue(), $this->getRightValue()));
$q = $this->_tree->returnQueryWithRootId($q, $this->getRootValue());
......
<?php
/*
* $Id$
*
* 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.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.phpdoctrine.com>.
*/
/**
* Doctrine_Template_NestedSet
*
* @package Doctrine
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @category Object Relational Mapping
* @link www.phpdoctrine.com
* @since 1.0
* @version $Revision$
* @author Roman Borschel <roman@code-factory.org>
*/
class Doctrine_Template_NestedSet extends Doctrine_Template
{
public function __construct(array $options)
{}
public function setUp()
{
$this->_table->setOption('treeImpl', 'NestedSet');
}
}
......@@ -32,6 +32,7 @@
class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Interface
{
private $_baseQuery;
private $_baseAlias = "base";
/**
* constructor, creates tree with reference to table and sets default root options
......@@ -109,7 +110,7 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int
public function fetchRoot($rootId = 1)
{
$q = $this->getBaseQuery();
$q = $q->addWhere('base.lft = ?', 1);
$q = $q->addWhere($this->_baseAlias . '.lft = ?', 1);
// if tree has many roots, then specify root id
$q = $this->returnQueryWithRootId($q, $rootId);
......@@ -143,14 +144,15 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int
// fetch tree
$q = $this->getBaseQuery();
$q = $q->addWhere("base.lft >= ?", 1);
$q = $q->addWhere($this->_baseAlias . ".lft >= ?", 1);
// if tree has many roots, then specify root id
$rootId = isset($options['root_id']) ? $options['root_id'] : '1';
if (is_array($rootId)) {
$q->addOrderBy("base." . $this->getAttribute('rootColumnName') . ", base.lft ASC");
$q->addOrderBy($this->_baseAlias . "." . $this->getAttribute('rootColumnName') .
", " . $this->_baseAlias . ".lft ASC");
} else {
$q->addOrderBy("base.lft ASC");
$q->addOrderBy($this->_baseAlias . ".lft ASC");
}
$q = $this->returnQueryWithRootId($q, $rootId);
......@@ -182,7 +184,8 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int
$q = $this->getBaseQuery();
$params = array($record->get('lft'), $record->get('rgt'));
$q->addWhere("base.lft >= ? AND base.rgt <= ?", $params)->addOrderBy("base.lft asc");
$q->addWhere($this->_baseAlias . ".lft >= ? AND " . $this->_baseAlias . ".rgt <= ?", $params)
->addOrderBy($this->_baseAlias . ".lft asc");
$q = $this->returnQueryWithRootId($q, $record->getNode()->getRootValue());
return $q->execute();
}
......@@ -196,7 +199,7 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int
public function fetchRoots()
{
$q = $this->getBaseQuery();
$q = $q->addWhere('base.lft = ?', 1);
$q = $q->addWhere($this->_baseAlias . '.lft = ?', 1);
return $q->execute();
}
......@@ -269,14 +272,24 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int
return clone $this->_baseQuery;
}
/**
* Enter description here...
*
*/
public function getBaseAlias()
{
return $this->_baseAlias;
}
/**
* Enter description here...
*
*/
private function _createBaseQuery()
{
$this->_baseAlias = "base";
$q = new Doctrine_Query();
$q->select("base.*")->from($this->getBaseComponent() . " base");
$q->select($this->_baseAlias . ".*")->from($this->getBaseComponent() . " " . $this->_baseAlias);
return $q;
}
......@@ -287,9 +300,10 @@ class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Int
*/
public function setBaseQuery(Doctrine_Query $query)
{
$query->addSelect("base.lft, base.rgt, base.level");
$this->_baseAlias = $query->getRootAlias();
$query->addSelect($this->_baseAlias . ".lft, " . $this->_baseAlias . ".rgt, ". $this->_baseAlias . ".level");
if ($this->getAttribute('rootColumnName')) {
$query->addSelect("base." . $this->getAttribute('rootColumnName'));
$query->addSelect($this->_baseAlias . "." . $this->getAttribute('rootColumnName'));
}
$this->_baseQuery = $query;
}
......
......@@ -2,4 +2,3 @@
+++ Adjacency list
+++ Nested set
+++ Materialized path
\ No newline at end of file
+++ Examples
This is an example to show how you would set up and use the doctrine tree interface with the {{NestedSet}} implementation (currently the most comprehensively supported by Doctrine)
<code type="php">
require_once("path/to/Doctrine.php");
function __autoload($classname) {
return Doctrine::autoload($classname);
}
// define our tree
class Menu extends Doctrine_Record {
public function setTableDefinition() {
$this->setTableName('menu');
// add this your table definition to set the table as NestedSet tree implementation
$this->actsAsTree('NestedSet');
// you do not need to add any columns specific to the nested set implementation
// these are added for you
$this->hasColumn("name","string",30);
}
// this __toString() function is used to get the name for the path, see node::getPath
public function __toString() {
return $this->get('name');
}
}
// set connections to database
$dsn = 'mysql:dbname=nestedset;host=localhost';
$user = 'user';
$password = 'pass';
try {
$dbh = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
}
$manager = Doctrine_Manager::getInstance();
$conn = $manager->openConnection($dbh);
// create root
$root = new Menu();
$root->set('name', 'root');
$manager->getTable('Menu')->getTree()->createRoot($root);
// build tree
$two = new Menu();
$two->set('name', '2');
$root->getNode()->addChild($two);
$one = new Menu();
$one->set('name', '1');
$one->getNode()->insertAsPrevSiblingOf($two);
// refresh as node's lft and rgt values have changed
$two->refresh();
$three = new Menu();
$three->set('name', '3');
$three->getNode()->insertAsNextSiblingOf($two);
$two->refresh();
$one_one = new Menu();
$one_one->set('name', '1.1');
$one_one->getNode()->insertAsFirstChildOf($one);
$one->refresh();
$one_two = new Menu();
$one_two->set('name', '1.2');
$one_two->getNode()->insertAsLastChildOf($one);
$one_two->refresh();
$one_two_one = new Menu();
$one_two_one->set('name', '1.2.1');
$one_two->getNode()->addChild($one_two_one);
$root->refresh();
$four = new Menu();
$four->set('name', '4');
$root->getNode()->addChild($four);
$root->refresh();
$five = new Menu();
$five->set('name', '5');
$root->getNode()->addChild($five);
$root->refresh();
$six = new Menu();
$six->set('name', '6');
$root->getNode()->addChild($six);
output_message('initial tree');
output_tree($root);
$one_one->refresh();
$six->set('name', '1.0 (was 6)');
$six->getNode()->moveAsPrevSiblingOf($one_one);
$one_two->refresh();
$five->refresh();
$five->set('name', '1.3 (was 5)');
$five->getNode()->moveAsNextSiblingOf($one_two);
$one_one->refresh();
$four->refresh();
$four->set('name', '1.1.1 (was 4)');
$four->getNode()->moveAsFirstChildOf($one_one);
$root->refresh();
$one_two_one->refresh();
$one_two_one->set('name', 'last (was 1.2.1)');
$one_two_one->getNode()->moveAsLastChildOf($root);
output_message('transformed tree');
output_tree($root);
$one_one->refresh();
$one_one->deleteNode();
output_message('delete 1.1');
output_tree($root);
// now test fetching root
$tree_root = $manager->getTable('Menu')->getTree()->findRoot();
output_message('testing fetch root and outputting tree from the root node');
output_tree($tree_root);
// now test fetching the tree
output_message('testing fetching entire tree using tree::fetchTree()');
$tree = $manager->getTable('Menu')->getTree()->fetchTree();
while($node = $tree->next())
{
output_node($node);
}
// now test fetching the tree
output_message('testing fetching entire tree using tree::fetchTree(), excluding root node');
$tree = $manager->getTable('Menu')->getTree()->fetchTree(array('include_record' => false));
while($node = $tree->next())
{
output_node($node);
}
// now test fetching the branch
output_message('testing fetching branch for 1, using tree::fetchBranch()');
$one->refresh();
$branch = $manager->getTable('Menu')->getTree()->fetchBranch($one->get('id'));
while($node = $branch->next())
{
output_node($node);
}
// now test fetching the tree
output_message('testing fetching branch for 1, using tree::fetchBranch() excluding node 1');
$tree = $manager->getTable('Menu')->getTree()->fetchBranch($one->get('id'), array('include_record' => false));
while($node = $tree->next())
{
output_node($node);
}
// now perform some tests
output_message('descendants for 1');
$descendants = $one->getNode()->getDescendants();
while($descendant = $descendants->next())
{
output_node($descendant);
}
// move one and children under two
$two->refresh();
$one->getNode()->moveAsFirstChildOf($two);
output_message('moved one as first child of 2');
output_tree($root);
output_message('descendants for 2');
$two->refresh();
$descendants = $two->getNode()->getDescendants();
while($descendant = $descendants->next())
{
output_node($descendant);
}
output_message('number descendants for 2');
echo $two->getNode()->getNumberDescendants() .'</br>';
output_message('children for 2 (notice excludes children of children, known as descendants)');
$children = $two->getNode()->getChildren();
while($child = $children->next())
{
output_node($child);
}
output_message('number children for 2');
echo $two->getNode()->getNumberChildren() .'</br>';
output_message('path to 1');
$path = $one->getNode()->getPath(' > ');
echo $path .'
';
output_message('path to 1 (including 1)');
$path = $one->getNode()->getPath(' > ', true);
echo $path .'
';
output_message('1 has parent');
$hasParent = $one->getNode()->hasParent();
$msg = $hasParent ? 'true' : 'false';
echo $msg . '</br/>';
output_message('parent to 1');
$parent = $one->getNode()->getParent();
if($parent->exists())
{
echo $parent->get('name') .'
';
}
output_message('root isRoot?');
$isRoot = $root->getNode()->isRoot();
$msg = $isRoot ? 'true' : 'false';
echo $msg . '</br/>';
output_message('one isRoot?');
$isRoot = $one->getNode()->isRoot();
$msg = $isRoot ? 'true' : 'false';
echo $msg . '</br/>';
output_message('root hasParent');
$hasParent = $root->getNode()->hasParent();
$msg = $hasParent ? 'true' : 'false';
echo $msg . '</br/>';
output_message('root getParent');
$parent = $root->getNode()->getParent();
if($parent->exists())
{
echo $parent->get('name') .'
';
}
output_message('get first child of root');
$record = $root->getNode()->getFirstChild();
if($record->exists())
{
echo $record->get('name') .'
';
}
output_message('get last child of root');
$record = $root->getNode()->getLastChild();
if($record->exists())
{
echo $record->get('name') .'
';
}
$one_two->refresh();
output_message('get prev sibling of 1.2');
$record = $one_two->getNode()->getPrevSibling();
if($record->exists())
{
echo $record->get('name') .'
';
}
output_message('get next sibling of 1.2');
$record = $one_two->getNode()->getNextSibling();
if($record->exists())
{
echo $record->get('name') .'
';
}
output_message('siblings of 1.2');
$siblings = $one_two->getNode()->getSiblings();
foreach($siblings as $sibling)
{
if($sibling->exists())
echo $sibling->get('name') .'
';
}
output_message('siblings of 1.2 (including 1.2)');
$siblings = $one_two->getNode()->getSiblings(true);
foreach($siblings as $sibling)
{
if($sibling->exists())
echo $sibling->get('name') .'
';
}
$new = new Menu();
$new->set('name', 'parent of 1.2');
$new->getNode()->insertAsParentOf($one_two);
output_message('added a parent to 1.2');
output_tree($root);
try {
$dummy = new Menu();
$dummy->set('name', 'dummy');
$dummy->save();
}
catch (Doctrine_Exception $e)
{
output_message('You cannot save a node unless it is in the tree');
}
try {
$fake = new Menu();
$fake->set('name', 'dummy');
$fake->set('lft', 200);
$fake->set('rgt', 1);
$fake->save();
}
catch (Doctrine_Exception $e)
{
output_message('You cannot save a node with bad lft and rgt values');
}
// check last remaining tests
output_message('New parent is descendant of 1');
$one->refresh();
$res = $new->getNode()->isDescendantOf($one);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of 2');
$two->refresh();
$res = $new->getNode()->isDescendantOf($two);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of 1.2');
$one_two->refresh();
$res = $new->getNode()->isDescendantOf($one_two);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of or equal to 1');
$one->refresh();
$res = $new->getNode()->isDescendantOfOrEqualTo($one);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of or equal to 1.2');
$one_two->refresh();
$res = $new->getNode()->isDescendantOfOrEqualTo($one_two);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
output_message('New parent is descendant of or equal to 1.3');
$five->refresh();
$res = $new->getNode()->isDescendantOfOrEqualTo($new);
$msg = $res ? 'true' : 'false';
echo $msg . '</br/>';
function output_tree($root)
{
// display tree
// first we must refresh the node as the tree has been transformed
$root->refresh();
// next we must get the iterator to traverse the tree from the root node
$traverse = $root->getNode()->traverse();
output_node($root);
// now we traverse the tree and output the menu items
while($item = $traverse->next())
{
output_node($item);
}
unset($traverse);
}
function output_node($record)
{
echo str_repeat('-', $record->getNode()->getLevel()) . $record->get('name')
. ' (has children:'.$record->getNode()->hasChildren().') '
. ' (is leaf:'.$record->getNode()->isLeaf().') '.'<br/>';
}
function output_message($msg)
{
echo "
**//$msg//**".'
';
}
</code>
\ No newline at end of file
++++ About
Most users at one time or another have dealt with hierarchical data in a SQL database and no doubt learned that the management of hierarchical data is not what a relational database is intended for. The tables of a relational database are not hierarchical (like XML), but are simply a flat list. Hierarchical data has a parent-child relationship that is not naturally represented in a relational database table.
For our purposes, hierarchical data is a collection of data where each item has a single parent and zero or more children (with the exception of the root item, which has no parent). Hierarchical data can be found in a variety of database applications, including forum and mailing list threads, business organization charts, content management categories, and product categories.
......@@ -18,231 +16,3 @@ These are explained in more detail in the following chapters, or see
* [http://dev.mysql.com/tech-resources/articles/hierarchical-data.html http://dev.mysql.com/tech-resources/articles/hierarchical-data.html]
++++ Setting up
Managing tree structures in doctrine is easy. Doctrine currently fully supports Nested Set, and plans to support the other implementations soon. To set your model to act as a tree, simply add the code below to your models table definition.
Now that Doctrine knows that this model acts as a tree, it will automatically add any required columns for your chosen implementation, so you do not need to set any tree specific columns within your table definition.
Doctrine has standard interface's for managing tree's, that are used by all the implementations. Every record in the table represents a node within the tree (the table), so doctrine provides two interfaces, Tree and Node.
<code type="php">
class Menu extends Doctrine_Record {
public function setTableDefinition() {
$this->setTableName('menu');
// add this your table definition to set the table as NestedSet tree implementation
// $implName is 'NestedSet' or 'AdjacencyList' or 'MaterializedPath'
// $options is an assoc array of options, see implementation docs for options
$this->option('treeImpl', $implName);
$this->option('treeOptions', $options);
// you do not need to add any columns specific to the nested set implementation,
// these are added for you
$this->hasColumn("name","string",30);
}
// this __toString() function is used to get the name for the path, see node::getPath()
public function __toString() {
return $this->get('name');
}
}
</code>
++++ Node interface
The node interface, for inserting and manipulating nodes within the tree, is accessed on a record level. A full implementation of this interface will be as follows:
<code type="php">
interface Doctrine_Node_Interface {
/**
* insert node into tree
*/
public function insertAsParentOf(Doctrine_Record $dest);
public function insertAsPrevSiblingOf(Doctrine_Record $dest);
public function insertAsNextSiblingOf(Doctrine_Record $dest);
public function insertAsFirstChildOf(Doctrine_Record $dest);
public function insertAsLastChildOf(Doctrine_Record $dest);
public function addChild(Doctrine_Record $record);
/**
* moves node (if has children, moves branch)
*
*/
public function moveAsPrevSiblingOf(Doctrine_Record $dest);
public function moveAsNextSiblingOf(Doctrine_Record $dest);
public function moveAsFirstChildOf(Doctrine_Record $dest);
public function moveAsLastChildOf(Doctrine_Record $dest);
/**
* node information
*/
public function getPrevSibling();
public function getNextSibling();
public function getSiblings($includeNode = false);
public function getFirstChild();
public function getLastChild();
public function getChildren();
public function getDescendants();
public function getParent();
public function getAncestors();
public function getPath($seperator = ' > ', $includeNode = false);
public function getLevel();
public function getNumberChildren();
public function getNumberDescendants();
/**
* node checks
*/
public function hasPrevSibling();
public function hasNextSibling();
public function hasChildren();
public function hasParent();
public function isLeaf();
public function isRoot();
public function isEqualTo(Doctrine_Record $subj);
public function isDescendantOf(Doctrine_Record $subj);
public function isDescendantOfOrEqualTo(Doctrine_Record $subj);
public function isValidNode();
/**
* deletes node and it's descendants
*/
public function delete();
}
// if your model acts as tree you can retrieve the associated node object as follows
$record = $manager->getTable('Model')->find($pk);
$nodeObj = $record->getNode();
</code>
++++ Tree interface
The tree interface, for creating and accessing the tree, is accessed on a table level. A full implementation of this interface would be as follows:
<code type="php">
interface Doctrine_Tree_Interface {
/**
* creates root node from given record or from a new record
*/
public function createRoot(Doctrine_Record $record = null);
/**
* returns root node
*/
public function findRoot($root_id = 1);
/**
* optimised method to returns iterator for traversal of the entire tree
* from root
*/
public function fetchTree($options = array());
/**
* optimised method that returns iterator for traversal of the tree from the
* given record's primary key
*/
public function fetchBranch($pk, $options = array());
}
// if your model acts as tree you can retrieve the associated tree object as follows
$treeObj = $manager->getTable('Model')->getTree();
</code>
++++ Traversing or Walking Trees
You can traverse a Tree in different ways, please see here for more information [http://en.wikipedia.org/wiki/Tree_traversal http://en.wikipedia.org/wiki/Tree_traversal].
The most common way of traversing a tree is Pre Order Traversal as explained in the link above, this is also what is known as walking the tree, this is the default approach when traversing a tree in Doctrine, however Doctrine does plan to provide support for Post and Level Order Traversal (not currently implemented)
<code type="php">
/*
* traverse the entire tree from root
*/
$root = $manager->getTable('Model')->getTree()->fetchRoot();
if($root->exists())
{
$tree = $root->traverse();
while($node = $tree->next())
{
// output your tree here
}
}
// or the optimised approach using tree::fetchTree
$tree = $manager->getTable('Model')->getTree()->fetchTree();
while($node = $tree->next())
{
// output tree here
}
/*
* traverse a branch of the tree
*/
$record = $manager->getTable('Model')->find($pk);
if($record->exists())
{
$branch = $record->traverse();
while($node = $branch->next())
{
// output your tree here
}
}
// or the optimised approach
$branch = $manager->getTable('Model')->getTree()->fetchBranch($pk);
while($node = $branch->traverse())
{
// output your tree here
}
</code>
++++ Read me
If performing batch tree manipulation tasks, then remember to refresh your records (see record::refresh()), as any transformations of the tree are likely to affect all instances of records that you have in your scope.
You can save an already existing node using record::save() without affecting it's position within the tree. Remember to never set the tree specific record attributes manually.
If you are inserting or moving a node within the tree, you must use the appropriate node method. Note: you do not need to save a record once you have inserted it or moved it within the tree, any other changes to your record will also be saved within these operations. You cannot save a new record without inserting it into the tree.
If you wish to delete a record, you MUST delete the node and not the record, using $record->deleteNode() or $record->getNode()->delete(). Deleting a node, will by default delete all its descendants. if you delete a record without using the node::delete() method you tree is likely to become corrupt (and fall down)!
The difference between descendants and children is that descendants include children of children whereas children are direct descendants of their parent (real children not gran children and great gran children etc etc).
++++ Introduction
Basically Nested Set is optimized for traversing trees, as this can be done with minimal queries, however updating the tree can be costly as it will affect all rows within the table.
Nested Set is a solution for storing hierarchical data that provides very fast read access. However, updating nested set trees is more costly.
Therefore this solution is best suited for hierarchies that are much more frequently read than written to. And because of the nature of the web,
this is the case for most web applications.
For more information, read here:
For more detailed information on the Nested Set, read here:
* [http://www.sitepoint.com/article/hierarchical-data-database/2 http://www.sitepoint.com/article/hierarchical-data-database/2]
* [http://dev.mysql.com/tech-resources/articles/hierarchical-data.html http://dev.mysql.com/tech-resources/articles/hierarchical-data.html]
......@@ -13,77 +15,214 @@ For more information, read here:
To set up your model as Nested Set, you must add the following code to your model's table definition.
<code type="php">
class Menu extends Doctrine_Record {
...
public function setTableDefinition() {
...
$this->setTableName('menu');
$this->actAs('NestedSet');
// add this your table definition to set the table as NestedSet tree
// implementation
$this->option('treeImpl', 'NestedSet');
$this->option('treeOptions', array());
...
}
...
</code>
// you do not need to add any columns specific to the nested set
// implementation, these are added for you
$this->hasColumn("name","string",30);
"actAs" is a convenience method that loads templates that are shipped with Doctrine (Doctrine_Template_* classes).
The more general alternative would look like this:
}
<code type="php">
...
public function setTableDefinition() {
...
$this->loadTemplate('Doctrine_Template_NestedSet');
// this __toString() function is used to get the name for the path, see
// node::getPath()
public function __toString() {
return $this->get('name');
...
}
}
...
</code>
Detailed information on Doctrine's templating model can be found in chapter 12: Class templates. These templates add some functionality to your model. In the example of the nested set, your model gets 3 additional fields: "lft", "rgt", "level". You never need to care about "lft" and "rgt". These are used internally to manage the tree structure. The "level" field however, is of interest for you because it's an integer value that represents the depth of a node within it's tree. A level of 0 means it's a root node. 1 means it's a direct child of a root node and so on. By reading the "level" field from your nodes you can easily display your tree with proper indendation.
**You must never assign values to lft, rgt, level. These are managed transparently by the nested set implementation.**
++++ Tree options
++++ More than 1 tree in a single table
The nested implementation can be configured to allow your table to have multiple root nodes, and therefore multiple trees within the same table.
The nested set implementation can be configured to allow your table to have multiple root nodes, and therefore multiple trees within the same table.
The example below shows how to setup and use multiple roots based upon the set up above:
<code type="php">
//use these options in the setTableDefinition
$options = array('hasManyRoots' => true, // enable many roots
...
public function setTableDefinition() {
...
$options = array('hasManyRoots' => true, // enable many roots
'rootColumnName' => 'root_id'); // set root column name, defaults to 'root_id'
$this->actAs('NestedSet', $options);
...
}
...
</code>
The rootColumnName is the column that is used to differentiate between trees. When you create a new node to insert it into an existing tree you dont need to care about this field. This is done by the nested set implementation. However, when you want to create a new root node you have the option to set the "root_id" manually. The nested set implementation will recognize that. In the same way you can move nodes between different trees without caring about the "root_id". All of this is handled for you.
++++ Working with the tree(s)
After you successfully set up your model as a nested set you can start working with it. Working with Doctrine's nested set implementation is all about 2 classes: Doctrine_Tree_NestedSet and Doctrine_Node_NestedSet. These are nested set implementations of the interfaces Doctrine_Tree_Interface and Doctrine_Node_Interface. Tree objects are bound to your table objects and node objects are bound to your record objects. This looks as follows:
<code type="php">
// Assuming $conn is an instance of some Doctrine_Connection
$treeObject = $conn->getTable('MyNestedSetModel')->getTree();
// ... the full tree interface is available on $treeObject
// To create new root nodes, if you have manually set the root_id, then it will be used
// otherwise it will automatically use the next available root id
$root = new Menu();
$root->set('name', 'root');
// Assuming $entity is an instance of MyNestedSetModel
$nodeObject = $entity->getNode();
// ... the full node interface is available on $nodeObject
</code>
// insert first root, will auto be assigned root_id = 1
$manager->getTable('Menu')->getTree()->createRoot($root);
In the following sub-chapters you'll see code snippets that demonstrate the most frequently used operations with the node and tree classes.
$another_root = new Menu();
$another_root->set('name', 'another root');
+++++ Creating a root node
// insert another root, will auto be assigned root_id = 2
$manager->getTable('Menu')->getTree()->createRoot($another_root);
<code type="php">
...
$root = new MyNestedSetModel();
$root->name = 'root';
$treeObject = $conn->getTable('MyNestedSetModel)->getTree();
$treeObject->createRoot($root); // calls $root->save() internally
...
</code>
// fetching a specifc root
$root = $manager->getTable('Menu')->getTree()->fetchRoot(1);
$another_root = $manager->getTable('Menu')->getTree()->fetchRoot(2);
+++++ Inserting a node
// fetching all roots
$roots = $manager->getTable('Menu')->getTree()->fetchRoots();
<code type="php">
...
// Assuming $someOtherRecord is an instance of MyNestedSetModel
$record = new MyNestedSetModel();
$record->name = 'somenode';
$record->getNode()->insertAsLastChildOf($someOtherRecord); // calls $record->save() internally
...
</code>
+++++ Deleting a node
++++ Node support
<code type="php">
...
// Assuming $record is an instance of MyNestedSetModel
$record->getNode()->delete(); // calls $record->delete() internally. It's important to delete on the node and not on the record. Otherwise you may corrupt the tree.
...
</code>
The nested set implementation fully supports the full Doctrine node interface
Deleting a node will also delete all descendants of that node. So make sure you move them elsewhere before you delete the node if you dont want to delete them.
+++++ Moving a node
++++ Tree support
<code type="php">
...
// Assuming $record and $someOtherRecord are both instances of MyNestedSetModel
$record->getNode()->moveAsLastChildOf($someOtherRecord);
...
</code>
There are 4 move methods: moveAsLastChildOf($other), moveAsFirstChildOf($other), moveAsPreviousSiblingOf($other) and moveAsNextSiblingOf($other). The method names are self-explanatory.
The nested set implementation fully supports the full Doctrine tree interface
+++++ Examining a node
<code type="php">
...
// Assuming $record is an instance of MyNestedSetModel
$isLeaf = $record->getNode()->isLeaf(); // true/false
$isRoot = $record->getNode()->isRoot(); // true/false
...
</code>
+++++ Examining and retrieving siblings
<code type="php">
...
// Assuming $record is an instance of MyNestedSetModel
$hasNextSib = $record->getNode()->hasNextSibling(); // true/false
$haPrevSib = $record->getNode()->hasPrevSibling(); // true/false
++++ Read me
$nextSib = $record->getNode()->getNextSibling(); // returns false if there is no next sibling, otherwise returns the sibling
$prevSib = $record->getNode()->getPrevSibling(); // returns false if there is no previous sibling, otherwise returns the sibling
$siblings = $record->getNode()->getSiblings(); // an array of all siblings
...
</code>
+++++ Examining and retrieving children / parents / descendants / ancestors
<code type="php">
...
// Assuming $record is an instance of MyNestedSetModel
$hasChildren = $record->getNode()->hasChildren(); // true/false
$hasParent = $record->getNode()->hasParent(); // true/false
$firstChild = $record->getNode()->getFirstChild(); // returns false if there is no first child, otherwise returns the child
$lastChild = $record->getNode()->getLastChild(); // returns false if there is no lase child, otherwise returns the child
$parent = $record->getNode()->getParent(); // returns false if there is no parent, otherwise returns the parent
$children = $record->getNode()->getChildren(); // returns false if there are no children, otherwise returns the children
// !!! IMPORATNT: getChildren() returns only the direct descendants. If you want all descendants, use getDescendants() !!!
$descendants = $record->getNode()->getDescendants(); // returns false if there are no descendants, otherwise returns the descendants
$ancestors = $record->getNode()->getAncestors(); // returns false if there are no ancestors, otherwise returns the ancestors
$numChildren = $record->getNode()->getNumberChildren(); // returns the number of children
$numDescendants = $record->getNode()->getNumberDescendants(); // returns the number of descendants
...
</code>
getDescendants() and getAncestors() both accept a parameter that you can use to specify the "depth" of the resulting branch. For example getDescendants(1) retrieves only the direct descendants (the descendants that are 1 level below, that's the same as getChildren()). In the same fashion getAncestors(1) would only retrieve the direct ancestor (the parent), etc. getAncestors() can be very useful to efficiently determine the path of this node up to the root node or up to some specific ancestor (i.e. to construct a breadcrumb navigation).
+++++ Simply Example: Displaying a tree
<code type="php">
...
$treeObject = $conn->getTable('MyNestedSetModel')->getTree();
$tree = $treeObject->fetchTree();
foreach ($tree as $node) {
echo str_repeat('&nbsp;&nbsp;', $node['level']) . $node['name'] . '<br />';
}
...
</code>
++++ Advanced usage
The previous sections have explained the basic usage of Doctrine's nested set implementation. This section will go one step further.
+++++ Fetching a tree with relations
If you're a demanding software developer this question may already have come into your mind: "How do i fetch a tree/branch with related data?". Simple example: You want to display a tree of categories, but you also want to display some related data of each category, let's say some details of the hottest product in that category. Fetching the tree as seen in the previous sections and simply accessing the relations while iterating over the tree is possible but produces a lot of unnecessary database queries. Luckily, Doctrine_Query and some flexibility in the nested set implementation have come to your rescue. The nested set implementation uses Doctrine_Query objects for all it's database work. By giving you access to the base query object of the nested set implementation you can unleash the full power of Doctrine_Query while using your nested set. Take a look at the following code snippet:
<code type="php">
$query = new Doctrine_Query();
$query->select("cat.name, hp.name, m.name")->from("Category cat")
->leftJoin("cat.hottestProduct hp")
->leftJoin("hp.manufacturer m");
$treeObject = $conn->getTable('Category')->getTree();
$treeObject->setBaseQuery($query);
$tree = $treeObject->fetchTree();
$treeObject->resetBaseQuery();
</code>
There it is, the tree with all the related data you need, all in one query.
You can take it even further. As mentioned in the chapter "Improving Performance" you should only fetch objects when you need them. So, if we need the tree only for display purposes (read-only) we can do:
<code type="php">
$query = new Doctrine_Query();
$query->select("base.name, hp.name, m.name")->from("Category base")
->leftJoin("base.hottestProduct hp")
->leftJoin("hp.manufacturer m")
->setHydrationMode(Doctrine_Query::HYDRATE_ARRAY);
$treeObject = $conn->getTable('Category')->getTree();
$treeObject->setBaseQuery($query);
$tree = $treeObject->fetchTree();
$treeObject->resetBaseQuery();
</code>
The most effective way to traverse a tree from the root node, is to use the {{tree::fetchTree()}} method. It will by default include the root node in the tree and will return an iterator to traverse the tree.
Now you got a nicely structured array in $tree and if you use array access on your records anyway, such a change will not even effect any other part of your code.
This method of modifying the query can be used for all node and tree methods (getAncestors(), getDescendants(), getChildren(), getParent(), ...). Simply create your query, set it as the base query on the tree object and then invoke the appropriate method.
To traverse a tree from a given node, it will normally cost 3 queries, one to fetch the starting node, one to fetch the branch from this node, and one to determine the level of the start node, the traversal algorithm with then determine the level of each subsequent node for you.
......@@ -893,9 +893,7 @@ class ValidatorTest_AddressModel extends Doctrine_Record {
class NestedSetTest_SingleRootNode extends Doctrine_Record {
public function setTableDefinition() {
// Nested set options
$this->option('treeImpl', 'NestedSet');
$this->actAs('NestedSet');
$this->hasColumn('name', 'string', 50, array('notnull'));
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment