Recursive PHP menu function

Hi,

I’m having difficulty with a function I am trying to write, basically, my function accepts a pre-defined (hard-coded) array and generates a menu from it. The menu can have as many levels as possible which is the reason behind my thinking of using a recursive function.

An example of the array is as follows:


$aMenu['aOptions']['activeClass'] = 'activeClass1';
$aMenu['aOptions']['inactiveClass'] = 'inactiveClass1';
$aMenu[1]['title'] = 'Home';
$aMenu[1]['url'] = 'link';
$aMenu[2]['title'] = 'MyPerformance';
$aMenu[2]['url'] = 'link';
$aMenu[2]['aOptions']['activeClass']    ='activeClass2';
$aMenu[2]['aOptions']['inactiveClass']    ='inactiveClass2';
$aMenu[2][0]['title'] = 'Child 1';
$aMenu[2][0]['url'] = 'link';
$aMenu[2][1]['title'] = 'Child 2';
$aMenu[2][1]['url'] = 'link';
$aMenu[2][2]['title'] = 'Child 3';
$aMenu[2][2]['url'] = 'link';
$aMenu[2][2]['aOptions']['activeClass']    ='activeClass3';
$aMenu[2][2]['aOptions']['inactiveClass']    ='inactiveClass3';
$aMenu[2][2][0]['title'] = 'Child 1';
$aMenu[2][2][0]['url'] = 'link';
$aMenu[2][2][1]['title'] = 'Child 2';
$aMenu[2][2][1]['url'] = 'link';
$aMenu[2][2][2]['title'] = 'Child 3';
$aMenu[2][2][2]['url'] = 'link';
$aMenu[3]['title'] = 'MyNotepad';
$aMenu[3]['url'] = 'link';
$aMenu[4]['title'] = 'OurBusiness';
$aMenu[4]['url'] = 'link';
$aMenu[5]['title'] = 'OurLibraries';
$aMenu[5]['url'] = 'link';
$aMenu[5]['aOptions']['activeClass'] = 'activeClass';
$aMenu[5]['aOptions']['inactiveClass'] = 'inactiveClass';
$aMenu[5][0]['title'] = 'Child 1';
$aMenu[5][0]['url'] = '';
$aMenu[5][1]['title'] = 'Child 2';
$aMenu[5][1]['url'] = '';
$aMenu[5][2]['title'] = 'Child 3';
$aMenu[5][2]['url'] = '';
$aMenu[6]['title'] = 'Reports';
$aMenu[6]['url'] = 'link';
$aMenu[7]['title'] = 'Dashboard';
$aMenu[7]['url'] = 'link';

As can be seen from the array each child menu needs to their own set of active and inactive styles. My intention was to detect the [‘aOptions’] section to determine when a child menu starts.

Unfortunately the function I have been working on doesn’t work or generate the output I require.


foreach ($aMenu as $section=>$link) {
      $ln.= '<ul>';
      if (isset($link['aOptions'])) {
          $ln.= $this->printMenu($link);
      } 
      $ln.= '<li><a href='.$link['url'].'>'.$link['title'].'</li>';
      $ln.= '</ul>';
} // end of foreach loop

I’m aware that this code isn’t going to function correctly, but the question is how can I generate code which will correctly recurse and output the correct links with urls and titles.

Thanks

The basic form of a recursive function to traverse a tree, using a generic object as an example:

function printMenu ($arrTreeToTraverse) {
  //Pre loop stuff
  echo "<ul>\\r\
";

  foreach ($arrTreeToTraverse as $objItem) {
    //Stuff relevant to the item, before looping over its children
    echo "<li><a href=\\"" . $objItem->getURL() . "\\">" . $objItem->getName() . "</a>";

    if ($objItem->hasChildren()) {
      echo "\\r\
";

      //Call the function again on the children
      printMenu($objItem->getChildren());
    }//if

    echo "</li>\\r\
";
  }//foreach

  //Post loop stuff
  echo "</ul>\\r\
";
}//function

Your function looks generally ok, but you can’t tell without posting the whole function - you look like you just have the middle bit there.

I don’t really have anything else, I never had to write a recursive function before so am not sure of the procedures I should follow (if any). The function is part of a class file I am currently writing.

In the function you have posted, why are you calling the function hasChildren and getChildren, I thought the whole point of a recursive function was to call the same function over and over until the array can’t be walked over again?

You can’t do recursion with a “foreach” statement. You need to wrap your code in a function and call the function with pointer to the top of your array. Then, within the function, you need to call the same function with a pointer to the next level if it exists. Something like:

function DoMenu($parent){
foreach($parent as $node){
if (something that tells you there is a sub-array)
DoMenu($node);
}
// do something on the array here
}

Good luck with your project

Hi jondolar, I’m not quite sure what you mean by not being able to do recursion within a foreach. The example you have posted is doing recursion within a foreach. h actually I see what you mean, I understand now. Maybe I didn’t make it clear but the foreach is within a function I just didn’t have this in the code I posted.

So in theory I call the function like this $menu->doMenu($parent=’’);

And within the doMenu() function I will loop over the menu and check for my [‘aOptions’] array key, if this is set, then I know there are some child menu’s to loop over, therefore call the doMenu() function again.

What do I pass though for the function to know the child links?

Excuse my lack of knowledge on this subject but I’m trying to think exactly how it works, it’s the recursion that is throwing me.

It is just a generic example, using an array of objects I made up. The function calls itself again on the children of the current node, basically, but if there are no children, you can just skip that step.

Here is an example of what I use a lot in my code. It steps over an array, just like in your case, and it will construct HTML code which is returned and then echo’d out.


