= 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 [http://dev.mysql.com/tech-resources/articles/hierarchical-data.html a good introduction to nested sets] 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 == Limitations == As of now, the plugin is known to work with MySQL and PostgreSQL (in trunk). Other RDBMS may work but without any guaranty. Patches are welcome, of course :) == Installation == * Install the plugin {{{ symfony plugin-install http://plugins.symfony-project.com/sfPropelActAsNestedSetBehaviorPlugin }}} * Add new fields to your schema.xml {{{ #!xml }}} `scope` and `tree_parent` columns can be of any type. * 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 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 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 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
    getTitle() ?> getDescendants() as $post): ?>
  • getTitle() ?>
}}} === Using nested sets and sfPropelPager === {{{ #!php 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(BaseObject $node)` : Returns true if given node is parent of node. * `bool isDescendantOf(BaseObject $node)` : Returns true if given node is descendant 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 === * `BaseObject|null getParent($peer_method = 'retrieveByPk')` : Returns node parent or null if node does not have a parent. * `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($peer_method = 'doSelectOne')` : Returns given node parent. * `array retrieveSiblings()` : Returns node siblings. * `array getPath($peer_method = 'doSelectOne')` : 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. == Roadmap == === 0.10.0 === ==== Features ==== * add support for symfony's i18n capabilities * add criteria option to more methods * add `$peer_method` as an optional parameter to `getParent()` and `getPath()` * add multiple connections support * add transactional support * get rid of mysql dependency : rewrite queries "criteria-like" or implement adapter. * add a method to copy a whole tree to another scope * make it possible to delete a root node that has children == Changelog == === 2007-07-23 | 0.9.1-beta === ==== Bugfixes ==== * fixed `getLevel()` cache (gordon franke) * fixed scope handling : scope can be any type of data (Jorn.Wagner) * `retrieveFirstChild()` and `retrieveLastChild()` missing references to scope node (Olivier.Mansour) * fixed postgresql compatibility (Maciej.Filipiak & Krasimir.Angelov) * added a note about supported RDBMS (tristan) * made roadmap clearer (tristan) * removed useless Propel::getConnection (Eric.Fredj) * fixed scope handling in `deleteDescendants()` (Piers.Warmers) * fixed new `getDescendants()` implementation node level caching (tristan) ==== Enhancements ==== * added new `isDescendantOf()` method (Piers.Warmers) * implemented faster getPath() method (francois) * implemented faster `getDescendants()` (Jon.Collins) === 2007-05-24 | 0.9.0-beta === * Licence change : MIT -> LGPL * Please welcome a new maintainer : Gordan Franke :) * tree "dumper" utility method : `sfPropelActAsNestedSetBehaviorUtils::dumpTree()` * add optional select method for getPath|getParent|retrieveParent (gordon) === 2007-04-18 | 0.8.2-beta === * added `getParent()` method (olivier mansour) * added `getLevel()` unit tests * implemented caching of level in collection retrieval methods : `getDescendants()`, `getChildren()`, `retrieveSiblings()` * defined plugin roadmap === 2007-03-22 | 0.8.1-beta === * 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-beta === Implemented more methods (+ unit tests) : * `insertAsParentOf` * `retrieveSiblings` * `isEqualTo` * `isChildOf` Enhanced internal API === 2007-02-19 | 0.7.0-beta === Implemented missing methods (+ unit tests) : * `moveToPrevSiblingOf` * `moveToNextSiblingOf` * `deleteChildren` * `deleteDescendants` === 2007-02-19 | 0.6.2-beta === Fixed a bug due to wrong usage of `rtrim`. (Thanks to Krešo Kunjas) === 2007-02-15 | 0.6.1-beta === Fixed minor bug in `getPath()` === 2007-02-15 | 0.6.0-beta === Implemented missing node retrieval methods : * `retrieveFirstChild` * `retrieveLastChild` * `retrieveParent` * `getPath` Updated docs and unit tests accordingly === 2007-02-14 | 0.5.1-beta === Pear package missed plugin's config.php file. === 2007-02-14 | 0.5.0-beta === 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` == Maintainers == Tristan Rivoallan and Gordon Franke. == Contributors == Krasimir Angelov, Jon Collins, Maciej Filipiak, Eric Fredj, Peter van Garderen, Olivier Mansour, Paul Markovitch, Krešo Kunjas, Piers Warmers, Francois Zaninotto. Plugin's code is based on work from [http://propel.phpdb.org/trac/ticket/312 Heltem] and [http://www.symfony-project.com/forum/index.php/m/20657/ Joe Simms]. Thanks to all of you !