NestedSet support in Propel

Description

With NestedSet implementation, trees are stored using different approach in databases: http://www.sitepoint.com/article/hierarchical-data-database

Nested Set implementation requires three dedicated fields in table structure

  • left
  • right

Plus an optional fields for multi nested set support

  • scope

NB: fields name are free and must be defined in schema.xml

To enable NestedSet support in table, schema.xml must define some specific attributes: treeMode which must take the value NestedSet

  <table name="menu" idMethod="native" treeMode="NestedSet">

Then, left and right field must be defined that way nestedSetLeftKey as a boolean value as nestedSetRightKey

    <column name="lft" type="INTEGER" required="true" default="0" nestedSetLeftKey="true"/>
    <column name="rgt" type="INTEGER" required="true" default="0" nestedSetRightKey="true"/>

For multi nestedset support, an other column must be defined with boolean attribute treeScopeKey set to true

    <column name="scope" type="INTEGER" required="true" default="0" treeScopeKey="true"/>

And then, let's the propel generator automagically create all the needed model and stub classes.

NestedSet usage in Propel

ex: schema.xml extract

  <table name="menu" idMethod="native" treeMode="NestedSet">
    <column name="id" type="INTEGER" required="true" autoIncrement="true" primaryKey="true"/>
    <column name="lft" type="INTEGER" required="true" default="0" nestedSetLeftKey="true"/>
    <column name="rgt" type="INTEGER" required="true" default="0" nestedSetRightKey="true"/>
    <column name="scope" type="INTEGER" required="true" default="0" treeScopeKey="true"/>
    <column name="text" type="VARCHAR" size="128" required="true" default=""/>
    <column name="link" type="VARCHAR" size="255" required="true" default=""/>
    <index name="lft">
      <index-column name="lft"/>
    </index>
    <index name="rgt">
      <index-column name="rgt"/>
    </index>
    <index name="scope">
      <index-column name="scope"/>
    </index>
  </table>

NestedSet insertion

<?php

$root = new Menu();
$root->setText('Google');
$root->setLink('http://www.google.com');

$root->makeRoot();
$root->save();

$menu = new Menu();
$menu->setText('Google Mail');
$menu->setLink('http://mail.google.com');
$menu->insertAsLastChildOf($root);
$menu->save();

$child = new Menu();
$child->setText('Google Maps');
$child->setLink('http://maps.google.com');
$child->insertAsLastChildOf($root);
$child->save();

$sibling = new Menu();
$sibling->setText('Yahoo!');
$sibling->setLink('http://www.yahoo.com');
$sibling->insertAsNextSiblingOf($root);
$sibling->save();

$child = new Menu();
$child->setText('Yahoo! Mail');
$child->setLink('http://mail.yahoo.com');
$child->insertAsLastChildOf($sibling);
$child->save();

Multi NestedSet insertion

<?php

// Create first root node
$root = new Menu();
$root->setText('Google');
$root->setLink('http://www.google.com');

$root->makeRoot();
$root->setScopeIdValue(1); // Tree 1
$root->save();

$menu = new Menu();
$menu->setText('Google Mail');
$menu->setLink('http://mail.google.com');
$menu->insertAsLastChildOf($root);
$menu->save();

// Create secund root node
$root2 = new Menu();
$root2->setText('Yahoo!');
$root2->setLink('http://www.yahoo.com');

$root2->makeRoot();
$root2->setScopeIdValue(2); // Tree 2
$root2->save();

$menu = new Menu();
$menu->setText('Yahoo! Mail');
$menu->setLink('http://mail.yahoo.com');
$menu->insertAsLastChildOf($root2);
$menu->save();

Tree retrieval

<?php
class myMenuOutput extends RecursiveIteratorIterator {
  function __construct(Menu $m) {
    parent::__construct($m, self::SELF_FIRST);
  }

  function beginChildren() {
    echo str_repeat("\t", $this->getDepth());
  }

  function endChildren() {
    echo str_repeat("\t", $this->getDepth() - 1);
  }
}

$menu = MenuPeer::retrieveTree($scopeId);
$it = new myMenuOutput($menu);
foreach($it as $m) {
  echo $m->getText(), '[', $m->getLeftValue(), '-', $m->getRightValue(), "]\n";
}

Tree traversal

NestetSet implementation use the SPL RecursiveIterator as suggested by soenke

NestedSet known broken behaviour

Issue description

For every changes applied on the tree, several entries in the database can be involved. So all already loaded nodes have to be refreshed with their new left/right values.

InstancePool enabled

In order to refresh all loaded nodes, an automatic internal call is made after each tree change to retrieve all instance in InstancePool and update them. And it works fine.

InstancePool disabled

When InstancePool is disabled, their is no way to retrieve references to all already loaded node and get them updated. So in most case, all loaded nodes are not updated and it leads to an inconsistency state. So, workaround is to do an explicit reload for any node you use after tree change.