Hello,

I'm using the following code to hook a module into the menu system:

function metamenu_menu() {
  $items['admin/settings/metamenu/path/%menu_tail'] = array(
    'title' => 'Meta Menu Edit Path',
    'description' => 'Assign flags to path',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('metamenu_edit_path', 4),
    'access arguments' => array('access administration pages'),
    'type' => MENU_LOCAL_TASK,
   );
  return $items;
}

function metamenu_edit_path(&$form_state, $path) {
    drupal_set_message($path);
    ...
}

Instead of the full menu tail, I'm getting just the first element of it -- e.g. if I visit "admin/settings/metamenu/path/foo/bar/baz", I only get "foo" output, instead of "foo/bar/baz", which is what I want.

I've googled the heck out of "menu_tail" and come up with nothing. Am I doing something wrong?

Thanks!

Comments

greenbeans’s picture

Never mind, figured it out. The callback function has to accept a variable number of arguments. The menu_tail is sent as a bunch of separate variables, rather than an array or a single string.

jweowu’s picture

I don't see how this is solved?

I'm seeing the same thing, and it's quite frustrating. menu_tail_to_arg() is being called, and it is generating the expected string ("foo/bar/baz" in the above example), but this value is not passed to the callback function. Which makes this useless?

As an example, the foobar() callback function below receives exactly the same arguments for both of the following paths:
foo/red/green/blue
bar/red/green/blue

<?php
function foo_bar() {
  return "<pre>" . print_r(func_get_args(), TRUE) . "</pre>";
}
function foo_menu() {
  $items = array();
  $items['foo/%menu_tail'] = array(
    'page callback'    => 'foo_bar',
    'page arguments'   => array(1),
    'access arguments' => array('access content'),
    'type'             => MENU_CALLBACK,
  );
  $items['bar'] = array(
    'page callback'    => 'foo_bar',
    'access arguments' => array('access content'),
    'type'             => MENU_CALLBACK,
  );
  return $items;
}

So what is the point?

The only place %menu_tail is used in Drupal core is in search.module, but it looks like it is completely ignored. It uses search_get_keys() to figure out the search pattern instead.

Am I completely failing to understand this, or is this mechanism actually broken?

jweowu’s picture

Following up my own query, I see now that %menu_tail does serve an obvious purpose. As explained here, search_menu() uses it to generate local tasks with the full correct URL, if the search term contains a '/'.

  foreach (module_implements('search') as $name) {
    $items['search/'. $name .'/%menu_tail'] = array(
      'title callback' => 'module_invoke',
      'title arguments' => array($name, 'search', 'name', TRUE),
      'page callback' => 'search_view',
      'page arguments' => array($name),
      'access callback' => '_search_menu',
      'access arguments' => array($name),
      'type' => MENU_LOCAL_TASK,
      'parent' => 'search',
      'file' => 'search.pages.inc',
    );
  }
  return $items;

But the problem still remains (to my mind) that %menu_tail is not then passed as a single argument to the page callback function.

As stated by greenbeans, as soon as you use it, your callback function has to parse func_get_args() and reconstruct the tail from there, which is unintuitive and seems inconsistent with the wildcard documentation.

If we create a menu_tail_load() function instead (but not with that exact name prefix, obviously) as a copy of menu_tail_to_arg(), and we add 'load arguments' => array('%map', '%index') to the menu item definition, then we can pass the tail to the callback, but we do not get the full tail in the menu item's URL.

There doesn't seem to be anything which does both, and I'm not seeing why the _to_arg() functions don't behave this way.

At the very least, there should be a clear note in the documentation along the lines of:
IMPORTANT: Unlike _load() functions, the return value of a _to_arg() function is NOT passed to the callback functions when you specify the arg number of the wildcard in your 'arguments' array. A _to_arg() function affects only the path of a menu item.

jweowu’s picture

And I might as well do that myself :)
Note added here:
http://drupal.org/node/109153#to_arg

jweowu’s picture

So in answer to my original question: Yes, I was completely failing to understand this.

Ironically, I explicitly ignored the solution in a previous reply:

If we create a menu_tail_load() function instead (but not with that exact name prefix, obviously)

In fact, that is exactly what you want to do.

After realising that the _to_arg() functions were purely about manipulating the path, and the _load() functions were purely about manipulating the callback arguments, and remembering that there were separate columns for each in the menu_router table, the penny finally dropped -- there's no conflict between these functions. You can implement both of them for a given wildcard prefix, and each will do their specific task.

Here's an example module based on the %menu_tail functionality.

<?php
function foo_menu() {
  $items = array();
  $items['foo'] = array(
    'title'            => 'foo',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('foo_form'),
    'access arguments' => array('access content'),
    'type'             => MENU_NORMAL_ITEM,
  );
  $items['foo/default'] = array(
    'title'            => 'foo',
    'page callback'    => 'drupal_goto',
    'page arguments'   => array('foo'),
    'access arguments' => array('access content'),
    'type'             => MENU_LOCAL_TASK,
  );
  $items['foo/%foo_tail'] = array(
    'title'            => 'tail test',
    'load arguments'   => array('%map', '%index'),
    'page callback'    => 'foo_callback',
    'page arguments'   => array(1),
    'access arguments' => array('access content'),
    'type'             => MENU_LOCAL_TASK,
  );
  return $items;
}

function foo_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

function foo_tail_load($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

function foo_callback($arg) {
  $form = drupal_get_form('foo_form', $arg);
  return "<pre>" . print_r(func_get_args(), TRUE) . "</pre>" . $form;
}

function foo_form($form_state, $arg = NULL) {
  return array(
    'input'  => array(
      '#type'          => 'textfield',
      '#default_value' => $arg ? $arg : 'red/green/blue',
    ),
    'submit' => array(
      '#type'          => 'submit',
      '#value'         => 'submit',
    ),
  );
}

function foo_form_submit($form, &$form_state) {
  drupal_goto('foo/' . $form_state['values']['input']);
}

I've also updated http://drupal.org/node/109153 more thoroughly, so hopefully this is much clearer now.