
Hector Virgen has released a brilliant Zend_Db_Table extension enabling simple use of the Modified Pre-order Tree Traversal pattern.
If you've never come across MPTT and are curious as to what it is and why, there is an excellent article on sitepoint explaining the whys & what fors.
MPTT is used on this site for the site map, menus at the top and the breadcrumb navigation, due to the simplicity of retrieving parents and children, it is ideal for this purpose.
Hectors class simplifies the use of the pattern quite considerably, it is capable of building the traversal across the whole table just by a parent reference and provides simple methods to fetch a tree, to fetch all the ancestors of a row as well as all of the decendants of a row.
Hectors original post is on his blog go there to grab the source
To use the modified preorder tree traversal algorithm in your table, you will initially have to do just a little bit of work but once it is set up everything should be automated for you.
Firstly, you will need to alter the table in your database and add columns to store the "left" and "right" values. (Remember that 'left' and 'right' are reserved words in SQL - so use something else or your queries will fail!)
Next, you will need to create your table class by extending Virgen_Db_Table, and declare the traversal properties.
<?php
class MyTree extends Virgen_Db_Table
{
protected $_name = 'tree_example';
protected $_traversal = array(
'left' => 'THE NAME OF YOUR LEFT COL HERE',
'right' => 'THE NAME OF YOUR RIGHT COL HERE',
'column' => 'THE NAME OF YOUR PRIMARY KEY COL HERE',
'refColumn' => 'THE NAME OF YOUR PARENT ID COL HERE'
);
}
Now when you insert a new record into your table, all of the left and right columns will be updated as necessary to reflect the new preorder tree.
If you have existing data in your table or want to rebuild the entire tree, Hector has provided a rebuildTreeTraversal() method.
<?php $mytree = new MyTree(); $mytree->rebuildTreeTraversal();
Once your tree is built, you can fetch all descendents of a node with fetchAllDescendents(). The first argument is the node to fetch the descendents of. The node can be either an instance of Zend_Db_Table_Row_Abstract or the string/numeric value of the columns id (based on $_traversal['column']). You can optionally pass in a select object to use as the second argument, which will be used when selecting the descendents (this is where to do any joins or where clauses).
<?php // Fetch all of the children for row with the ID of 1 $node = $mytree->find(1)->current(); $descendents = $mytree->fetchAllDescendents($node); // Identical to: $descendents = $mytree->fetchAllDescendents(1);
// With optional select object
$select = $mytree->select()->where('published = ?', 1)->limit(5);
$descendends = $mytree->fetchAllDescendents($node, $select);
You can also fetch the ancestors just as easily with fetchAllAncestors(). All ancestors from the immediate parent up to the root of the tree will be returned.
<?php $ancestors = $mytree->fetchAllAncestors($node);
You can fetch nodes as a tree by calling $table->fetchTree(). Its functionality is similar to fetchDescendents, except that it returns a modified rowset in that each row contains a tree_depth value.
<?php
$tree = $mytree->fetchTree();
foreach ($tree as $node) {
echo str_repeat(' ', $node->tree_depth * 4) . $node->id . PHP_EOL;
}
This site uses PHPTAL as its template engine, as there is no simple way (it is possible, but is vastly over-complicated) to programatically iterate through the resultset in its returned form, so a simple loop to place all the children of parent rows into an associative array was needed!
For this to work you need to alter your tree fetch statement to the following
<?php $tree = $mytree->fetchTree()->toArray();
and then loop through your tree like this
<?php
$menu = array();
$ref = array();
foreach( $tree as $d ) {
$d['Children'] = array();
if( isset( $ref[ $d['Parent_ID'] ] )) {
$ref[ $d['Parent_ID'] ]['Children'][ $d['Row_ID'] ] = $d;
$ref[ $d['Row_ID'] ] =& $ref[ $d['Parent_ID'] ]['Children'][ $d['Row_ID'] ];
} else {
$menu[ $d['Row_ID'] ] = $d;
$ref[ $d['Row_ID'] ] =& $menu[ $d['Row_ID'] ];
}
}
You are then left with a structured multidimensional array ($menu).
To produce a set of nested uniform lists recursively in TAL to display your tree, you'll need something like this
<ul metal:define-macro="output_list" tal:condition="menu">
<li tal:repeat="list_item menu">
${list_item/Row_Name}
<tal:block tal:define="tree list_item/Children" metal:use-macro="output_list" />
</li>
</ul>
<tal:block metal:use-macro="output_list" />
A big thanks to Hector for his work on this
tqsfmu69b5
It’s really an interesting fact which I just got to know via this blog. Making this blog and keeping it updated is the best possible thing which customer always ask for. So I would like to thank for this interesting knowledge given by the moderator!
Hi
I have some problem with the insert...and probably I will have also with other functions...but I can fetch the tree so I think initialization is ok...
My data array is like this:
array(3) {
["category_name"]=> string(7) "sdadasd"
["category_description"]=> string(11) "asdasdasdas"
["parent_id"]=> string(1) "4"
}
But when I call the insert function in both virgen or zend_db_table_mptt I got this error:
An error occurred
Application error
Exception information:
Message: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ’+ 2 WHERE (left > 6)’ at line 1
Stack trace:
#0 C:xampphtdocsfattoquilibraryZendDbStatement.php(300): Zend_Db_Statement_Pdo->_execute(Array)
#1 C:xampphtdocsfattoquilibraryZendDbAdapterAbstract.php(479): Zend_Db_Statement->execute(Array)
#2 C:xampphtdocsfattoquilibraryZendDbAdapterPdoAbstract.php(238): Zend_Db_Adapter_Abstract->query(’UPDATE `testcat...’, Array)
#3 C:xampphtdocsfattoquilibraryZendDbAdapterAbstract.php(632): Zend_Db_Adapter_Pdo_Abstract->query(’UPDATE `testcat...’, Array)
#4 C:xampphtdocsfattoquilibraryZendDbTableAbstract.php(1130): Zend_Db_Adapter_Abstract->update(’testcat2’, Array, Array)
#5 C:xampphtdocsfattoquiapplicationmodelsvirgen.php(468): Zend_Db_Table_Abstract->update(Array, Array)
#6 C:xampphtdocsfattoquiapplicationmodelsvirgen.php(291): Application_Model_virgen->_insertTraversable(Array)
#7 C:xampphtdocsfattoquiapplicationcontrollersIndex2Controller.php(31): Application_Model_virgen->insert(Array)
#8 C:xampphtdocsfattoquilibraryZendControllerAction.php(513): Index2Controller->addAction()
#9 C:xampphtdocsfattoquilibraryZendControllerDispatcherStandard.php(295): Zend_Controller_Action->dispatch(’addAction’)
#10 C:xampphtdocsfattoquilibraryZendControllerFront.php(954): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Http), Object(Zend_Controller_Response_Http))
#11 C:xampphtdocsfattoquilibraryZendApplicationBootstrapBootstrap.php(97): Zend_Controller_Front->dispatch()
#12 C:xampphtdocsfattoquilibraryZendApplication.php(366): Zend_Application_Bootstrap_Bootstrap->run()
#13 C:xampphtdocsfattoquiindex.php(28): Zend_Application->run()
#14 {main}
Request Parameters:
array (
’controller’ => ’index2’,
’action’ => ’add’,
’module’ => ’default’,
’categoria’ => ’sdadasd’,
’descrizione’ => ’asdasdasdas’,
’padre’ => ’4’,
’Salva’ => ’Salva’,
)
Since I would really like to use the MPTT system to manage my data I would like also to understand how to use this classes.
Hope you could help me.
Thanks
I added a function to my model based on your code, and made changes that someone else might find useful. Also, I\’m using the returned nested array with Zend_Navigation. [code] public function getNavigation($row = null, Zend_Db_Select $select = null) { // local vars $menu = array(); $ref = array(); $id_key = $this->_traversal[\’column\’]; $parent_key = $this->_traversal[\’refColumn\’]; $child_key = \’pages\’; // Get the tree $tree = $this->fetchTree($row, $select)->toArray(); foreach ($tree as $d) { $d[$child_key] = array(); if (isset($ref[$d[$parent_key]])) { $ref[$d[$parent_key]][$child_key][$d[$id_key]] = $d; $ref[$d[$id_key]] =& $ref[$d[$parent_key]][$child_key][$d[$id_key]]; } else { $menu[$d[$id_key]] = $d; $ref[$d[$id_key]] =& $menu[$d[$id_key]]; } } return $menu; } [/code] I also do some manipulation on each navigation record inside the loop that\’s specific to my implementation, so I snipped that out.
HTML tags allowed in comments are: strong,em,ul,ol,li, URL's are automatically converted to links so no need to use <a>.