Making use of wildcards

(This section is a brief summary of Dynamic arguments replacement)

With Drupal 5, if you wanted to define a dynamic menu path (so that node/1, node/342, node/563, ad infinitum were all handled by the node_page_view function), then you would have to check the $may_cache flag in your hook_menu implementation and then check each piece of the path using arg, like this example from D5's node.module:

    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(arg(1));
      if ($node->nid) {
        $items[] = array('path' => 'node/'. arg(1), 'title' => t('View'),
          'callback' => 'node_page_view',
          'callback arguments' => array($node),
          'access' => node_access('view', $node),
          'type' => MENU_CALLBACK); 

Drupal 6 changes this to a much more elegant system, the following being the equivalent code from D6's node.module:

  $items['node/%node'] = array(
    'title' => 'View',
    'page callback' => 'node_page_view',
    'page arguments' => array(1),
    'access callback' => 'node_access',
    'access arguments' => array('view', 1),
    'type' => MENU_CALLBACK); 

Instead of checking if arg(0) was 'node' and arg(1) was a valid node ID, the %node wildcard is given in the menu path (% is the actual wildcard, the rest simply names a load function). The menu system itself will call node_load and check that the node ID is valid, and then instead of specifying $node in the callback arguments, 1 is used, as %node is the 1st part of the path (counting from 0, using / as a separator). To use wildcards in your hook_menu, use %loader_name as one of the parts of the path, and place the number corresponding to the position of the wildcard into the appropriate callback argument arrays. This will cause the result of loader_name_load to be passed to the callback. The core modules in Drupal 6 provide some wildcard loaders that you can use in your menu hook, or you can define your own.

Defining your own wildcard loader

As with any other wildcard loader, place %your_loader_name into the menu path (again, the % is the wildcard, the rest is the name of the loader function), and then implement a function called your_loader_name_load. If you used %flexifilter in a menu path, you might then implement the following:

/**
 * Menu callback; loads a flexifilter object
 */
function flexifilter_load($fid) {
  if (!is_numeric($fid)) {
    return FALSE;
  }
  $filters = flexifilter_get_filters();
  if (!isset($filters[$fid])) {
    return FALSE;
  }
  return $filters[$fid];
}

The load function takes a single argument, which is the part of the URL that matched the wildcard (if the URL was "admin/flexifilter/34/edit", then the argument would be "34"). Remember that this argument came from the URL, which was in turn provided by the user. Remember to be very careful when using it. Do not assume that it will be a positive integer, do not assume that it will match the ID of an existing node/user/entity, do not blindly insert it into SQL queries and do not output it to the page without calling check_plain. Know what kind of input is required, validate it, escape it in any SQL queries, and perform all the other precautions that must be taken with user provided data.

In the example above, there is some simple (but sufficient) error checking in place. Firstly, if the argument isn't a number, and therefore has no chance of being an ID, then the function returns false (which results in a page not found page). Then a list of valid entities is acquired, and if the ID isn't one of those, then false is returned. On the other hand, if the argument matches up to one of the entities, then that entity is returned. This return value is passed on to whatever callback functions referenced the wildcard in their arguments array.

The code from hook_menu to use this wildcard loader would look something like this:

  $items['admin/build/flexifilters/%flexifilter/edit'] = array(
    'title' => 'Edit Flexifilter',
    'type' => MENU_CALLBACK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array('flexifilter_filter_edit_form', 3),
    'file' => 'flexifilter.admin.inc',
  ); 

The mentioned flexifilter_filter_edit_form function would then have the following prototype:

function flexifilter_filter_edit_form($form_state, $flexifilter) { 

The first argument coming from the form system and the second being the result of the flexifilter_load function.

You can specify additional arguments for the load function with the load arguments entry of a menu item. See %user_category for an example of these in use, along with the documentation on load arguments. Furthermore, the function loader_name_to_arg can be implemented to cause a link to be made in the menu when the loader is specified in a menu path. See %user_uid_optional for a practical example of this, or the _to_arg documentation.

Existing wildcard loaders provided in D6 core

%actions

Load function: actions_load
Used by: System module from D6 core
Example usage (from system.module):

  $items['admin/settings/actions/delete/%actions'] = array(
    'title' => 'Delete action',
    'description' => 'Delete an action.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('system_actions_delete_form', 4),
    'type' => MENU_CALLBACK,
  );

Note that %actions is the 4th part of the path (counting from 0, using / as a separator), hence the 4 in the page arguments array.

%aggregator_category

Load function: aggregator_category_load
Used by: Aggregator module from D6 core
Example usage (from aggregator.module):

  $items['aggregator/categories/%aggregator_category'] = array(
    'title callback' => '_aggregator_category_title',
    'title arguments' => array(2),
    'page callback' => 'aggregator_page_category',
    'page arguments' => array(2),
    'access callback' => 'user_access',
    'access arguments' => array('access news feeds'),
    'file' => 'aggregator.pages.inc',
  );

Here, the wildcard is referenced in the title arguments as well as the page arguments. It could have also gone into the access arguments.

%aggregator_feed

Load function: aggregator_feed_load
Used by: Aggregator module from D6 core
Example usage (from aggregator.module):

  $items['admin/content/aggregator/remove/%aggregator_feed'] = array(
    'title' => 'Remove items',
    'page callback' => 'aggregator_admin_remove_feed',
    'page arguments' => array(4),
    'access arguments' => array('administer news feeds'),
    'type' => MENU_CALLBACK,
    'file' => 'aggregator.admin.inc',
  );

%contact

Load function: contact_load
Used by: Contact module from D6 core
Example usage (from contact.module):

  $items['admin/build/contact/edit/%contact'] = array(
    'title' => 'Edit contact category',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('contact_admin_edit', 3, 4),
    'type' => MENU_CALLBACK,
    'file' => 'contact.admin.inc',
  );

Even though part 3 of the path is not a wildcard, it can still be inserted into the arguments array, and will result in the string 'edit' being passed as a parameter to the callback function.

%filter_format

Load function: filter_format_load
Used by: Filter module from D6 core
Example usage (from filter.module):

  $items['admin/settings/filters/%filter_format'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => 'filter_admin_format_page',
    'page arguments' => array(3),
    'access arguments' => array('administer filters'),
    'file' => 'filter.admin.inc',
  );

%forum_term

Load function: forum_term_load
Used by: Forum module from D6 core
Example usage (from forum.module):

  $items['admin/content/forum/edit/%forum_term'] = array(
    'page callback' => 'forum_form_main',
    'type' => MENU_CALLBACK,
    'file' => 'forum.admin.inc',
  );

%menu

Load function: menu_load
Used by: Menu module from D6 core
Example usage (from menu.module):

  $items['admin/build/menu-customize/%menu'] = array(
    'title' => 'Customize menu',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('menu_overview_form', 3),
    'title callback' => 'menu_overview_title',
    'title arguments' => array(3),
    'access arguments' => array('administer menu'),
    'type' => MENU_CALLBACK,
    'file' => 'menu.admin.inc',
  );

%menu_link

Load function: menu_link_load
Used by: Menu module from D6 core
Example usage (from menu.module):

  $items['admin/build/menu/item/%menu_link/edit'] = array(
    'title' => 'Edit menu item',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('menu_edit_item', 'edit', 4, NULL),
    'type' => MENU_CALLBACK,
    'file' => 'menu.admin.inc',
  );

For the page arguments, 5 could have been used instead of 'edit'.

%node

Load function: node_load
Used by: Poll, Comment, Translation, Statistics, Node and Book modules from D6 core
Example usage (from poll.module):

  $items['node/%node/votes'] = array(
    'title' => 'Votes',
    'page callback' => 'poll_votes',
    'page arguments' => array(1),
    'access callback' => '_poll_menu_access',
    'access arguments' => array(1, 'inspect all votes', FALSE),
    'weight' => 3,
    'type' => MENU_LOCAL_TASK,
    'file' => 'poll.pages.inc',
  );

%taxonomy_vocabulary

Load function: taxonomy_vocabulary_load
Used by: Taxonomy module from D6 core
Example usage (from taxonomy.module):

  $items['admin/content/taxonomy/edit/vocabulary/%taxonomy_vocabulary'] = array(
    'title' => 'Edit vocabulary',
    'page callback' => 'taxonomy_admin_vocabulary_edit',
    'page arguments' => array(5),
    'type' => MENU_CALLBACK,
    'file' => 'taxonomy.admin.inc',
  );

%user

Load function: user_load
Used by: Contact, Blog, User, Statistics, OpenID and Tracker modules from D6 core
Example usage (from contact.module):

  $items['user/%user/contact'] = array(
    'title' => 'Contact',
    'page callback' => 'contact_user_page',
    'page arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'access callback' => '_contact_user_tab_access',
    'access arguments' => array(1),
    'weight' => 2,
    'file' => 'contact.pages.inc',
  );

%user_category

Load function: user_category_load
Used by: User module from D6 core
Example usage (from user.module):

  $items['user/%user_category/edit'] = array(
    'title' => 'Edit',
    'page callback' => 'user_edit',
    'page arguments' => array(1),
    'access callback' => 'user_edit_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'load arguments' => array('%map', '%index'),
    'file' => 'user.pages.inc',
  );

Note the use of load arguments, which must be present (as shown) in order to use %user_category. See the documentation on load arguments for more information.

%user_uid_optional (was %user_current in 6.0 and 6.1)

Load function: user_uid_optional_load
Used by: Tracker, User and Blog modules from D6 core
Example usage (from tracker.module):

  $items['tracker/%user_uid_optional'] = array(
    'title' => 'My recent posts',
    'access callback' => '_tracker_myrecent_access',
    'access arguments' => array(1),
    'page arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
  );

%user_uid_optional behaves identically to %user (user_uid_optional_load directly calls user_load), however, %user_uid_optional creates a menu link as well. Hence there is a "My recent posts" link in the Drupal 6 menu which leads to tracker/(your user id), but you can also go to tracker/(any user id) to see recent posts from other people. See _to_arg for more information on this.

Comments

jvandervort’s picture

I've found that custom wildcard loaders must be included in the main include path
(ie NOT in the 'file' => 'mymodule.admin.inc' statement in the items array of the _menu hook).
This may be by design, but it was frustrating to figure out.

-John

-John

joachim’s picture

Would it not be simpler to say, before we get to the long examples with old code:

- a numeric argument in either page_arguments or access_arguments is replaced with the return of the function WILDCARD_load, where WILDCARD is the string found after the percent sign in the path component given by the number.

hargobind’s picture

It should be noted that if you have OPTIONAL arguments at the end of your path, you DON'T need to add a wildcard sign to capture those arguments.

For example:

$items['print_navigation'] = array(
  'title' => 'Print Navigation',
  'type' => MENU_CALLBACK,
  'access callback' => TRUE,
  'page callback' => 'mymodule_print_navigation',
);

function mymodule_print_navigation($arg1 = null, $arg2 = null, $arg3 = null) {
  //- Do something. -//
}

By calling the following paths, you can see what the values of the "$arg" variables in mymodule_print_navigation() are:

http://example.com/print_navigation -- [all three $args are null]
http://example.com/print_navigation/primary-links -- [$arg1 is "primary-links"]
http://example.com/print_navigation/menu-news/425 -- [$arg1 is my custom "news" menu, $arg2 is "425"]

However, if you use a wildcard like this:

$items['print_navigation/%'] = array(
  'title' => 'Print Navigation',
  'type' => MENU_CALLBACK,
  'access callback' => TRUE,
  'page callback' => 'mymodule_print_navigation',
);

...then if you try to visit http://example.com/print_navigation, it will give you a "404 file not found" message.

codycraven’s picture

Your post about OPTIONAL arguments is just what I was looking for. Thanks!

mpruitt’s picture

Linxor, in your example, I understand that in the last path example, "425" is $arg2. The page callback in your example is "mymodule_print_navigation". When you call it, since you've set $arg2 = null in your parameters, when you start your function, how does it know that "425" is available as $arg2?

Thanks.

hargobind’s picture

What you're asking about is a basic feature of PHP called "default arguments" or "optional arguments" (a generally accepted term for this feature).

You can see an example here: http://us.php.net/manual/en/functions.arguments.php#functions.arguments....

In that example, when the first function is called, there is no argument passed in, and therefore $type gets the default value of "cappucino". But when the function is called the second and third times, $type now has the value of what was passed into the function.

kingandy’s picture

... Doesn't this usage conflict with hook_load()?

I know in theory anyone using hook_load to populate their module-specific node type is pretty much always going to use the %node wildcard for menu purposes - and I know this is not technically a hook, given that it has to be explicitly referenced by the hook_menu implementation rather than a general "anyone who declares this hook" type of thing - but still it feels like overloading. I check out hook_load every time I come to build a menu...

++Andy
Developing Drupal websites for Livelink New Media since 2008

mikey_p’s picture

If you name everything the same, then yes, it would conflict, but hook_load() isn't a true hook, it's technically a named callback that gets the replacement pattern for hook_ from the 'module' key of hook_node_info (see http://api.drupal.org/api/drupal/developer--hooks--node.php/function/hoo...). Also you can make your load function any name you want, so you don't have to map it to %my_module if your module is my_module.

In other words, neither the hook_ portion of hook_load() nor the name of your load function is tied to the name of your module (although both should probably contain the module_name as a prefix for namespacing reasons).

kenorb’s picture

If your %wildcard has the same name as module_wildcard_load, then yes, you'll end up with this:
#1102570: array_flip() [function.array-flip] issue in DrupalDefaultEntityController / entity.inc

rooby’s picture

Using the init_theme() function from your wildcard loader function can cause strange problems with your theme so it should be avoided.

You might be calling it unwittingly via some other function, for example using views execute_display() function makes a call to init_theme().

You should not really be executing views from a menu loader anyway but I came across it on a site so I thought I would mention it.

fehin’s picture

Subscribing