= 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
<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="scope" type="INTEGER" required="true" />
}}}
`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
<?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(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 !