<?php
	// Define your menu array here.
	$aMenu = array();
	
	function recursiveMenu($menu)
	{
		$html = '';
		
		foreach ($menu as $item)
		{
			// Here we check to see if the item we are looking at
			// is simply another array. In other words, if the item
			// has children.
			if (is_array($item))
			{
				// if it does, then call this function again, passing
				// the item with children as a parameter.
				$html .= recursiveMenu($item);
			}
			else
			{
				// Here you would construct the menu item in HTML code
				$html .= '...';
			}
		}
		
		return $html;
	}
	
	// call the menu function and echo out the html code
	echo recursiveMenu($aMenu);
?>

I’ve added comments all over that script, but essentially it works on the concept that when you’re stepping through an array, the element you’re currently looking at can either be text data (in which case it’s a menu item) or it can be another array. In that case it is a sub-menu and needs to be iterated through separately.

The main idea of a recursive function is that it calls itself again if certain conditions are met. So in the case of this function, if the element currently being looked at is another array, it calls itself again. Recursive functions are extremely powerful, but they can also be very confusing which could lend to their misuse. So be careful. :wink:

Hi Lamiunto, thanks for that, it is something similar to what I currently have. If for example each child menu requires there own css styles for active and inactive states, how can I apply something like this in the function?

Another thought is how do I detect which menu’s above an active menu to show. I.e. Menu 3 could be active and as a result I would need to highlight to two relevant menu links above it.

Currently I am passing these styles like so:

$aMenu[2][‘aOptions’][‘activeClass’] =‘activeClass2’;
$aMenu[2][‘aOptions’][‘inactiveClass’] =‘inactiveClass2’;

The problem I’m having now is that each item within the array is being output to screen rather than ‘title’ and ‘link’ being classed as one item, see output:


[LIST]
[LIST]
[*]&lt;A href=""&gt;activeClass1
[/LIST]
[LIST]
[*]&lt;A href=""&gt;inactiveClass1
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Home
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;MyPerformance
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;activeClass2
[/LIST]
[LIST]
[*]&lt;A href=""&gt;inactiveClass2
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 1
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 2
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 3
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;activeClass3
[/LIST]
[LIST]
[*]&lt;A href=""&gt;inactiveClass3
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 1
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 2
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 3
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;activeClass4
[/LIST]
[LIST]
[*]&lt;A href=""&gt;inactiveClass4
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 1
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[/LIST]
[/LIST]
[LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;activeClass2
[/LIST]
[LIST]
[*]&lt;A href=""&gt;inactiveClass2
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 5
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;MyNotepad
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;OurBusiness
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;OurLibraries
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;activeClass
[/LIST]
[LIST]
[*]&lt;A href=""&gt;inactiveClass
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 1
[/LIST]
[LIST]
[*]&lt;A href=""&gt;
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 2
[/LIST]
[LIST]
[*]&lt;A href=""&gt;
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Child 3
[/LIST]
[LIST]
[*]&lt;A href=""&gt;
[/LIST]
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Reports
[/LIST]
[LIST]
[*]&lt;A href=""&gt;link
[/LIST]
[/LIST]
[LIST]
[LIST]
[*]&lt;A href=""&gt;Dashboard
[/LIST]
[LIST]
[*]link
[/LIST]
[/LIST] 
[/html/ 
For example the 'home' is correct but 'link' below it should be the link of home and not a seperate item.

I think your trouble comes because your array structure isn’t very consistent. You have the title and url for each thing, but then you also just number the submenu items and put them at the same level.

You should create another array key for sub menu items and use that, eg ‘children’ or something.

So instead of:

$aMenu[2]['title'] = 'MyPerformance';
$aMenu[2]['url'] = 'link';
$aMenu[2]['aOptions']['activeClass']    ='activeClass2';
$aMenu[2]['aOptions']['inactiveClass']    ='inactiveClass2';
$aMenu[2][0]['title'] = 'Child 1';
$aMenu[2][0]['url'] = 'link';

use

$aMenu[2]['title'] = 'MyPerformance';
$aMenu[2]['url'] = 'link';
$aMenu[2]['aOptions']['activeClass']    ='activeClass2';
$aMenu[2]['aOptions']['inactiveClass']    ='inactiveClass2';
$aMenu[2]['children'][0]['title'] = 'Child 1';
$aMenu[2]['children'][0]['url'] = 'link';

This will make it a lot easier to iterate over the children of the array without including the options, url etc.

If I were making that menu, I would use an XML file for the simple reason that iterating through the menu’s would be easier, and retrieving different properties would also not require much code.

Your XML file could look something like this


<menu>
  <item>
    <url>http://www.somewhere.com/</url>
    <title>Some link</title>
    <active>activeClass</active>
    <inactive>inactiveClass</inactive>
    <children>
      <child>
        <url>...specify more links, etc...</url>
      </child>
    </children>
  </item>

  <item>
    ....
  </item>
</menu>

You would use SimpleXML to iterate through your XML file and construct a menu.

It’s a bit late here, so if someone else is up to drafting the code to iterate through an XML file with SimpleXML, go for it.

Thanks all, still not been able to figure it out. Think we are going to change the functonality of the menu as we need three menu’s rather than a menu with sub-menu’s inside.

The menu will be called on each page using something like this $oPage->printMenu(2,2,1); The numbers signify the level at which the menu should be active i.e. on the first level, menu 2 will be active followed by the child menu where 2 is also active and the third where 1 is active.

I’ll then write a function to highlight the necessary menu based on these.

Watch this space :goof: