Search code examples
symfony1doctrinedoctrine-1.2

Traverse Doctrine NestedSet structure without querying


I usually fetch NestedSet tree like this:

class ModelTable extends Doctrine_Table
{
  /**
   * Gets tree elements in one query
   */
  public function getMenuTree()
  {
    $q = $this->createQuery('p')
      ->orderBy('p.root_id')
      ->addOrderBy('p.lft');

    $tree = $q->execute(array(),  Doctrine_Core::HYDRATE_RECORD_HIERARCHY);      

    return $tree;
  }
}

So I can actually display the whole tree while using only one query to the database.. until I try to traverse the tree. For example, if you call a method on a node like this:

$node->getNode()->getAncestors()

Doctrine will build up a whole new query for this (have a look at Doctrine_Node_NestedSet::getAncestors()). Other traversing methods like getChildren() also use DQL. But this is somewhat inefficient, isn't it? Once I fetched the whole tree, I don't want to query the database any more.

Maybe someone has written a driver to do it the right way? (without DQL)


Solution

  • If you only want to fetch the children (which is the most likely, why would you need getAncestors() to iterate on a tree?), you could also keep the code you showed us as example, and do something like this this:

    foreach ($categories->getFirst()->get('__children') as $child) {
        // ...
    }
    

    This is documented here (hard to find unless you choose to read the whole documentation).

    I have once used recursive code on a whole tree with only ONE query.

    1015 lib % ack --type="php" "_node"                                                                                                                                                          2011-05-15 14:26:22 greg pts/1
    vendor/doctrine/Doctrine/Record.php
    94:    protected $_node;
    814:        unset($vars['_node']);
    2403:        if ( ! isset($this->_node)) {
    2404:            $this->_node = Doctrine_Node::factory($this,
    2410:        return $this->_node;
    liche ~/source/symfony/1.4/lib/plugins/sfDoctrinePlugin/lib
    

    _node only seems to be set in getNode() itself, I don't know whether you can hydrate it like any other field, nor how you would do this.

    I think that getNode() should only be used for modifications on the tree. If you want to display the path from the root, you should use a recursive method to display the tree, whith an argument containing the parent's path . If there is anything else for which you would need the tree functionality, tell us...

    UPDATE

    I think I eventually got it. You want to display a tree menu AND a breadcrumb, and you want to reuse the data of the menu in the breadcrumb, isn't it? To display your breadcrumb, you have to recurse on $tree, and display a node if and only if it is an ancestor of the current page. And there is a method for that : isAncestorOf(). So "all you have to do" is a template which does something like this:

    //module/templates/_breadcrumbElement.php
    foreach ($node->get('__children') as $child) :
      if ($child->isAncestorOf($pageNode)):
         echo link_to($child->getName(), $child->getUrl());
         include_partial('module/breadcrumbElement', array('node' => $child, 'pageNode' => $pageNode));
      endif;
    endforeach;
    

    Feed it the root of your tree and you'll be fine. Hopefully.