Unlike Doctrine 1 with it’s NestedSet behaviour, there is no nested set functionality in the core of Doctrine 2. There are a few extensions available that offer nested set support:
- DoctrineExtensions by Gediminas Morkevicius
- Doctrine2 Hierarchical Structural Behavior by Guilherme Blanco
- Doctrine2 NestedSet by Brandon Turner
I tried all of these extensions, but none of them felt simple or lightweight enough for my application. What I wanted to do was have a Category entity which could have a tree of sub-categories, e.g:
Food
Pizza
Margherita
La Reine
Giardiniera
Chocolate
Dark
Milk
White
The simplest way I found of doing this without using any extensions was to make use of SPL’s RecursiveIterator and RecursiveIteratorIterator classes. Here’s the final code, to output a drop-down menu like this one:
/** @var $em \Doctrine\ORM\EntityManager */
$root_categories = $em->getRepository('Entity\Category')->findBy(array('parent_category' => null));
$collection = new Doctrine\Common\Collections\ArrayCollection($root_categories);
$category_iterator = new Entity\RecursiveCategoryIterator($collection);
$recursive_iterator = new RecursiveIteratorIterator($category_iterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($recursive_iterator as $index => $child_category)
{
echo '<option value="' . $child_category->getId() . '">' . str_repeat(' ', $recursive_iterator->getDepth()) . $child_category->getTitle() . '</option>';
}
Here’s how it’s done. Start out with an entity class that looks something like this:
namespace Entity;
/**
* @Entity
* @Table(name="category")
*/
class Category
{
/**
* @Id
* @Column(type="integer", nullable=false)
* @GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* @Column(type="string", length=130, nullable=false)
*/
protected $title;
/**
* @ManyToOne(targetEntity="Category", inversedBy="child_categories")
* @JoinColumn(name="parent_category_id", referencedColumnName="id")
*/
protected $parent_category;
/**
* @OneToMany(targetEntity="Category", mappedBy="parent_category")
*/
protected $child_categories;
public function __construct()
{
$this->child_categories = new \Doctrine\Common\Collections\ArrayCollection;
}
// Getters and setters ...
}
Next, the RecursiveCategoryIterator class. I have written this to interface with Doctrine’s Collection object, but it could easily be re-written to work with native PHP arrays (see this note in the PHP manual for an example of a RecursiveIterator class that uses native arrays).
namespace Entity;
use Doctrine\Common\Collections\Collection;
class RecursiveCategoryIterator implements \RecursiveIterator
{
private $_data;
public function __construct(Collection $data)
{
$this->_data = $data;
}
public function hasChildren()
{
return ( ! $this->_data->current()->getChildCategories()->isEmpty());
}
public function getChildren()
{
return new RecursiveCategoryIterator($this->_data->current()->getChildCategories());
}
public function current()
{
return $this->_data->current();
}
public function next()
{
$this->_data->next();
}
public function key()
{
return $this->_data->key();
}
public function valid()
{
return $this->_data->current() instanceof \Entity\Category;
}
public function rewind()
{
$this->_data->first();
}
}
That’s everything! A very simple Nested Set behaviour in Doctrine 2 using only a simple RecursiveIterator class – no complicated extensions.
Edit: As Hans has pointed out, while this approach does allow us to model hierarchies, it is not truly a nested set model as it does not number each node to allow for tree traversal. This model is probably closer to an adjacency list.