Nested menu with PHP’s RecursiveIteratorIterator

The Standard PHP Library (SPL) has been around for years now, yet we don’t see them being used in our daily codes. Frameworks like Symfony2, Laravel use them internally. Today I am writing about one of the key members of the SPL, the RecursiveIteratorIterator, and implement something web developers always have to deal with, nested menus.

As the name suggests, it is used to iterate through a recursive iterator. There are many recursive iterators provided in SPL like RecursiveArrayIterator and RecursiveDirectoryIterator. A recursive iterator should implement the RecursiveIterator interface. There is not much documentation about these in the PHP website, but there are examples you can learn from.

If you have never used any of the Iterators, you can think of them as ‘loopables’.  The `RecursiveIterator` interface extends `Iterator` which extends `Traversable`, which is described as “Interface to detect if a class is traversable using foreach“, meaning iterators can be directly passed to foreach and looped.

The difference between `Iterator` and `RecursiveIterator` is that `RecursiveIterator` has 2 extra methods that accounts for children `hasChildren` and `getChildren`.

Here we will implement `RecursiveIterator` for menu items. The code sample below are for doctrine and symfony for easily demostrating relation as well. You can implement the same logic without doctrine or symfony.

Here is what our `MenuItem` looks like. It will have `OneToMany` relation on itself for representing child menu items, and `ManyToOne` relation on itself to represent the parent menu item.

<?php
namespace Broncha\Bundle\MenuBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
 
/**
* 
* @ORM\Entity
* @ORM\Table(name="menu_iems")
*/
class MenuItem
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
     private $id;
 
    /**
     * @ORM\Column(name="name", type="string", length=100)
     */
     private $name;
 
    /**
     * @ORM\Column(name="url", type="string")
     */
     private $url;
 
    /**
     * @ORM\ManyToOne(targetEntity="MenuItem", inversedBy="children")
     * @ORM\JoinColumn(name="parent_item_id", referencedColumnName="id")
     */
     private $parent;
 
    /**
     * @var ArrayCollection
     * @ORM\OneToMany(targetEntity="MenuItem", mappedBy="parent")
     */
     private $children;
 
     public function __construct()
     {
         $this->children = new ArrayCollection();
     }
 
     // mutators omitted
}

And now we create out RecursiveMenuItemIterator

<?php
 
namespace Broncha\Bundle\MenuBundle\MenuItem;
use Broncha\Bundle\MenuBundle\Entity\MenuItem;
use Doctrine\Common\Collections\Collection;
 
class RecursiveMenuItemIterator implements \RecursiveIterator
{
    /**
    * @var ArrayCollection | MenuItem[]
    */
    private $_data;
 
    public function __construct(ArrayCollection $data)
    {
        // initialize the iterator with the root menu, i.e. parent id null
        $this->_data = $data;
    }
 
    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 MenuItem;
    }
 
    public function rewind()
    {
        $this->_data->first();
    }
 
    public function hasChildren()
    {
        return ( ! $this->_data->current()->getChildren()->isEmpty());
    }
 
    public function getChildren()
    {
        return new RecursiveMenuItemIterator($this->_data->current()->getChildren());
    }
}

Now we are ready to render our menu. In the controller we will need to initialize the RecursiveMenuItemIterator with the root menuitems (the items that has no parent).
In your controller

<?php
 
namespace Broncha\Bundle\MenuBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Doctrine\Common\Collections\ArrayCollection;
use Broncha\Bundle\MenuBundle\MenuItem\RecursiveMenuItemIterator;
 
class FrontController extends Controller
{
    public function indexAction()
    {
        $em = $this->getDoctrine->getManager();
        $rootMenuItems = $em->getRepository("BronchaMenuBundle:MenuItem")->findBy(['parent' => null]);
 
        $collection = new ArrayCollection($rootMenuItems);
        $recursivemenuIterator = new RecursiveMenuItemIterator($collection);
 
        $iterator = new \RecursiveIteratorIterator($recursivemenuIterator, \RecursiveIteratorIterator::SELF_FIRST);
 
        return $this->render("BronchaMenuBundle:menu.html.twig", ['menuitems' => $iterator])
    }
}

Now we will iterate over the iterator in out twig template

{# BronchaMenuBundle:menu.html.twig #}
 
<ul class="mainmenu">
{% set _depth = 0 %}
{% for item in iterator %}
    {% if loop.index > 1 %}
        {% if iterator.depth > _depth %}
            <ul class="submenu">
        {% elseif iterator.depth < _depth %}
            </li></ul>
        {% else %}
            </li>
        {% endif %}
 
        {% set _depth = iterator.depth %}
    {% endif %}
 
    <li><a href="{{ item.url }}">{{ item.name }}</a>
 
{% endfor%}

This rendering part is a bit tricky. There is no way to know if we are currently in the child node or the parent node. This makes sense as well, as there can be n level of children. So the RecursiveIteratorIterator provides the getDepth method which tells us the depth of the tree where the iterator is right now. So we save the initial depth of the iterator 0 and check the depth on every item from the second item. Notice we didn’t close the <li> tag. That is because if the first item has children, coming to the second item will increase the depth. If the second item is a child item, we add a new <ul> and continue rendering the children, while saving the current depth to _depth. If all children of an item are rendered, the iterator comes back to the previous level, i.e. the depth is decreased. Thats where we close the <li> of the parent and the <ul> of the submenu. If the depth is unchanged we just close the <li> tag.

The above code would render HTML that looks like this.

&lt;ul class="mainmenu"&gt;
    &lt;li&gt;&lt;a href="http://someurl"&gt;Home&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;
        &lt;a href="#"&gt;Categories&lt;/a&gt;
        &lt;ul class="submenu"&gt;
            &lt;li&gt;
                &lt;a href="http://mens"&gt;Mens&lt;/a&gt;
                &lt;ul class="submenu"&gt;
                    &lt;li&gt;&lt;a href="http://accessories"&gt;Accessories&lt;/a&gt;&lt;/li&gt;
                    &lt;li&gt;&lt;a href="http://jeans"&gt;Jeans&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
            &lt;/li&gt;
            &lt;li&gt;&lt;a href="http://women"&gt;Women&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/li&gt;
&lt;/ul&gt;

So no more hand coded recursion! Enjoy.

7 thoughts on “Nested menu with PHP’s RecursiveIteratorIterator”

  1. I have problem with this line: ” {% elseif %}”. Error-message: “Unexpected token “end of statement block” of value “” in AppBundle::menu.html.twig at line 8.”

    1. You can add the display_order property to menu item and when you initialize the iterator, get the menuitems ordered by that field.

Leave a Reply

Your email address will not be published. Required fields are marked *