0

I have a problem building an unordered list from multidimensional array containing entities and their children. The problem is I do not want to use recursion as the tree could get very deep and recursion could produce unnecessary load on server.

This is an example of such an array (it is simplified just to contain title and children and the entities could also be objects).

$array = array(
    array('title' => '1', 'children' => array()),
    array('title' => '2', 'children' => array()),
    array('title' => '3', 'children' => array()),
    array('title' => '4', 'children' => array(
        array('title' => '41', 'children' => array()),
        array('title' => '42', 'children' => array()),
        array('title' => '43', 'children' => array()),
        array('title' => '44', 'children' => array(
            array('title' => '441', 'children' => array()),
            array('title' => '442', 'children' => array()),
            array('title' => '443', 'children' => array()),
            array('title' => '444', 'children' => array(
                array('title' => '4441', 'children' => array()),
                array('title' => '4442', 'children' => array()),
                array('title' => '4443', 'children' => array())
            )),
        )),
        array('title' => '45', 'children' => array())
    )),
    array('title' => '5', 'children' => array()),
    array('title' => '6', 'children' => array(
        array('title' => '61', 'children' => array()),
        array('title' => '62', 'children' => array()),
        array('title' => '63', 'children' => array())
    )),
    array('title' => '7', 'children' => array())
);

Doing some research here on SO I came up with this solution that is very close to my desired one:

<html>
<head></head>
<body>
<ul>
<?php
$stack = $array;
$i = 0;
$counts = array();
while(!empty($stack)) {
    $node = array_shift($stack);
    echo "<li>{$node['title']}";
    if($node['children']) {
        echo "<ul>";
        $counts[] = count($node['children']);
        $node['children'] = array_reverse($node['children']);
        foreach($node['children'] as $ch)
            array_unshift($stack, $ch);
    }
    if(!empty($counts)) {
        end($counts);
        if($counts[$key] == 0) {
            echo "</ul>";
            array_pop($counts);
        } else {
            $counts[$key]--;
        }
    }
    if(!$node['children']) {
        echo "</li>";
    }

    // just to make sure we won't end in infinite loop
    $i++;
    if($i == 50) break;
}
?>
</ul>
</body>
</html>

The output is below - as You can see, the problem I have is only the closing </ul> for the sub-trees. My questions: am I overthinking it or am I blind and don't see an obvious mistake? Could You please push me forward to finite solution or give me Your own?

The output:

  • 1
  • 2
  • 3
  • 4
    • 41
    • 42
    • 43
    • 44
      • 441
      • 442
      • 443
      • 444
        • 4441
        • 4442
        • 4443
      • 45
    • 5
    • 6
      • 61
      • 62
      • 63
  • 7
shadyyx
  • 15,825
  • 6
  • 60
  • 95
  • Regardless of the problem with the closing ul tag you are missing the 3 items below the entry marked as 6 anyway. So the algorithm itself is not yet finished. Just saying. – Osiris76 Jan 14 '13 at 16:10
  • It was displayed correctly, I just forgot to write it down here :-) – shadyyx Jan 14 '13 at 16:25
  • It may be difficult to penetrate if you're not familiar with spl iterators because the documentation is so meager, but spl does have solutions for this. The spl author documented an [example of using iterators to build html menus](http://talks.somabo.de/200509_toronto_happy_spling.pps)(with real world functionality like not showing some elements). You can also look at [RecursiveTreeIterator](http://php.net/manual/en/class.recursivetreeiterator.php) and possibly just modify the prefix parts – goat Jan 14 '13 at 16:56

2 Answers2

1

This is not a fix to your code, but maybe it will help you anyway:

function helper($input) {
  $input[] = '</ul>';
  $input = array_reverse($input);
  $input[] = '<ul>';

  // output
  while (sizeof($input) > 0) {
    $el = array_pop($input);

    if (!is_array($el)) {
      echo $el; 
    }
    else {
      // add current element
      $input[] = sprintf('<li>%s', $el['title']);

      // add children
      if (sizeof($el['children']) > 0) {
        $input[] = '</ul>';
        $input = array_merge($input, array_reverse($el['children']));
        $input[] = '<ul>';
      }

      // add closing li
      $input[] = '</li>';
    }
  }
}

helper($array);

demo: http://codepad.viper-7.com/qin15V

Yoshi
  • 54,081
  • 14
  • 89
  • 103
  • Thank You, Yoshi. Though this is much easier and shorter code it is mixing the array of entities with the html elements wich is even less neat than my code, IMHO... But it could be useful in other situations, though. Thank You and +1. – shadyyx Jan 14 '13 at 16:54
  • @shadyyx Personally I would only use the above code inside some function wich in turn would not manipulate the input-array. This way the *mixing* is only used for rendering purpose and thus an *implementation detail*. – Yoshi Jan 14 '13 at 17:00
0

OK, as it usually goes - ask for a solution You are solving for hours and immediately You are out with one... - so did I some more thinking after a small relax and came up with this solution:

<html>
<head></head>
<body>
<ul>
<?php
$i = 0;
$counts = array();
while(!empty($stack)) {
    $node = array_shift($stack);
    if(!empty($counts)) {
        while(end($counts) === 0) {
            $key = key($counts);
            echo "</li></ul>";
            array_pop($counts);
        }
        end($counts);
        $key = key($counts);
        if(isset($counts[$key])) {
            $counts[$key]--;
        }
    }
    echo "<li>{$node['title']}";
    if($node['children']) {
        echo "<ul>";
        $counts[] = count($node['children']);
        $node['children'] = array_reverse($node['children']);
        foreach($node['children'] as $ch)
            array_unshift($stack, $ch);
    }
    if(!$node['children']) {
        echo "</li>";
    }


    $i++;
    if($i == 50) break;
}
?>
</ul>
<p>$i = <?php echo $i; ?></p>
</body>
</html>

The problem was I was missing another while that should subtract the $counts value all the way back up the tree...

I will wait with accepting my own answer just in the case somebody would post their better solution that should be accepted rather than my not so neat solution.

shadyyx
  • 15,825
  • 6
  • 60
  • 95