Blood, sweat and mlids: rendering menus in Drupal 6

Drupal 6 menu system is a big improvement over Drupal 5. Of course, it's new, and like any major rewrite it will take a few iterations to reach its peak.

I struggled recently with the menu system in a website upgrade from Drupal 5. And again rending fiddly table of contents for a site that uses the book module. What I really miss, as with Drupal 5 before it, are some really nice API functions for building, manipulating and rendering menu structures that are defined in the menu_links table.

So, this tutorial explains how to tackle the Drupal 6 menu system to generate the HTML for a subset of a menu. If you are familiar with nice_menus, that module has some helper functions for similar things.

The goal

For the site I was working on, there is a block that loads part of the primary or secondary menus and renders it as HTML. The rules are:

  1. When current page corresponds to a level 1 menu item: display it's level 2 children
  2. When current page corresponds to a level 2 menu item: display level 2 siblings and level 3 children
  3. When current page matches level 3 or deeper menu items: find the level 2 parent and follow the previous rule.

Hacky solution

After struggling with the Menu API, I stumbled on Nice Menus. Nice menus provides some functions for working with the menu system. I was happy to use its menu-to-html rendering function theme('nice_menu_build') for the final output of the menu fragment.

The following code is my rendering function. It is very site specific so it doesn't take any parameters. I don't really like it and would love a cleaner way. Later in the article you'll see an example of how I'd love to be able to code the same solution.


<?php
/**
* Build a dodgy old tree of menu links and
* hand to nice_menus to render as html.
*/
function mymodule_menu_data_render() {

// OK, the menu system lets us find out about the current page.
// This item will have a bunch of cool stuff ...
$item = menu_get_item();
if (!$item) { return FALSE; }

// ... but not the menu link id.
// So get the menu link id, just the first one from menus of interest.
$result = db_query("
SELECT * FROM {menu_links}
WHERE link_path = '%s' AND hidden <> 1
AND menu_name IN ('primary-links', 'secondary-links')
LIMIT 1",
$item['href']
);

// Now we have a link ID for the active item, load it.
if ($row = db_fetch_object($result)) {
$active_link = menu_link_load($row->mlid);
}
else {
// The active item is not in a menu of interest.
return FALSE;
}

// Now.. we need to build up a replica of the tree that
// menu_tree_all_data() builds. Note that you can only
// get a full tree from that function. Look at theme_nice_menu_tree()
// in http://drupal.org/project/nice_menus for another example
// of how difficult it is to manipulate that tree.

// First step is to gather all the Level 2 links (depth = 2)
// based on a search for all links that share the top level
// parent also owned by the active link (p1 = $active_link['p1']).
$menu = array();
$result = db_query("
SELECT * FROM {menu_links}
WHERE (p1 = %d AND depth = 2)
ORDER BY weight ASC",
$active_link['p1']
);

while ($row = db_fetch_object($result)) {
// menu_link_load() gives us the same structure seen
// in elements of the array from menu_tree_all_data()
$l = menu_link_load($row->mlid);

// Build our fake array.
$menu[$l['mlid']]['link'] = $l;
}
if (!count($menu)) {
return FALSE;
}

// Now determine if we need to display the level 3 children
// of any of our level 2 items. We again do this by using
// the parent hierachy information (p1, p2, p3 ..)
// from our active item we initially loaded.
if ($active_link['depth'] > 1) {
$result = db_query("
SELECT * FROM {menu_links}
WHERE (p1 = %d AND p2 = %d AND depth = 3)
ORDER BY weight ASC",
$active_link['p1'],
$active_link['p2']
);

while ($row = db_fetch_object($result)) {
$l = menu_link_load($row->mlid);
// Add the children to our spoofed menu array.
$menu[$l['plid']]['below'][$l['mlid']]['link'] = $l;
}
}

// At this point I can use nice_menus.
$output = '

    ' . theme('nice_menu_build', $menu) . '

';

return $output;
}
?>

What would be better ...

If you go to any Drupal 6 site and dump the output of menu_tree_all_data('navigation') you can see how the menu tree looks. It is an array with a simple tree structure but with unfortunately weird keys. There was probably a reason for the keys, but because of them, and combined with my lack of skill iterating over large trees, I really don't want to write and maintain this type of obtuse code.

It would be great if there were functions to load parts of the tree and do stuff. In an ideal world the following would do the same thing as the code above.


<?php
/**
* Custom snippet of code for a website.
* Build up a menu fragment based on the current page.
* Anything marked FAKE_ are functions I'd like to see
* in the menu system.
*/
function hypothetical_menu_data_render() {

// This is totally made up code. Don't pastebin it in IRC
// and ask chx to help you with it!

$menu_name = 'primary-links'

// Get all the menu links that are relevant to the current page.
$item = menu_get_item();
$menu_links = FAKE_menu_get_links($menu_name, $item['href']);
// Just want the first matched link.
$active_link = reset($menu_links);

// So $active looks like the output of menu_link_load().

// Get the second level items based on the top level of the active.
$menu_tree = FAKE_menu_tree_get_children($active_link['p0']);

// Get the third level items if the active item is 2rd level or more
if ($active_link['depth'] > 2) {
$menu_tree[$active_link['p0']]['below'] = FAKE_menu_tree_get_children($active_link['depth']);
}

// OK we've now got a tree that is a smaller version
// of menu_tree_all_data()
// We want to render it as a standard menu.
return theme('FAKE_menu_tree_data', $menu_name, $menu_tree);

}
?>

Wouldn't it be nice!