Last updated March 7, 2012. Created on January 13, 2007.
Edited by mlncn, jweowu, Matt V., beginner. Log in to edit this page.

Example

Let's start with an example:

  $items['node/%node/edit'] = array(
    'access callback' => 'node_access',
    'access arguments' => array('update', 1),

node_access('update', node_load(arg(1))); will be called for access checking. %foo means foo_load will be called. For more, it is easiest if we begin from the old menu system.

Background

In the past we had code that ran on every page request:


if (arg(0) == 'node' && arg(1) && is_numeric(arg(1)) && ($node = node_load(arg(1)))) {
  $items[] = array(
    'path' => 'node/'. $node->nid,
    'access' => node_access('view', $node),
    'callback' => 'node_view_page',
    'callback arguments' => array($node),
  );
}

And then for comment module:


if (arg(0) == 'comment' && arg(1) == 'reply' && arg(2) && is_numeric(arg(2)) && ($node = node_load(arg(2)))) {
  $items[] = array(
    'path' => 'comment/reply/'. $node->nid,
    'access' => node_access('view', $node),
    'callback' => 'comment_reply',
    'callback arguments' => array($node),
  );
}

Now look at these two definitions! If you take apart the if the first part makes sure you are at a given path (node/123, comment/reply/123) and the second half loads the node with an id of 123. The new menu system already knows how to match dynamic paths (see wildcards for more). For the second half, the is_numeric check can be centralized and then all the menu system needs to know is that we want to perform a load of an object identified by a specific argument and this object happens to be a node.

A very crude translation to new menu system would be:

  $items['node/%'] = array(
    'access callback' => 'node_access',
    'access arguments' => array('view', '$node'),
    'page callback' => 'node_view_page',
    'page arguments' => array('$node'),
    'the argument that specifies $node' => 1,
    'object type to load for argument 1' => 'node',
  );

We needed to quote $node because the new menu system does not run this definition on every page request -- but based on the above it can find out how to produce it because

all the menu system needs to know is that we want to perform a load of an object identified by a specific argument and this object happens to be a node

and we specified the argument and the object type both. We could actually use this notation, but it's simply not nice. Our first observation is that 'the argument that specifies $node' => 1, could be moved in the place of '$node':

  $items['node/%'] = array(
    'access callback' => 'node_access',
    'access arguments' => array('view', 1),
    'page callback' => 'node_view_page',
    'page arguments' => array(1),
    'object type to load for argument 1' => 'node',
  );

Now, why can we load an object for argument 1? Because it's a wildcard. Now, why can't the wildcard tell us something about the object type it replaces?

%wildcard_load()

(See also: Wildcard Loader Arguments.)

  $items['node/%node'] = array(
    'access callback' => 'node_access',
    'access arguments' => array('view', 1),
    'page callback' => 'node_view_page',
    'page arguments' => array(1),
  );
  $items['comment/reply/%node'] = array(
    'access callback' => 'node_access',
    'access arguments' => array('view', 2),
    'page callback' => 'comment_reply',
    'page arguments' => array(2),
  );

When calling comment_reply, the 2 in the arguments array will be replaced by node_load(arg(2)).

The function name is the name of the wildcard with a _load suffix; hence node_load() is used for a %node wildcard.

Access to node/123 is therefore determined by a call to node_access('view', node_load(123)).

What happens with the edit tab?

  $items['node/%node/edit'] = array(
    'access callback' => 'node_access',
    'access arguments' => array('update', 1),
    'page callback' => 'node_edit_page',
    'page arguments' => array(1),
    'title' => 'edit',
    'type' => MENU_LOCAL_TASK
  );

If you are on the path node/123 you will expect this item to produce a tab pointing to node/123/edit. This is rather easy, we replace node/%/edit with node/123/edit where 123 is simply the relevant arg. When you click this tab, then you will land on node/123/edit where the usual object substitution will happen.

You can overwrite the latter behavior for the situation when you want to substitute a value dynamically into a link (or tab) that's being displayed (see %wildcard_to_arg() below).

Additional arguments to the load function can be specified by the load arguments key. Special load arguments are %map and %index. %map is an array containing the path parts, for user/1/edit/this/is/a/category, it'll be array('user', '1', 'edit', 'this', 'is', 'a', 'category'). %index specifies which argument we are at, for the node/%node path it'll be 1, for comment/reply/%node it'll be 2. An example for using these is function user_category_load($uid, &$map, $index) which handles user/%user_category/edit/'. $category['name']. The most important part of this function is:

    // Since the path is like user/%/edit/category_name, the category name will
    // be at a position 2 beyond the index corresponding to the % wildcard.
    $category_index = $index + 2;
    // Valid categories may contain slashes, and hence need to be imploded.
    $category_path = implode('/', array_slice($map, $category_index));

So the map at the end will be array('user', $account, 'edit', 'this/is/a/category').

%wildcard_to_arg()

When you want to substitute a value dynamically into the path of a menu link (or tab), you can define a function with the suffix _to_arg.

A good example of this in core is the function user_uid_optional_to_arg() (with corresponding menu path user/%user_uid_optional) which is used to dynamically change the 'My account' link to point to the account page for the current user.

The special load arguments %map and %index (see above) are automatically passed to a _to_arg function.

Another useful _to_arg function is menu_tail_to_arg().

  $search['search/'. $name .'/%menu_tail'] = ...

This will take all arguments from the wildcard onwards, and return them as one string. This way if you search for foo/bar then the search tabs will point to search/node/foo/bar and search/user/foo/bar, without the menu_tail trick it'd be just search/user/foo.

Note that unlike _load() functions, the return value of a _to_arg() function is not passed to callback functions when you specify the arg number of the wildcard in the 'arguments' array. The value passed will simply be the value of arg(n). A _to_arg() function affects only the path of a menu item.

Combining _load and _to_arg

There is no conflict between these two function types. If you want both the path and the callback arguments to receive the same replacement value, simply implement both a _load() and a _to_arg() function for the same wildcard.

For example, user.module defines both user_uid_optional_load() and user_uid_optional_to_arg() for %user_uid_optional substitutions.

There is no menu_tail_load() defined, on the other hand, so the search_view() callback function for the 'search/'. $name .'/%menu_tail' menu items must recreate the %menu_tail search string from the unmodified args, using search_get_keys().

Integer callback arguments

Any integer n in an 'arguments' array refers to the value of arg(n), or its wildcard_load()ed substitution. This is because the menu_unserialize function uses an is_int check.

This applies to integer literals, constants, and variables; so if your arguments array is array($count), and $count is an integer, then arg($count) will be the value passed.

If you want to pass an integer value to a menu callback, you should instead use a string:

  • '3'
  • (string) 3
  • (string) MY_CONSTANT
  • (string) $my_variable
  • "{$my_variable}"

In Drupal 5, we used to use foreach() loops to recursively declare several MENU_ITEMs or MENU_SUGGESTED_ITEMs.

In Drupal 6, we only need one entry in hook_menu() with the use of a proper wildcard. Additionally, you can create as many menu items (enabled or not) as you see fit with menu_link_save().

For a detailed discussion on this topic, see this issue.

Looking for support? Visit the Drupal.org forums, or join #drupal-support in IRC.

Comments

rjbrown99’s picture

Should the examples use a page callback of node_page_view instead of node_view_page?

http://api.drupal.org/api/function/node_page_view/6

kbk’s picture

would help ease the reader into the materials.

dshumaker’s picture

Hi,

I believe in the section where it says "What happens with the edit tab?" the code is wrong. It says:

'page callback' => 'node_edit_page',

and this failed for me. And looking at the api: node_page_edit

I believe he meant:

'page callback' => 'node_page_edit',

-hopefully helpful,
-d