# sfPropelActAsNestedSetBehaviorPlugin plugin The `sfPropelActAsNestedSetBehaviorPlugin` is a symfony plugin that provides nested set capabilities to Propel objects. Nested sets (aka modified preorder tree traversal) is a very efficient way (in terms of performances) to browse and edit a tree like structure in an RDBMS. You can read [a good introduction to nested sets](http://dev.mysql.com/tech-resources/articles/hierarchical-data.html) on MySQL developers' zone. ## Features * Fully unit tested * Possibility to store multiple trees in the same table * Atomic operations * Also maintains adjacency list properties, making your classes compatible with code based on this tree traversal methodology ## Installation * Install the plugin symfony plugin-install http://plugins.symfony-project.com/sfPropelActAsNestedSetBehaviorPlugin * Add new fields to your schema.xml [xml] <column name="tree_left" type="INTEGER" required="true" /> <column name="tree_right" type="INTEGER" required="true" /> <column name="tree_parent" type="INTEGER" required="true" /> <column name="topic_id" type="INTEGER" required="true" /> * Enable Propel behavior support in `propel.ini`: propel.builder.AddBehaviors = true If you have to enable the behavior support, rebuild your model: symfony propel-build-model * Enable the behavior for one of your Propel model: [php] <?php // lib/model/ForumPost.php class ForumPost { } $columns_map = array('left' => ForumPostPeer::TREE_LEFT, 'right' => ForumPostPeer::TREE_RIGHT, 'parent' => ForumPostPeer::TREE_PARENT, 'scope' => ForumPostPeer::TOPIC_ID); sfPropelBehavior::add('ForumPost', array('actasnestedset' => array('columns' => $columns_map))); The *column map* is used by the behavior to know which columns hold information it needs : * left : Model column holding nested set left value for a row * right : Model column holding nested set right value for a row * parent : Model column holding row's parent id (this is necessary because we use adjacency list tree traversal for some methods) * scope : Model column holding row's scope id. The scope is used to differenciate trees stored in the same table ## Usage ### Simple tree creation [php] <?php $root = new ForumPost(); $root->makeRoot(); $root->save(); $p1 = new ForumPost(); $p1->insertAsFirstChildOf($root); $p1->save(); $p2 = new ForumPost(); $p2->insertAsFirstChildOf($p1); $p2->save(); /* * Resulting tree : * * ROOT * |- P1 * |- P2 */ ### Multiple trees in a single table [php] <?php $root1 = new ForumPost(); $root1->makeRoot(); $root1->setTopicId(1); $root1->save(); $root2 = new ForumPost(); $root2->makeRoot(); $root2->setTopicId(2); $root2->save(); $p1 = new ForumPost(); $p1->insertAsFirstChildOf($root1); $p1->save(); $p2 = new ForumPost(); $p2->insertAsFirstChildOf($root2); $p2->save(); /* * Resulting trees : * * ROOT1 * |- P1 * * ROOT2 * |- P2 */ ### Lame threaded forum posts list example [php] <?php $root = ForumPostPeer::retrieveByPk(rootnodepk); ?> <ul> <?php echo $root->getTitle() ?> <?php foreach ($root->getDescendants() as $post): ?> <li style="padding-left: <?php echo $post->getLevel() ?>em;"> <?php echo $post->getTitle() ?> </li> <?php endforeach; ?> </ul> ### Using nested sets and sfPropelPager [php] <?php // Decide which posts to fetch $c = new Criteria(); $c->add(ForumPostPeer::TOPIC_ID, $topic_id); $c->addAscendingOrderByColumn(ForumPostPeer::TREE_LEFT); // ForumPostPeer::TREE_LEFT is the column holding nested set's left value // Create pager $pager = new sfPropelPager('ForumPost', 10); $pager->setCriteria($c); $pager->setPage($this->getRequestParameter('page', 1)); $pager->init(); ## Public API Enabling the behaviors adds the following method to the Propel objects : ### Insertion methods * `void insertAsFirstChildOf(BaseObject $dest_node)` : Inserts node as first child of given node. * `void insertAsLastChildOf(BaseObject $dest_node)` : Inserts node as last child of given node. * `void insertAsNextSiblingOf(BaseObject $dest_node)` : Inserts node as next sibling of given node. * `void insertAsPrevSiblingOf(BaseObject $dest_node)` : Inserts node as previous sibling of given node. * `void insertAsParentOf(BaseObject $dest_node)` : Inserts node as parent of given node ### Informational methods * `bool hasChildren()` : Returns true if given node as one or several children. * `bool isRoot()` : Returns true if given node is a root node. * `bool hasParent()` : Returns true if given node has a parent node. * `bool hasNextSibling()` : Returns true if given node has a next sibling. * `bool hasPrevSibling()` : Returns true if given node has a previous sibling. * `bool isLeaf()` : Returns true if given node does not have children. * `bool isChildOf()` : Returns true if given node is parent of node. * `integer getNumberOfChildren()` : Returns given node number of direct children. * `integer getNumberOfDescendants()` : Returns given node number of descendants (n level). * `integer getLevel()` : Returns given node level. ### Node retrieval methods * `array getChildren($peer_method = 'doSelect')` : Returns given node direct children. * `array getDescendants($peer_method = 'doSelect')` : Returns given node descendants (n level). * `BaseObject retrieveNextSibling()` : Returns given node next sibling. * `BaseObject retrievePrevSibling()` : Returns given node previous sibling. * `BaseObject retrieveFirstChild()` : Returns given node first child. * `BaseObject retrieveLastChild()` : Returns given node last child. * `BaseObject retrieveParent()` : Returns given node parent. * `array retrieveSiblings()` : Returns node siblings. * `array getPath()` : Returns path to a specific node as an array, useful to create breadcrumbs. ### Tree modification methods * `void moveToFirstChildOf(BaseObject $dest_node)` : Moves node to first child of given node. * `void moveToLastChildOf(BaseObject $dest_node)` : Moves node to last child of given node. * `void moveToNextSiblingOf(BaseObject $dest_node)` : Moves node to next sibling of given node. * `void moveToPrevSiblingOf(BaseObject $dest_node)` : Moves node to previous sibling of given node. * `void deleteChildren()` : Deletes node direct children * `void deleteDescendants()` : Deletes node descendants (n level) ### Helper methods * `void makeRoot()` : Sets node properties to make it a root node. * `BaseObject reload()` : Returns an up to date version of node * `bool isEqualTo(BaseObject $node)` : Returns true if given node is equivalent to node. ## Changelog ### 2007-03-22 | 0.8.1 * fixed #1480 : non-abstracted column name (paul markovitch) * fixed bug in `getStubFromPeer()` * `makeRoot()` should accept non new objects (peter van garderen) * `getDescendants()` should not try to get descendants if node is a leaf (peter van garderen) * updated unit tests * enabled syntax highlighting in README ### 2007-02-19 | 0.8.0 Implemented more methods (+ unit tests) : * `insertAsParentOf` * `retrieveSiblings` * `isEqualTo` * `isChildOf` Enhanced internal API ### 2007-02-19 | 0.7.0 Implemented missing methods (+ unit tests) : * `moveToPrevSiblingOf` * `moveToNextSiblingOf` * `deleteChildren` * `deleteDescendants` ### 2007-02-19 | 0.6.2 Fixed a bug due to wrong usage of `rtrim`. (Thanks to Krešo Kunjas) ### 2007-02-15 | 0.6.1 Fixed minor bug in `getPath()` ### 2007-02-15 | 0.6.0 Implemented missing node retrieval methods : * `retrieveFirstChild` * `retrieveLastChild` * `retrieveParent` * `getPath` Updated docs and unit tests accordingly ### 2007-02-14 | 0.5.1 Pear package missed plugin's config.php file. ### 2007-02-14 | 0.5.0 Initial public release. The behavior is stable and fully unit-tested, but the API is not yet complete. Missing methods : * `retrieveFirstChild` * `retrieveLastChild` * `moveToPrevSiblingOf` * `moveToNextSiblingOf` * `deleteChildren` * `deleteTree` * `getPath`