357 lines
11 KiB
Plaintext
357 lines
11 KiB
Plaintext
= 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 !
|