= 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($peer_method = 'doSelect')` : 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 ==

=== 2008-06-07 | Trunk ===

 * feature: Added a backward compatible Criteria parameter in `getChildren()`, `getDescendants()`, `retrieveSiblings()` and `getPath()` (fzaninotto)
 * feature: Added a backward compatible `$peer_method` in `deleteDescendants()` method (works the same way
   as ->getChildren() and ->getDescendants() $peer_method parameter)
 * feature: Allow user to change behaviour name in app.yml (Frederic Coelho)

=== 2007-07-30 | 0.9.1-beta2 ===

 * bugfix: Implemented more reliable `deleteDescendants()` method (Alexander Alexandrov)

=== 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 ==

Alexander Alexandrov, Krasimir Angelov, Jon Collins, Maciej Filipiak, Eric Fredj, Peter van Garderen, Olivier Mansour, Paul Markovitch,
Krešo Kunjas, Piers Warmers, Francois Zaninotto, Romain Dorgueil.

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 !
