1
votes

I have a very similar situation to this post

PHP: nested menu with a recursive function, expand only some nodes (not all the tree)

and I need some help...

Here is the (partial) contents of my $menuJSONArray variable (used when calling the function):

    Array
(
[0] => Array
    (
        [Menu_IDX] => 1
        [Order] => 1
        [Name] => History
        [Parent] => 
        [Path] => History
        [Link] => 
    )

[1] => Array
    (
        [Menu_IDX] => 2
        [Order] => 25
        [Name] => Review
        [Parent] => 
        [Path] => Review
        [Link] => Review
    )

[2] => Array
    (
        [Menu_IDX] => 3
        [Order] => 35
        [Name] => Past Medical History
        [Parent] => 
        [Path] => Past Medical History
        [Link] => Past Medical History
    )
 [3] => Array
    (
        [Menu_IDX] => 4
        [Order] => 45
        [Name] => Item 1
        [Parent] => 0
        [Path] => Item 1
        [Link] => Item 1
    )
 [4] => Array
    (
        [Menu_IDX] => 5
        [Order] => 55
        [Name] => Item 2
        [Parent] => 0
        [Path] => Item 2
        [Link] => Item 2
    )
 [5] => Array
    (
        [Menu_IDX] => 6
        [Order] => 65
        [Name] => Item 3
        [Parent] => 0
        [Path] => Item 3
        [Link] => Item 3
    )
)

and so on...

I am using the function below but I keep getting stuck in the loop at the first item. Really need some help wrapping my brain around this function. Thank you all ahead of time, this community rocks!!

///RECURSIVE MENU
function recursive($parent, $array) {
$has_children = false; //set value of has children to false by default
foreach($array as $key => $value) {  //loop through the array as key-value pairs
    if ($value['Parent'] == $parent) {  //if the value of Parent field is equal to parent variable   
        if ($has_children === false && $parent) {  //if children is false but parent is not null, this is a sub item
            $has_children = true;  //children is true
            echo '<ul>' ."\n";  //create parent menu ul
        } //otherwise just create the item
        echo '<li>' . "\n";  //create li for parent menu item
        echo '<a href="'.$value['Path'].'">' . $value['Name'] . '</a>' . " \n";  //create link for menu item
            recursive($key, $array); //create sub menu
        echo "</li>\n"; //end parent menu item
    }
}
if ($has_children === true && $parent) echo "</ul>\n"; //end parent menu
}

?>
<?php echo recursive(0, $menuJSONArray); ?></ul>

What I get in return is:

<ul>



<li>
<a href="History">History</a> 
<li>
<a href="History">History</a> 
<li>
<a href="History">History</a> 
<li>
<a href="History">History</a> 
<li>
<a href="History">History</a> 
<li>
<a href="History">History</a>

Can't seem to get out of this loop. Thanks!!

P.S. I am not concerned with the collapsing of the tree etc. from the referenced post as I will be handling that with jquery and css. I just cannot get the proper output of the menu syntax.

2
Personally, why not just use extra levels in your array to denote parent/child relationships: [{"name":"Top Level","path":"/path","children":[{"name":"child","path":"/path/child"}]},{"name":"No Children","path":"/childless"}]Tim Withers
Using a loop inside of a recursive function can be dangerous. Looping is the nature of recursion, so calling a recursive function from inside of a loop may not yield the results you're expecting. You're also using a foreach loop as key/value pairs to iterate over an array with numerical indexes, which is problematic, because when you call your recursive function from the loop, you're not incrementing/changing the array key that gets passed into the recursive call, and so you get the same array node passed into the function, making an infinite loop.Keon Ramezani
@Tim Withers: I had thought of this, however, the array I am building is from a DB (not SQL, otherwise this would be easy) and I want to prevent from having to alter the client's DB structure. Thanks for your input though.FMPHPguy
@KeonRamezani: Ok, this is helpful. I will have to think about this a bit. Recursion tends to send my brain into a loop! I previously had an increment at the end and I got the same result so I knew I did not implement it properly. First thoughts are re-forming the foreach removing the key-value, it makes sense why that wouldn't yield the correct result there.FMPHPguy
You can review this comment. stackoverflow.com/questions/23916237/…Yasin UYSAL

