Dynamic argument replacement (wildcard)
Let's start with an example:
<?php
$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.
In the past we had code that run on every page request:
<?php
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:
<?php
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:
<?php
$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':
<?php
$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?
<?php
$items['node/%node'] = array(
'access callback' => 'node_access',
'access arguments' => array('view', 1),
'page callback' => 'node_view_page',
'page arguments' => array(1),
);
?><?php
$items['comment/reply/%node'] = array(
'access callback' => 'node_access',
'access arguments' => array('view', 2),
'page callback' => 'comment_reply',
'page arguments' => array(2),
);
?>Note that for matching purposes we will still only use the % wildcard. But when calling comment_reply, the 2 in arguments will be replaced by node_load(arg(2)).
What happens with the edit tab?
<?php
$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. By defining a function called node_to_arg and then node_to_arg(arg(1)) will be called and whatever the function returns will be the replacement of the wildcard. The best example of the use of this in core is function user_uid_optional_to_arg() which is used to dynamically change the 'My account' link to point to the account page for the current user. Note that the naming of this function is based on the name of the object that's loaded- the corresponding menu path is user/%user_current.
A very useful _to_arg function is menu_tail_to_arg.
<?php
$search['search/'. $name .'/%menu_tail'] = ...
?>This will take all arguments after the first 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.
If you want to pass an integer number to a menu callback, use '0' (or (string)MY_CONSTANT if the value is a constant), as the menu_unserialize function uses an is_int check.
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:
<?php
// 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').
About wildcards and MENU_ITEMs
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.
