0

I need to load a full menu for navigation in a custom theme with active trail, and can't find any way to do it, but that can't be right. I can't imagine such an obvious and widespread use-case having no core support, but I've scoured menu.inc and found nothing. The functions offered range from inadequate (no active trail) to completely unusable (menu is missing children for the CSS dropdowns/submenus outside the active trail, and even empty when needed most, such as on broken links).

I've tried using the following functions in my THEME_preprocess_page hook:

  • menu_tree (only renders siblings of the active trail--the navbar can even end up empty)
  • menu_tree_page_data (same result)
  • menu_tree_all_data (contrary to the documentation--luckily--this method does not return hidden items, but there's no active trail)

So far as I can tell, my only options are to copy and rewrite several functions from menu.inc, or to load two separate menus and apply deep analysis/merging to find and duplicate the active trail from the incomplete menu into the complete one--and this is just as bad because the data structures don't match. Is there no other way to get a full menu with active trail?

Also, what if I do go with the latter option using menu_tree_all_data? Will I end up exposing links that aren't properly going through access checks, or have similar issues? How the heck does Drupal itself load a full menu, like the Navigation menu which doesn't go away while visiting other parts of the site?

I did find another nearly identical question but no one answered it and that was 9 months ago. This is for a project that's already running well behind schedule due pretty much exclusively to unforseen Drupal-related issues, so any pointers are much appreciated.

EDIT: Whoops...tried to be thorough and missed a major detail :P I'm on Drupal 7.

  • Hi there, please, could you add a Drupal version? – Ambidex Aug 15 '12 at 6:15
1

First of all, I have to say, the menu system that is implemented in Drupal is, in my eyes, really dodgy. You'll encounter stuff you simply cannot realize with the core implementation, which you would think is basic use cases.

Depending on which Drupal major you are using, I'd suggest taking a look at the Menu Settings per Content Type (CTM) when your at D6. This module will enable you to setup menu settings per content-type, which contains an option to "expand items by default".

On D7 this feature is built-in, when you create a menu item under admin/structure/menu you will find a checkbox Show as expanded, which will do the same.

Edit:

Menu block module

I was just thinking about it again, and remembered that menu_block module also will help you out in this situation. You can create a block which has a lot more options then al the built-in menu functionalities. Also there is an option: Expand all children of this tree., I think that'll be your ticket out of this problem, and should leave the burdon for your clients to actually manually expand them

  • Can "show as expanded" be made to apply by default or triggered in code (in some Drupal-'proper' way)? Clients will be creating content that they put in the menu so the procedure needs to be kept straightforward without hidden steps. – HonoredMule Aug 15 '12 at 16:15
  • It looks like I'll have to write my own "menu_tree_full_data" function based on "menu_tree_page_data." I'm awarding the answer since the "show as expanded" hint is what finally put me on the right track. – HonoredMule Aug 16 '12 at 3:00
  • @HonoredMule, I've edited my original answer. Please take a look at it, I think this will sort out your problems, without the need of hooking PHP up in the core module. – Ambidex Aug 16 '12 at 8:34
  • I'm actually already using menu_block for other things, but because I need to inject the menu into a menu bar area and not a region, this wouldn't work. It's possible I might have been able to turn the menu bar into a region, but my efforts in that direction went quite badly, screwing up the surrounding header layout. I now have a solution I'm very happy with, and I'll post it shortly since it's very reusable. – HonoredMule Aug 16 '12 at 15:53
  • Turning the menu bar into a region would have been pretty wonky anyway, since no block would ever be able to go there without the layout going pear-shaped. – HonoredMule Aug 16 '12 at 16:34
1

I ended up writing my own "menu_tree_full_data" function that builds the menu, still using all the appropriate cache facilities, etc. Also take note any Drupal Core developers around: this is how you write a function that isn't incomprehensibly nested 9 blocks deep.

function menu_tree_full_data($menu_name) {
    $tree = &drupal_static(__FUNCTION__, array());

    // Check if the active trail has been overridden for this menu tree.
    $active_path = menu_tree_get_path($menu_name);

    // Generate a cache ID(cid) specific for this page
    $item = menu_get_item($active_path);
    $cid = "links:$menu_name:full:{$item['href']}:{$GLOBALS['language']->language}";

    // Did we already build this menu during this request?
    if(isset($tree[$cid]))
        return $tree[$cid];

    // If the static variable doesn't have the data, check {cache_menu}.
    $cache = cache_get($cid, 'cache_menu');
    if($cache && isset($cache->data)) {
        $tree_params = $cache->data;
        if(isset($tree_params))
            return $tree[$cid] = menu_build_tree($menu_name, $tree_params);
    }

    $tree_params = array(
        'min_depth' => 1,
        'max_depth' => null,
    );
    // Parent mlids; used both as key and value to ensure uniqueness.
    // We always want all the top-level links with plid == 0.
    $active_trail = array(0 => 0);

    // Find a menu link corresponding to the current path. If $active_path
    // is NULL, let menu_link_get_preferred() determine the path.
    $active_link = menu_link_get_preferred($active_path, $menu_name);
    // The active link may only be taken into account to build the
    // active trail, if it resides in the requested menu. Otherwise,
    // we'd needlessly re-run _menu_build_tree() queries for every menu
    // on every page.
    if(@$active_link['menu_name'] == $menu_name) {
        // Use all the coordinates, except the last one because there
        // can be no child beyond the last column.
        for($i = 1; $i < MENU_MAX_DEPTH; $i++) {
            if($active_link['p' . $i])
                $active_trail[$active_link['p' . $i]] = $active_link['p' . $i];
        }
    }

    $parents = $active_trail;
    do {
        $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
            ->fields('menu_links', array('mlid'))
            ->condition('menu_name', $menu_name)
            //->condition('expanded', 1)
            ->condition('has_children', 1)
            ->condition('plid', $parents, 'IN')
            ->condition('mlid', $parents, 'NOT IN')
            ->execute();
        $num_rows = FALSE;
        foreach($result as $item) {
            $parents[$item['mlid']] = $item['mlid'];
            $num_rows = TRUE;
        }
    } while($num_rows);
    $tree_params['expanded'] = $parents;
    $tree_params['active_trail'] = $active_trail;

    // Cache the tree building parameters using the page-specific cid.
    cache_set($cid, $tree_params, 'cache_menu');

    // Build the tree using the parameters; the resulting tree will be cached by _menu_build_tree().
    return $tree[$cid] = menu_build_tree($menu_name, $tree_params);
}

Aside from changing the cache key, commenting out the expanded condition, dropping the no-longer-relevant $max_depth and $only_active_trail arguments, and the branching on $item['access'] (whether the user can access current page), this is logically equivalent to menu_tree_page_data. 4 of the extra 5 levels of nested code blocks could be just as easily removed from the original function as well.

Now I can finally just load the menu I need as I need it with no mess or fuss, like so:

function THEME_preprocess_page(&$vars) {

    if(@$vars['main_menu']) {
        $menu = variable_get('menu_main_links_source', 'main-menu');
        $vars['main_menu'] = menu_tree_output(menu_tree_full_data($menu));
    }
}

And use it in my theme like so:

<?= render($main_menu); ?>

Or something like so (this is how a lot of other themes seem to handle it, though it just makes a mess for me):

<?php if ($main_menu): ?>
    <nav id="primary-menu" role="navigation">
        <?php print theme('links__system_primary_menu', array(
            'links' => $main_menu,
            'attributes' => array(
                'class' => array('links', 'inline', 'clearfix'),
            )
        )); ?>
    </nav>
<?php endif; ?>

Of course you still have to define or include the menu_tree_full_data function somewhere as well. For me, it goes in a "Shorthand" module I maintain with various simplified object-oriented API wrappers, where it'll also get turned into a method in SH_Menu class and get loaded on demand with shorthand_load_menu(); (Note you've gotta switch __FUNCTION__ for 'menu_tree_full_data' if you do something similar, or the caching won't work correctly.)

  • This code is truly horrible. You really think you write better code than a whole Drupal community? Then why is your “brilliant” code not on d.o. I imagine it'd immediately be picked up and they would hail you for it. (Note: sarcasm). Your code caused our clients database to explode. The cache_menu table would be several GB’s due to this code. This is because you pass the active trail to the _menu_build_tree function, which causes the freaking hash that function creates to be different on every page. Result, almost identical cache entries are flooding the database. You should be embarrassed! – Jeroen Nov 21 '16 at 13:30
  • Let this be a cautionary tale for everyone who wants to copy code from self-proclaimed experts that have no idea how a complex, pluggable CMS is built. Granted Drupal isn’t the best there is, but please… Don’t go around telling people you know it better when you clearly have no clue. – Jeroen Nov 21 '16 at 13:30
  • This code is 4 years old - you took it and just plugged it into a current running system? And if you didn't expect a different cache entry per page, maybe you should have read the code you were copying, or the functional change it was accomplishing. The caching-volume consequence is clearly spelled out in the second comment line (and consistent with the core code at that time). Finally, whatever skill I possess or lack, the commentary on runaway structual complexity of a single function stands. But no, I should be embarrassed. :) – HonoredMule Nov 22 '16 at 2:47
  • Granted. My rant was perhaps misdirected and should have been focused on the themer who googled for 5 minutes and just copied some code. Modern programming ey :ugh: And granted once more, I’m still trying to understand the menu_build_tree and the hundred or so accompanying functions. So yes, I get your point – Jeroen Nov 22 '16 at 10:12
  • Feathers smoothed - and I can commiserate where deciphering Drupal core is involved. I'm quite happy to have moved on from Drupal work myself. – HonoredMule Nov 22 '16 at 19:40
1

I just want to add my experience to the lot. I had similar issues using Drupal menu. The solution I used is:

  • Set all menu items with children to "Expanded". This as to be done manually: Structure > Menus > List links, edit links which has children, check the Show as expanded checkbox.
  • Use menu_tree_page_data function (example: $menu_tree = menu_tree_page_data('main_menu')) to get the menu, with all its children AND with active trail.
  • Create a renderable array using menu_tree_output function (example: $renderable_array = menu_tree_output($menu_tree)) to prepare the menu for rendering.
  • Use the renderable array as content for my custom block, but you could call drupal_render($renderable_array) to get the rendered HTML representing the menu.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.