2 Answers

4
votes

As I agree with @Tim Withers I start to solve problem from preparing current array:

function prepareMenu($array)
{
  $return = array();
  //1
  krsort($array);
  foreach ($array as $k => &$item)
  {
    if (is_numeric($item['Parent']))
    {
      $parent = $item['Parent'];
      if (empty($array[$parent]['Childs']))
      {
        $array[$parent]['Childs'] = array();
      }
      //2
      array_unshift($array[$parent]['Childs'],$item);
      unset($array[$k]);
    }
  }
  //3
  ksort($array);
  return $array;
}

Some explanation.

  1. This is a weak point as I assumed that order of your menu array will be constant. Assumed order is:
    • top elements first
    • after that children
    • after that children of children
    • so on..
  2. Here is a place where I add a child at beginning of array to save original order.
  3. Rollback to original order.

Then function to build menu:

function buildMenu($array)
{
  echo '<ul>';
  foreach ($array as $item)
  {
    echo '<li>';
    echo $item['Name'];
    if (!empty($item['Childs']))
    {
      buildMenu($item['Childs']);
    }
    echo '</li>';
  }
  echo '</ul>';
}

With this and proper array order, no matter how deep rabbit hole is - you have your tree.

Usage:

$menu = prepareMenu($menu);
buildMenu($menu);

Of course... There must be better way... :-P


EDIT:

For array (a little midification [next child]):

$menu = array(
array(
        'Menu_IDX' => '1',
        'Order' => '1',
        'Name' => 'History',
        'Parent' => '',
        'Path' => 'History',
        'Link' => '',
    ),
array
    (
        'Menu_IDX' => '2',
        'Order' => '25',
        'Name' => 'Review',
        'Parent' => '',
        'Path' => 'Review',
        'Link' => 'Review',
    ),
array
    (
        'Menu_IDX' => '3',
        'Order' => '35',
        'Name' => 'Past Medical History',
        'Parent' => '',
        'Path' => 'Past Medical History',
        'Link' => 'Past Medical History',
    ),
array
    (
        'Menu_IDX' => '4',
        'Order' => '45',
        'Name' => 'Item 1',
        'Parent' => '0',
        'Path' => 'Item 1',
        'Link' => 'Item 1',
    ),
array
    (
        'Menu_IDX' => '5',
        'Order' => '55',
        'Name' => 'Item 2',
        'Parent' => '0',
        'Path' => 'Item 2',
        'Link' => 'Item 2',
    ),
array
    (
        'Menu_IDX' => '6',
        'Order' => '65',
        'Name' => 'Item 3',
        'Parent' => '0',
        'Path' => 'Item 3',
        'Link' => 'Item 3',
    ),
array
    (
        'Menu_IDX' => '7',
        'Order' => '65',
        'Name' => 'Item 31',
        'Parent' => '5',
        'Path' => 'Item 31',
        'Link' => 'Item 31',
    )
);

Output will be:

  • History
    • Item 1
    • Item 2
    • Item 3
      • Item 31
    • Review
    • Past Medical History
1
votes

Plenty of detail in your question, that's great.

Looking at the code, the first issue I see is with the line:

if ($value['Parent'] == $parent) {

The first time you enter the foreach loop, $value['Parent'] is NULL, and $parent is 0.

Because you are doing a == comparison, this evaluates to TRUE. Try instead:

if ($value['Parent'] === $parent) {

Note the ===. This also checks the data type and requires it to match as well.

The second issue will then be with the line:

if ($has_children === false && $parent) {  //if children is false but parent is not null, this is a sub item

The comment makes it clear that you want to check that $parent is not null, but you aren't doing that, you are casting the integer value of $parent to a Boolean, so 0 will be treated as FALSE, and anything else as TRUE.

Try instead:

if ($has_children === false && !is_null($parent)) {  //if children is false but parent is not null, this is a sub item.