March 7, 2012

Nested lists using PHP's iterator?

Question by thelolcat

I’m trying to display this kind of array:

$nodes = array(

  1 => array(
         'title'    => 'NodeLvl1',
         'children' => array(),
       ),    
  2 => array(
         'title'    => 'NodeLvl1',
         'children' => array(        
                         1 => array(
                                'title'    => 'NodeLvl2',
                                'children' => array(),
                             ),    
                         2 => array(
                                'title'    => 'NodeLvl2',
                                'children' => array(


                                   1 => array(
                                          'title'    => 'NodeLvl3',
                                          'children' => array(),
                                       ),


                                   2 => array(
                                          'title'    => 'NodeLvl3',
                                          'children' => array(),
                                       ),    
                                ),
                              ),    

                       ),
       ),

  3 => array(
         'title'    => 'NodeLvl1',
         'children' => array(),
       ),    
);

like this:

<ul>
  <li>
    NodeLvl1
  </li>
  <li>
    NodeLvl1
      <ul>
        <li>NodeLv2</li>
         ...

      </ul>
  </li>
  ...

Basically a nested list taking into account the “children” property. So far I’ve come up with this:

class It extends RecursiveIteratorIterator{

  protected
    $tab    = "t";

  public function beginChildren(){

    if(count($this->getInnerIterator()) == 0)
      return;

    echo str_repeat($this->tab, $this->getDepth())."<ul>n";
  }

  public function endChildren(){


    if(count($this->getInnerIterator()) == 0)
      return;

    echo str_repeat($this->tab, $this->getDepth())."n</ul>";
  }

  public function nextElement(){
    echo str_repeat($this->tab, $this->getDepth() + 1).'<li>';
  }

}

$it = new It(new RecursiveArrayIterator($nodes));

foreach($it as $key => $item)
  echo $item;

Which doesn’t work quite right: I get each item wrapped between <ul>s and I don’t know how can I close <li>s…

Any ideas on how to make this work? Also is it possible to get all the array properties (the actual element), instead of just the “title” property inside my foreach() loop? And can this be done with objects instead of arrays?

Answer by satrun77

You can use RecursiveCachingIterator to do what you want. Here is an example, (source: https://github.com/cballou/PHP-SPL-Iterator-Interface-Examples/blob/master/recursive-caching-iterator.php)

<?php
// example navigation array
$nav = array(
    'Home' => '/home',
    'Fake' => array(
        'Double Fake' => array(
            'Nested Double Fake' => '/fake/double/nested',
            'Doubly Nested Double Fake' => '/fake/double/doubly'
        ),
        'Triple Fake' => '/fake/tripe'
    ),
    'Products' => array(
        'Product 1' => '/products/1',
        'Product 2' => '/products/2',
        'Product 3' => '/products/3',
        'Nested Product' => array(
            'Nested 1' => '/products/nested/1',
            'Nested 2' => '/products/nested/2'
        )
    ),
    'Company' => '/company',
    'Privacy Policy' => '/privacy-policy'
);

class NavBuilder extends RecursiveIteratorIterator {

    // stores the previous depth
    private $_depth = 0;

    // stores the current iteration's depth
    private $_curDepth = 0;

    // store the iterator
    protected $_it;

    /**
     * Constructor.
     *
     * @access  public
     * @param   Traversable $it
     * @param   int         $mode
     * @param   int         $flags
     */
    public function __construct(Traversable $it, $mode = RecursiveIteratorIterator::SELF_FIRST, $flags = 0)
    {
        parent::__construct($it, $mode, $flags);

        // store the caching iterator
        $this->_it = $it;
    }

    /**
     * Override the return values.
     *
     * @access  public
     */
    public function current()
    {
        // the return output string
        $output = '';

        // set the current depth
        $this->_curDepth = parent::getDepth();

        // store the difference in depths
        $diff = abs($this->_curDepth - $this->_depth);

        // get the name and url of the nav item
        $name = parent::key();
        $url = parent::current();

        // close previous nested levels
        if ($this->_curDepth < $this->_depth) {
            $output .= str_repeat('</ul></li>', $diff);
        }

        // check if we have the last nav item
        if ($this->hasNext()) {
            $output .= '<li><a href="' . $url . '">' . $name . '</a>';
        } else {
            $output .= '<li class="last"><a href="' . $url . '">' . $name . '</a>';
        }

        // either add a subnav or close the list item
        if ($this->hasChildren()) {
            $output .= '<ul>';
        } else {
            $output .= '</li>';
        }

        // cache the depth
        $this->_depth = $this->_curDepth;

        // return the output ( we could've also overridden current())
        return $output;
    }

}
?>

Usage

<?php

try {

    // generate the recursive caching iterator
    $it = new RecursiveCachingIterator(new RecursiveArrayIterator($nav));

    // build the navigation with the iterator
    $it = new NavBuilder($it, RecursiveIteratorIterator::SELF_FIRST);

    // display the resulting navigation
    echo '<ul id="nav">' . PHP_EOL;
    foreach ($it as $value) {
        echo $value . "n";
    }
    echo '</ul>' . PHP_EOL;

} catch (Exception $e) {
    var_dump($e); die;
}
?>

Answer by Starx

First let me explain few things to you. Your array has two pattens

  1. One with numeric indexes
  2. One with string indexes, with title and children which has be parsed differently

I think a recursive function plays very nice role on this part, rather than complex logics. And our recursive function has to be able to handle both patterns separately.

Here is my version of the function you could use with explanation

function arraytolist(Array $array) { //ensure what you receive is array
  if(count($array)) { //only if it has some items
    //In case the array has `title` index we encountered out PATTERN 2
    if(isset($array['title'])) {
        $o = "<li>";
        $o .= $array['title']; //simply add the title
        $o .= arraytolist($array['children']); //and pass the children to this function to verify again
        $o .= "</li>";
    } else { //if its a normal array, //PATTERN 1
        $o = "<ul>";
        foreach($array as $value) {
            $n = "";
            if(is_array($value)) {  //in case its an array again, 
                //send it to this very same function so that it will return as output again
                $n .= arraytolist($value);
            } else {
                $n .= "<li>$value</li>";
            }
            $o .= strlen($n) ? $n : ""; //if $n has something use it otherwise not
        }
        $o .= "</ul>"; //lets close the ul
    }
    return $o;
  }
}

Some Advantage of this function

  • No iteration level
  • As long as its an array and has item, keeps on building them
  • Power of simple logic in PHP

Author: Nabin Nepal (Starx)

Hello, I am Nabin Nepal and you can call me Starx. This is my blog where write about my life and my involvements. I am a Software Developer, A Cyclist and a Realist. I hope you will find my blog interesting. Follow me on Google+

...

Please fill the form - I will response as fast as I can!