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
- One with numeric indexes
- One with string indexes, with
title
andchildren
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