ITT #13: Build a Menu with Recursive Functions
Call Functions Recursively to Increase Productivity
To continue my recent obsession with array handling, I want to spend this week talking about recursive functions and their application in dealing with arrays.
The goal of our exercise today is to build a menu, complete with sub-menu, all based off one function that runs recursively.
What Is Recursion?
In computer science, recursion is a concept in which a function can call itself. This is similar to the idea of looping in PHP, but it provides an opportunity, in the case of array handling, to add more fine-grain control into a program.
On a basic level, recursion can be illustrated with the following code:
function plusOne($x)
{
if($x<10)
{
echo ++$x, "<br />";
plusOne($x);
}
else
{
echo 'Finished! <br />';
}
}
Calling this function will result in the following output:
1
2
3
4
5
6
7
8
9
10
Finished!
Obviously, the above could have been accomplished very simply with a loop, but it shows the basic idea behind recursion.
Constructing a Menu
In order to write our recursive array-handling function, let's first define the array we'll be using. To build a menu, we'll need an element for each menu item that contains information about each item.
NOTE: Bear in mind that this example is very bare-bones, but should extend easily to include further information about your menu, including classes, IDs, or any other attributes you would want to add.
The Basic Array Structure
To start, let's build a very basic menu. We'll pretend we have a site that needs About Us, Blog, Links, and Contact pages. To create this, our array might look like this:
$menu = array(
'about' => array(
'display' => 'About Us'
),
'blog' => array(
'display' => 'Read Our Blog'
),
'links' => array(
'display' => 'Recommended Links'
)
),
'contact' => array(
'display' => 'Contact Us'
)
);
Above, we've given each menu item an array key that easily defines it, and in the case of this example, will serve as the item's URL unless it is otherwise defined.
Adding Sub-Menus
Let's say that under our Recommended Links item, we need to add a sub-menu that holds links for a Products and a Services page. Additionally, the Services page needs a sub-menu that leads the user to Local and Online services.
To accomplish this, we'll add another array element to our "links" element that contains an array holding information about our sub-menus:
'links' => array(
'display' => 'Recommended Links',
'sub' => array(
'products' => array(
'display' => 'High-Quality Products',
'url' => 'links/#products'
),
'services' => array(
'display' => 'Helpful Services',
'url' => 'links/#services',
'sub' => array(
'local' => array(
'display' => 'Local Services',
'url' => 'links/#services_local'
),
'online' => array(
'display' => 'Online Services',
'url' => 'links/#services_online'
)
)
)
)
)
Notice that we included an element called "sub" that holds an array with two elements: "products" and "services". Also, note that the "services" element has a "sub" element as well, containing an array with two elements: "local" and "online".
We've also added a "url" element to our sub-menu items. This is so that we can have more control over where the menu is sending our users.
Turning the Array into a Usable Menu
With our array created, we need to write a function that will crawl through the array and build HTML markup based on the information supplied to it. To start, we need to define our function, which we'll call buildMenu().
function buildMenu($menu_array, $is_sub=FALSE)
{
/*
* If the supplied array is part of a sub-menu, add the
* sub-menu class instead of the menu ID for CSS styling
*/
$attr = (!$is_sub) ? ' id="menu"' : ' class="submenu"';
$menu = "<ul$attr>n"; // Open the menu container
// Create menu HTML here...
/*
* Close the menu container and return the markup for output
*/
return $menu . "</ul>n";
}
We'll be passing two parameters to the function, $menu_array and $is_sub. The first, obviously, is the array containing the array of menu items. The second is a flag that will let our function know if it is creating a sub-menu. This flag is to allow us to add an attribute to the menu HTML for easier CSS styling.
Our function will return a string, $menu, that will contain the HTML markup to display our menu. Since the standard convention for marking up menus is to use the <ul> tag, that's what we'll be doing here.
Step 1: Loop Through the Elements
After checking the $is_sub flag and determining whether or not we're dealing with a sub-menu, we can loop through our elements. Because we're dealing with multi-dimensional arrays, we'll have to nest loops, which sounds complex, but is actually pretty simple once you see it in action.
The way we'll be constructing our loops will follow a pattern like this: for each element in the array, run a loop to extract information out of the nested array.
In theory, the code will look like this:
function buildMenu($menu_array, $is_sub=FALSE)
{
/*
* If the supplied array is part of a sub-menu, add the
* sub-menu class instead of the menu ID for CSS styling
*/
$attr = (!$is_sub) ? ' id="menu"' : ' class="submenu"';
$menu = "<ul$attr>n"; // Open the menu container
/*
* Loop through the array to extract element values
*/
foreach($menu_array as $id => $properties) {
/*
* Because each page element is another array, we
* need to loop again. This time, we save individual
* array elements as variables, using the array key
* as the variable name.
*/
foreach($properties as $key => $val) {
/*
* If the array element contains another array,
* call the buildMenu() function recursively to
* build the sub-menu and store it in $sub
*/
// Code here (see Step 2)...
/*
* Otherwise, set $sub to NULL and store the
* element's value in a variable
*/
// Code here (see Step 2)...
}
/*
* If no array element had the key 'url', set the
* $url variable equal to the containing element's ID
*/
// Code here (see Step 3)...
/*
* Use the created variables to output HTML
*/
// Code here (see Step 3)...
/*
* Destroy the variables to ensure they're reset
* on each iteration
*/
// Code here (see Step 3)...
}
/*
* Close the menu container and return the markup for output
*/
return $menu . "</ul>n";
}
The comments above frame out the steps we need to take in order to loop through the array and generate our markup.
Step 2: Extracting Array Data
Now that our loops are set up, we need to write the code that will process the array elements as they are looped.
Our first step is to determine whether or not the value of the current element is an array. If so, we want to call buildMenu() again. The reason for this is that by doing so, we create another unordered list within our menu that we can store in a variable ($sub) for output with the parent element's markup (thus generating valid XHTML markup).
If the current element is not an array, we use a programming trick called variable variables to dynamically name a variable with the element key (i.e. 'display' => 'About Us' is processed as $display = 'About Us';).
function buildMenu($menu_array, $is_sub=FALSE)
{
/*
* If the supplied array is part of a sub-menu, add the
* sub-menu class instead of the menu ID for CSS styling
*/
$attr = (!$is_sub) ? ' id="menu"' : ' class="submenu"';
$menu = "<ul$attr>n"; // Open the menu container
/*
* Loop through the array to extract element values
*/
foreach($menu_array as $id => $properties) {
/*
* Because each page element is another array, we
* need to loop again. This time, we save individual
* array elements as variables, using the array key
* as the variable name.
*/
foreach($properties as $key => $val) {
/*
* If the array element contains another array,
* call the buildMenu() function recursively to
* build the sub-menu and store it in $sub
*/
if(is_array($val))
{
$sub = buildMenu($val, TRUE);
}
/*
* Otherwise, set $sub to NULL and store the
* element's value in a variable
*/
else
{
$sub = NULL;
$$key = $val;
}
}
/*
* If no array element had the key 'url', set the
* $url variable equal to the containing element's ID
*/
// Code here (see Step 3)...
/*
* Use the created variables to output HTML
*/
// Code here (see Step 3)...
/*
* Destroy the variables to ensure they're reset
* on each iteration
*/
// Code here (see Step 3)...
}
/*
* Close the menu container and return the markup for output
*/
return $menu . "</ul>n";
}
Step 3: Generate HTML and Clean Up
Our final step is to use the information we've extracted to generate our markup.
To start, we need to verify that a URL was supplied, and if not, substitute the top-most element's key as the URL. This provides a shortcut for menu items that don't need fancy URLs (i.e. http://example.com/about/) by allowing us to omit redundant information.
To accomplish this, we simply check that a variable was created called $url, and if not, we create it and set it to the value of $id that was extracted in our outermost loop.
With our URL determined, we're ready to generate markup. This is a fairly straightforward step, as we're just using the URL and display name extracted from our array to create an <li> tag.
With our menu item generated and added to the $menu variable, we can safely unset the variables and start our next iteration of the outermost loop. Unsetting the variables prevents issues with the $url variable storing the original default URL and breaking our menu.
The final function looks like this:
/*
* Loop through the array to extract element values
*/
foreach($menu_array as $id => $properties) {
/*
* Because each page element is another array, we
* need to loop again. This time, we save individual
* array elements as variables, using the array key
* as the variable name.
*/
foreach($properties as $key => $val) {
/*
* If the array element contains another array,
* call the buildMenu() function recursively to
* build the sub-menu and store it in $sub
*/
if(is_array($val))
{
$sub = buildMenu($val, TRUE);
}
/*
* Otherwise, set $sub to NULL and store the
* element's value in a variable
*/
else
{
$sub = NULL;
$$key = $val;
}
}
/*
* If no array element had the key 'url', set the
* $url variable equal to the containing element's ID
*/
if(!isset($url)) {
$url = $id;
}
/*
* Use the created variables to output HTML
*/
$menu .= "<li><a href="$url">$display</a>$sub</li>n";
/*
* Destroy the variables to ensure they're reset
* on each iteration
*/
unset($url, $display, $sub);
}
The Generated Menu
When we run our function with the array we created earlier, we'll see the following output:
Our function was able to create a menu with two sub-levels (and it will go even deeper than that if necessary). The benefit of a function like this is that it allows us to forego the annoying task of editing menu HTML for every new client that we sign up, rather enabling us to quickly define an array that will be parsed into HTML quickly and easily.
Summary
Recursive functions are a great way to overcome complex tasks, especially when handling complex arrays such as our menu above. Taking a little time to get the hang of them can go a long way toward eliminating long strings of spaghetti code that muddies up our scripts.
How do you build your client menus? Do you have any clever ways to use recursion? Let me know in the comments!
Comments for This Entry
Oh, very useful. I could use this in the future.
Good stuff. A lot of PHP developers seem to skip the benefits of recursion in favor of convoluted trees of IF statements. This is a great example of its use.
Real, real neat! I did some php to build my arrays but it wasn't nearly this complex.
This is a nice example of recursion.
I used an array like this in some of my sites:
$pages = array('Home'=>'index.php',
'Services'=>'services.php',
'Portfolio'=>'portfolio.php',
'Contact'=>'contact.php');
Your way allows nesting etc... Great work!
@Brenelz:
One of the benefits of using the above method is that you're able to include selector attributes, like IDs and classes, right in the array:
'twitter' => array(
'display' => 'About Us',
'url' => 'http://twitter.com/',
'class' => 'show_status',
'rel' => 'external'
);
That makes it much easier to have some items styled differently, set as external links, or pretty much anything else that might need to be used. Setting the function to handle it is as easy as checking if the field was set!
Thanks for your feedback, everyone!
this is awesome thanks for this tutorial!
It seems one can use the same exact concept for threaded comments on a blog.
FYI... Your captcha is not very friendly :-)
Nice, I was searching for something like this.
I've pasted some code because my menu building isn't working correctly.. and I can't figure out where I go wrong :S
code:
http://pastie.org/private/a6vmyjkcyqkjdwspcwbeg
@awake:
Recursive functions have tons of great applications, and I imagine threaded comments could fall into that category.
@Monkeytail:
Upon looking at your code, it looks like you need to change this:
echo $menu . "n";
to this:
return $menu . "n";
And modify your show_menu() method to this:
function show_menu() {
echo $this->_build_menu($this->_menu);
}
Otherwise, the sub-menus are output immediately rather than stored in the variable.
Jason,
Thanks! Now it works.
(Would be nice If you could make/explain a database equivalent of the menu - that is now filled by an array)
Hi Jason, any thoughts on how you would create a recursive function to convert this JSON object into an HTML tree?...
{
"folder" : [
{
"title" : "1",
"folder" : [
{
"title" : "1.1"
},
{
"title" : "1.2"
}
]
},
{
"title" : "2",
"folder" : [
{
"title" : "2.1",
"folder" : [
{
"title" : "2.1.1"
}
]
}
]
}
]
}
@brent:
PHP can convert JSON to arrays using json_decode(). The entry on it is here: http://us2.php.net/manual/en/function.json-decode.php
After decoding the JSON, the recursive function we've written should work with some tweaking. Let me know what you come up with!
Thanks for the post...
Yep - Recursion is great at this kind of thing, but of course it comes at with 2 costs - Time and possibly a Server crash.
With shallow depths, recursion is just fine, but if you have deep structures, all hell can break loose.
I'm just starting to write some generalized functions (element emitters) that will do just this, but on a larger scale. Therefore, the code will have to be straight line, but intelligent. Kind of quasi-recursive…
As stated, the function will run without recursion because I estimate it will generate about 250k+ elements per minute at varying levels of depth. Although this would only end up being seconds distributed across all users and the performance hit would be marginal, the risk of hitting “r-depth” is too high in this situation.
To reiterate, the main problem with recursion is: “recursion depth" and as you might imagine, deep list structures can reach the limit (r-depth) pretty quickly. Without guard conditions in your code (checks for current r-depth), the code can bring down a server.
Here are 2 humorous links on recursion:
http://digg.com/tech_news/Did_you_mean_recursion?t=27175533
http://www.gtricks.com/google-tricks/funny-recursion-loop-display/
I must say, without finding this little tutorial, I would have had a friggin stroke. I've been working hard the last 2 days trying to get a dynamic menu of categories created from a single database query, and this tutorial was the missing link to me completing what I was working on.
I've been working on an ecommerce application for CodeIgniter, called Community Cart. The changes that will include your work haven't been posted yet, but will in the next release. Credit is given to you in code comments.
THANK YOU!!!!
@Brian:
Glad I could help out! If you don't mind (and you remember), could you shoot me a link to the project when it goes up?
Thanks!
@Jason:
I just released a new version of the application, and your credit (and modified code) is in the category_menu view. The application can be found in the CodeIgniter Wiki, at http://codeigniter.com/wiki/Community_Cart/
Perhaps in a future release I will use jQuery and make an accordian type menu, but I've got a lot of work to do, so I don't know when/if that will happen.
Thanks again!
While the code is good, how hard can it be to clean it up so it can be FUNCTIONAL right off the bat?
You might be a good programmer, but your sloppiness destroys all that, sorry.
@Jason
Sorry you're having trouble; which part isn't functioning for you?
Aaah, trolls...
Post a Comment
Want to show your face? Get a gravatar!