6

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?

5 Answers 5

15

Do you need a class iterator for this? You could do this with just a simple function...

function arrayToListHTML($array, $level = 0) {
    static $tab = "\t";
    if (empty($array)) return;
    $tabs = str_repeat($tab, $level * 2);
    $result = "{$tabs}<ul>\n";
    foreach ($array as $i => $node):
        $result .= "{$tabs}{$tab}<li>\n{$tabs}{$tab}{$tab}{$node['title']}\n".arrayToListHTML($node['children'], $level + 1)."{$tabs}{$tab}</li>\n";
    endforeach;
    $result .= "{$tabs}</ul>\n";
    return $result;
}

Which will produce this output:

<ul>
    <li>
        NodeLvl1
    </li>
    <li>
        NodeLvl1
        <ul>
            <li>
                NodeLvl2
            </li>
            <li>
                NodeLvl2
                <ul>
                    <li>
                        NodeLvl3
                    </li>
                    <li>
                        NodeLvl3
                    </li>
                </ul>
            </li>
        </ul>
    </li>
    <li>
        NodeLvl1
    </li>
</ul>

This covers what you've shown us, but I'm not sure what you mean by other properties. Are there more properties in each array other than title and children?

Sign up to request clarification or add additional context in comments.

4 Comments

Well I wanted to use these classes because they allow me to iterate trough the array like it would be a flat array..
@animuson: I knew there are other people out there who care about small pleasures like correct indentation!
Hardly small pleasures! I consider correct indentation indispensable. :P
Thank you, I was searching for something simple and clever and your function saved me lots of time!
4

Instead of trying to use your class like an array in foreach() consider using your class to perform the function. For instance, the following code will output correctly but the function is performed inside the class.

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())."<li>".$this->current()."</li>\n";
  }

}

$it = new It(new RecursiveArrayIterator($nodes));
foreach($it as $key => $item)
  //echo $item;
  //it will be better to write a function inside your custom iterator class to handle iterations
?>

Comments

3
+125

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;
}
?>

2 Comments

Whilst this may theoretically answer the question, it would be preferable to include the essential parts of the answer here, and provide the link for reference. Make your answer actually worthy of the bounty!
That code crashes on PHP 7.2 with "Array to string conversion" message.
3

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

Comments

3

I would opt for a simple recursive function that flattens the array into the text/html format:

function arrToList( $arr, $embedded = false ) {
    $output = array();
    if ( $embedded ) $output[] = '<li>';
    $output[] = '<ul>';
    foreach ( $arr as $key => $values ) {
        $output[] = '<li>'.$values['title'].'</li>';
        if ( $values['children'] ) {
            $output[] = arrToList( $values['children'], true );
        }
    }
    $output[] = '</ul>';
    if ( $embedded ) $output[] = '</li>';
    return implode(PHP_EOL, $output);
}

Output from using your input:

  • NodeLvl1
  • NodeLvl1
    • NodeLvl2
    • NodeLvl2
      • NodeLvl3
      • NodeLvl3
  • NodeLvl1

or the actual code:

<ul>
<li>NodeLvl1</li>
<li>NodeLvl1</li>
<li>
<ul>
<li>NodeLvl2</li>
<li>NodeLvl2</li>
<li>
<ul>
<li>NodeLvl3</li>
<li>NodeLvl3</li>
</ul>
</li>
</ul>
</li>
<li>NodeLvl1</li>
</ul>

Cheers

1 Comment

Heads up on code produced being invalid html - ul cannot be a child of another ul; animuson's answer above has that taken into account

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.