function hook_menu

You are here

7 system.api.php hook_menu()
4.6 core.php hook_menu($may_cache)
4.7 core.php hook_menu($may_cache)
5 core.php hook_menu($may_cache)
6 core.php hook_menu()

Define menu items and page callbacks.

This hook enables modules to register paths in order to define how URL requests are handled. Paths may be registered for URL handling only, or they can register a link to be placed in a menu (usually the Navigation menu). A path and its associated information is commonly called a "menu router item". This hook is rarely called (for example, when modules are enabled), and its results are cached in the database.

hook_menu() implementations return an associative array whose keys define paths and whose values are an associative array of properties for each path. (The complete list of properties is in the return value section below.)

The definition for each path may include a page callback function, which is invoked when the registered path is requested. If there is no other registered path that fits the requested path better, any further path components are passed to the callback function. For example, your module could register path 'abc/def':

  function mymodule_menu() {
    $items['abc/def'] = array(
      'page callback' => 'mymodule_abc_view',
    );
    return $items;
  }

  function mymodule_abc_view($ghi = 0, $jkl = '') {
    // ...
  }

When path 'abc/def' is requested, no further path components are in the request, and no additional arguments are passed to the callback function (so $ghi and $jkl would take the default values as defined in the function signature). When 'abc/def/123/foo' is requested, $ghi will be '123' and $jkl will be 'foo'. Note that this automatic passing of optional path arguments applies only to page and theme callback functions.

In addition to optional path arguments, the page callback and other callback functions may specify argument lists as arrays. These argument lists may contain both fixed/hard-coded argument values and integers that correspond to path components. When integers are used and the callback function is called, the corresponding path components will be substituted for the integers. That is, the integer 0 in an argument list will be replaced with the first path component, integer 1 with the second, and so on (path components are numbered starting from zero). To pass an integer without it being replaced with its respective path component, use the string value of the integer (e.g., '1') as the argument value. This substitution feature allows you to re-use a callback function for several different paths. For example:

  function mymodule_menu() {
    $items['abc/def'] = array(
      'page callback' => 'mymodule_abc_view',
      'page arguments' => array(1, 'foo'),
    );
    return $items;
  }

When path 'abc/def' is requested, the page callback function will get 'def' as the first argument and (always) 'foo' as the second argument.

If a page callback function uses an argument list array, and its path is requested with optional path arguments, then the list array's arguments are passed to the callback function first, followed by the optional path arguments. Using the above example, when path 'abc/def/bar/baz' is requested, mymodule_abc_view() will be called with 'def', 'foo', 'bar' and 'baz' as arguments, in that order.

Wildcards within paths also work with integer substitution. For example, your module could register path 'my-module/%/edit':

  $items['my-module/%/edit'] = array(
    'page callback' => 'mymodule_abc_edit',
    'page arguments' => array(1),
  );

When path 'my-module/foo/edit' is requested, integer 1 will be replaced with 'foo' and passed to the callback function.

Registered paths may also contain special "auto-loader" wildcard components in the form of '%mymodule_abc', where the '%' part means that this path component is a wildcard, and the 'mymodule_abc' part defines the prefix for a load function, which here would be named mymodule_abc_load(). When a matching path is requested, your load function will receive as its first argument the path component in the position of the wildcard; load functions may also be passed additional arguments (see "load arguments" in the return value section below). For example, your module could register path 'my-module/%mymodule_abc/edit':

  $items['my-module/%mymodule_abc/edit'] = array(
    'page callback' => 'mymodule_abc_edit',
    'page arguments' => array(1),
  );

When path 'my-module/123/edit' is requested, your load function mymodule_abc_load() will be invoked with the argument '123', and should load and return an "abc" object with internal id 123:

  function mymodule_abc_load($abc_id) {
    return db_query("SELECT * FROM {mymodule_abc} WHERE abc_id = :abc_id", array(':abc_id' => $abc_id))->fetchObject();
  }

This 'abc' object will then be passed into the callback functions defined for the menu item, such as the page callback function mymodule_abc_edit() to replace the integer 1 in the argument array. Note that a load function should return FALSE when it is unable to provide a loadable object. For example, the node_load() function for the 'node/%node/edit' menu item will return FALSE for the path 'node/999/edit' if a node with a node ID of 999 does not exist. The menu routing system will return a 404 error in this case.

You can also make groups of menu items to be rendered (by default) as tabs on a page. To do that, first create one menu item of type MENU_NORMAL_ITEM, with your chosen path, such as 'foo'. Then duplicate that menu item, using a subdirectory path, such as 'foo/tab1', and changing the type to MENU_DEFAULT_LOCAL_TASK to make it the default tab for the group. Then add the additional tab items, with paths such as "foo/tab2" etc., with type MENU_LOCAL_TASK. Example:

// Make "Foo settings" appear on the admin Config page
$items['admin/config/foo'] = array(
  'title' => 'Foo settings',
  'type' => MENU_NORMAL_ITEM,
  // Page callback, etc. need to be added here.
);
// Make "Global settings" the main tab on the "Foo settings" page
$items['admin/config/foo/global'] = array(
  'title' => 'Global settings',
  'type' => MENU_DEFAULT_LOCAL_TASK,
  // Access callback, page callback, and theme callback will be inherited
  // from 'admin/config/foo', if not specified here to override.
);
// Make an additional tab called "Node settings" on "Foo settings"
$items['admin/config/foo/node'] = array(
  'title' => 'Node settings',
  'type' => MENU_LOCAL_TASK,
  // Page callback and theme callback will be inherited from
  // 'admin/config/foo', if not specified here to override.
  // Need to add access callback or access arguments.
);

Return value

An array of menu items. Each menu item has a key corresponding to the Drupal path being registered. The corresponding array value is an associative array that may contain the following key-value pairs:

  • "title": Required. The untranslated title of the menu item.
  • "title callback": Function to generate the title; defaults to t(). If you require only the raw string to be output, set this to FALSE.
  • "title arguments": Arguments to send to t() or your custom callback, with path component substitution as described above.
  • "description": The untranslated description of the menu item.
  • "page callback": The function to call to display a web page when the user visits the path. If omitted, the parent menu item's callback will be used instead.
  • "page arguments": An array of arguments to pass to the page callback function, with path component substitution as described above.
  • "access callback": A function returning TRUE if the user has access rights to this menu item, and FALSE if not. It can also be a boolean constant instead of a function, and you can also use numeric values (will be cast to boolean). Defaults to user_access() unless a value is inherited from the parent menu item; only MENU_DEFAULT_LOCAL_TASK items can inherit access callbacks. To use the user_access() default callback, you must specify the permission to check as 'access arguments' (see below).
  • "access arguments": An array of arguments to pass to the access callback function, with path component substitution as described above. If the access callback is inherited (see above), the access arguments will be inherited with it, unless overridden in the child menu item.
  • "file": A file that will be included before the page callback is called; this allows page callback functions to be in separate files. The file should be relative to the implementing module's directory unless otherwise specified by the "file path" option. Does not apply to other callbacks (only page callback).
  • "file path": The path to the directory containing the file specified in "file". This defaults to the path to the module implementing the hook.
  • "load arguments": An array of arguments to be passed to each of the wildcard object loaders in the path, after the path argument itself. For example, if a module registers path node/%node/revisions/%/view with load arguments set to array(3), the '%node' in the path indicates that the loader function node_load() will be called with the second path component as the first argument. The 3 in the load arguments indicates that the fourth path component will also be passed to node_load() (numbering of path components starts at zero). So, if path node/12/revisions/29/view is requested, node_load(12, 29) will be called. There are also two "magic" values that can be used in load arguments. "%index" indicates the index of the wildcard path component. "%map" indicates the path components as an array. For example, if a module registers for several paths of the form 'user/%user_category/edit/*', all of them can use the same load function user_category_load(), by setting the load arguments to array('%map', '%index'). For instance, if the user is editing category 'foo' by requesting path 'user/32/edit/foo', the load function user_category_load() will be called with 32 as its first argument, the array ('user', 32, 'edit', 'foo') as the map argument, and 1 as the index argument (because %user_category is the second path component and numbering starts at zero). user_category_load() can then use these values to extract the information that 'foo' is the category being requested.
  • "weight": An integer that determines the relative position of items in the menu; higher-weighted items sink. Defaults to 0. Menu items with the same weight are ordered alphabetically.
  • "menu_name": Optional. Set this to a custom menu if you don't want your item to be placed in Navigation.
  • "tab_parent": For local task menu items, the path of the task's parent item; defaults to the same path without the last component (e.g., the default parent for 'admin/people/create' is 'admin/people').
  • "tab_root": For local task menu items, the path of the closest non-tab item; same default as "tab_parent".
  • "position": Position of the block ('left' or 'right') on the system administration page for this item.
  • "type": A bitmask of flags describing properties of the menu item. Many shortcut bitmasks are provided as constants in menu.inc:

    • MENU_NORMAL_ITEM: Normal menu items show up in the menu tree and can be moved/hidden by the administrator.
    • MENU_CALLBACK: Callbacks simply register a path so that the correct information is generated when the path is accessed.
    • MENU_SUGGESTED_ITEM: Modules may "suggest" menu items that the administrator may enable.
    • MENU_LOCAL_TASK: Local tasks are menu items that describe different displays of data, and are generally rendered as tabs.
    • MENU_DEFAULT_LOCAL_TASK: Every set of local tasks should provide one "default" task, which should display the same page as the parent item.

    If the "type" element is omitted, MENU_NORMAL_ITEM is assumed.

For a detailed usage example, see page_example.module. For comprehensive documentation on the menu system, see http://drupal.org/node/102338.

Related topics

32 functions implement hook_menu()

Note: this list is generated by pattern matching, so it may include some functions that are not actually implementations of this hook.

aggregator_menu in modules/aggregator/aggregator.module
Implementation of hook_menu().
block_menu in modules/block/block.module
Implementation of hook_menu().
blogapi_menu in modules/blogapi/blogapi.module
blog_menu in modules/blog/blog.module
Implementation of hook_menu().
book_get_flat_menu in modules/book/book.module
Get the book menu tree for a page, and return it as a linear array.

... See full list

3 invocations of hook_menu()
drupal_uninstall_module in includes/install.inc
Calls the uninstall function and updates the system table for a given module.
menu_router_build in includes/menu.inc
Collect, alter and store the menu definitions.
system_get_module_admin_tasks in modules/system/system.module
Generate a list of tasks offered by a specified module.

File

developer/hooks/core.php, line 1340
These are the hooks that are invoked by the Drupal core.

Code

function hook_menu() {
  $items = array();

  $items['example'] = array(
    'title' => 'Example Page',
    'page callback' => 'example_page',
    'access arguments' => array('access content'),
    'type' => MENU_SUGGESTED_ITEM,
  );
  $items['example/feed'] = array(
    'title' => 'Example RSS feed',
    'page callback' => 'example_feed',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );

  return $items;
}

Comments

Note, that not only the title, but also the description don't need the t() wrapper in order to be translatable. A description, if given, provides it's translatability by default.

It took me an embarrassing amount of time to figure this out, so I thought I'd make note of it here. Leave the slash "/" off the end of the path on a parent item to display its child items in their proper hierarchy in the menu. e.g.:

Do this:

<?php
  $items
= array();

 

$items['admin/content/name'] = array(
   
'title' => 'Parent Menu Item',
    ...
etc ....
  );
 
$items['admin/content/name/set'] = array(
   
'title' => 'Child 1',
   
'type' => MENU_DEFAULT_LOCAL_TASK,
   
'weight' => -10,
  );
 
$items['admin/content/name/list'] = array(
   
'title' => 'Child 2',
    ...
etc ...
   );
   
  return
$items;
?>

Don't do this:

<?php
  $items
= array();

 

$items['admin/content/name/'] = array(
   
'title' => 'Parent Menu Item',
    ...
etc ....
  );
 
$items['admin/content/name/set'] = array(
   
'title' => 'Child 1',
   
'type' => MENU_DEFAULT_LOCAL_TASK,
   
'weight' => -10,
  );
 
$items['admin/content/name/list'] = array(
   
'title' => 'Child 2',
    ...
etc ...
   );
   
  return
$items;
?>

(Note the slash "/" at the end of "admin/content/name/" in the parent item.)

The doc says to name your custom menu if you don't want a MENU_NORMAL_ITEM to be put into the regular Navigation menu. Most people seem to use Primary Links, though. If you want your menu item to be there, use:

    'menu_name' => 'primary-links';

We had a case where the child item had to be in another menu than its parent. Example: try placing 'webshop' in the main menu and 'webshop/cart' in another menu (secondary links). Impossible. The only way we could fix this was to drag the link by hand. Or change 'webshop/cart' into 'cart'.

If you want to restrict access to a module's menu(s), and want to allow more than one role to access it, you have to define an access callback function. The following is a generic implementation that worked for me.

In the hook_menu function define the 'access callback' and 'access arguments' keys.

function your_module_menu() {
    $items['admin/settings/your_module'] = array(
        'title' => t('Your Module Name'),
        'description' => t('Menu's description.'),
        'page callback' => 'page_callback_funtion',
        'page arguments' => array('page callback arguments'),
        'access callback' => 'your_module_access',           // the function that validates access based on the user's role(s).
        'access arguments' => array(array(role1', 'role2')), // list of roles authorized - note nested array.
        'type' => MENU_NORMAL_ITEM,
    );
    return $items;
}

Add a new permission which can be assigned to roles to allow accessing your menu item.

function custom_forms_perm() {
    return array('Administer Your Module');
}

The access callback function checks each permission against the user's roles until a match is found or all permissions have been checked. It
returns TRUE if the user should be allowed access or FALSE if not.

/**
* Function to authenticate user for access to administer the contact forms.
*
* @param $perms
*   The nested array of roles allow access to this menu item.
*
* @return $allow
*    TRUE if the user is authorized else FALSE
*/
function custom_forms_access($perms) {
    global $user;  // required to access the user's roles

    $allow = false; // assume the user is NOT authorized

    // Check each permited role agest the user's roles until a match is found or all combinations are checked.
    foreach($perms as $perm) {
        foreach($user->roles as $role) {
            if($perm == $role) {
                $allow = true;  // user is authorized
                break;          // quit the inner loop
            }
        }
        if($allow) {
            break;  // quit the outer loop as soon a we know the user is authorized.
        }
    }
    return $allow;
}

An easier way to do this is to use user_access as your access callback. eg:

<?php
function your_module_perm() {
  return array(
'Administer Your Module');
}

function

your_module_menu() {
 
$items['admin/settings/your_module'] = array(
   
'title' => 'Your Module Settings',       // NOTE: t() not needed
   
'description' => 'Menu description',     // ... for these elements
   
'page callback' => 'page_callback_funtion',
   
'page arguments' => array('page callback arguments'),
   
'access callback' => 'user_access',                     // Check that the current user
   
'access arguments' => array('Administer Your Module'),  // ... has this permission.
   
'type' => MENU_NORMAL_ITEM,
  );
  return
$items;
}
?>

You can assign as many roles the "Administer Your Module" privilege as you want through the admin/user/permissions page. Using user_access also ensures consistent behavior such as letting user #1 (super-user) access your module's admin page.

mikeker is right that we should be using user_access().
However, his example did not work as expected for me.

So I used a modified version of edrubins's example. It's the same code apart from the hook_access() function.

<?php
function your_module_access($perms) {
 
$allow = false;

  foreach(

$perms as $perm) {
    if (
user_access($perm)) {
     
$allow = true;
      break;
    }
  }
  return
$allow;
}
?>

This hook is very rarely called and can result in lots of hairpulling if you forget to 'reload the modules page' everytime you make a change to the hook in your module. Putting a call to menu_rebuild() somewhere like in hook_init() in your module while you are developing it can be VERY useful and a huge timesaver. But once your module is ready, take it out. You dont want the menu system rebuilt on every page call on a live site lol.

It also gives you some other useful functions. Check it out here.

... crystaldawn's idea means you don't need to manually click the Rebuild Menus link every time (and remember to click it).

As opposed to thinking of the modules page, or menu_rebuild (which is a deeper function than most people are aware of,) know that the menu results are cached. Menus will be rebuilt whenever the cache expires, is emptied or when an administrative task calls for a rebuild (such as when a new module is enabled.)

This is the reason for the creative arguments system used in menu. Items are cached and callbacks are executed on page load. The application state during execution of your menu hook (for rebuild) may differ from the state during page load.

A fast way to force a rebuild is to empty the appropriate cache tables - during development of course. This is what the devel module does.

The documentation for hook_menu for Drupal 7 is really good! I think the system is very similar (if not exactly the same) to the D6 hook_menu system but the D7 docs are much better. Click on the Drupal 7 tab at the top of the page (or this link) to see!

It is worth mentioning that Drupal 6 limits the maximum path length to 7 elements. (Drupal 7 extends that to nine.)

hook_menu doesn't create links to items that use the %user argument. You have to use %user_uid_optional instead.

In order to get a menu item link with the user id or username in the URL, you have to use:

$items['user/%user_uid_optional/whatever'] = array(...);

Instead of

$items['user/%user/whatever'] = array(...);

If you need your menu item is expanded by default, set the key 'expanded':
'expanded' => TRUE,

I hit a roadblock trying to specify a menu_name. None of my links would appear in the correct custom menu, even though things seemed to be set up correctly. It turns out if you explicitly specify the menu type to be MENU_NORMAL_ITEM, which is the default, D6 will automatically put the link into the 'navigation' menu. To avoid this, simply don't set the type.

WRONG

  $items['example'] = array(
    'title' => 'Example',
    'page callback' => 'page_callback',
    'access arguments' => array('access content'),
    'menu_name'=>"menu-example",
    'type' => MENU_NORMAL_ITEM,
    );

RIGHT

  $items['example'] = array(
    'title' => 'Example',
    'page callback' => 'page_callback',
    'access arguments' => array('access content'),
    'menu_name'=>"menu-example",
    );

Simply will not work together..

Doesn't matter what type you specify, if both are set, it won't show up in the custom menu you specify :(

If you are going to try to use MENU_DEFAULT_TASK and and specify 'menu_name', you are going to have issues..

You can't just do it the way the docs here and menu_example say to. it doesn't work.. you either get 2 separate links with their own independent active states, or no links at all.

I've tried this every way imagineable, and you can't do it with 'menu_name'.. without monkeying in the drupal UI.

The way I did it was first setting the menu with my module:

<?php
   
//your own person profile
   
$items['profile'] = array(
       
'title' => 'Details',
       
'description' => 'User profile details',
       
'access arguments' => array('access content'),
       
'file' => 'includes/profile.pages.inc',
       
'page callback' => '_page_function',
       
'menu_name' => 'menu-profile-links'
   
);
                
   
$items['profile/locations'] = array(
       
'title' => 'Locations',
       
'description' => 'User locations',
       
'access arguments' => array('access content'),
       
'file' => 'includes/profile.pages.inc',
       
'page callback' => '_page_function_location',
       
'menu_name' => 'menu-profile-links'
   
);
   
$items['profile/schedule'] = array(
       
'title' => 'Schedule',
       
'description' => 'User schedule',
       
'access arguments' => array('access content'),    
       
'file' => 'includes/profile.pages.inc',   
       
'page callback' => '_page_function_schedule',
       
'menu_name' => 'menu-profile-links'
   
);
?>

that put it in the /admin/build/menu page under my "profile links" menu just fine.. but the first link is the parent ( /profile ) instead of the MENU_DEFAULT_LOCAL_TASK which was my ultimate goal -- i didn't specify that in PHP because doing so blasts it into space and it never renders anywhere ( because setting 'type' and 'menu_name' apparently just doesn't work )

So once i had the desired default as the parent, I just manually adjusted the links in the UI to put them all on the same level..

VERY FRUSTRATING but it works.

Something that doesn't seem to be written down anywhere is that the access callback is also used when deciding to display a NORMAL_MENU_ITEM as well as when deciding if the user has access to the page pointed to by the NORMAL_MENU_ITEM.

This can make writing custom user access routines a little tricky since you don't really have any way to check which context the user access routine is being called in.

I would propose that the Drupal 7 (or, 6 for that matter) folks add another key to the menu definition:

'menu access'

which is called to determine if the NORMAL_MENU_ITEM is to be displayed and reserved 'access callback' to determine if the page pointed to by the NORMAL_MENU_ITEM can be accessed.

This callback need not take arguments (although it should, just in case someone can figure out a use for them) in which case another key:

'menu access arguments'

can be an array of arguments as per 'access arguments'.

If the 'menu access' is omitted, but the 'menu access arguments' are supplied, then the 'user access' callback is taken with the 'menu access arguments'. If both are omitted, then the 'user access' arguments are called, which is the current behavior.

That way, no code breaks.

Best,

Dick Munroe

So if I'm understanding your comment (and my own observations correctly), 'access callback' (and it looks like 'title callback' too) gets called on every single page to determine if a link should be displayed and what it should say if so?

That seems really inefficient! My menu router tables tend to have thousands of entries, and we're calling two functions per entry regardless on every page? (Even when a hook is registered as MENU_CALLBACK, I'm finding!)

Implementing a 'menu access' (and possibly 'menu title') key would allow us cut out a lot of those calls.

When requesting an url of type MENU_CALLBACK with an ajax request, the theme is not used to render the output.
To apply a certain theme you have to change the global variable $custom_theme :

<?php
  $items
['admin_wave_file/ajax'] = array(
   
'page callback' => 'exemple_module_admin_wave_file_ajax_callback',
   
'access arguments' => array('access content'),
   
'access callback' => TRUE,
   
'type' => MENU_CALLBACK,
  );
?>
<?php
function exemple_module_admin_wave_file_ajax_callback($vague) {
  global
$custom_theme;
 
$custom_theme = 'my_theme_name';

 

//then we can render and output a view using the template files of the theme.
 
print views_embed_view('liste_wave_files', 'default', $vague);
}
?>

I hope this can help some people.
And if this is not the right way to do this, then reply to this post ;).

well...
you've provided access arguments when your access callback is TRUE

Remember that after create a hook_menu, you must clean cache.

Hook menu doesn't working? this is why!

I am trying to find what is considered valid menu names.
This code does not work.

<?php
function custom_module_menu() {
   
$items = array();   
   
$items['144'] = array(
       
'title' => 'Extension 144',
       
'page callback' => 'process_144',
       
'access callback' => TRUE,
       
'type' => MENU_CALLBACK,
    );
   
    return
$items;
}

function

process_144() {
    print
'processing 144';
}
?>

However, if the line is changed from $items['144'] = array( to $items['go/144'] = array( - it works as expected.

This works!

<?php
function custom_module_menu() {
   
$items = array();   
   
$items['go/144'] = array(
       
'title' => 'Extension 144',
       
'page callback' => 'process_144',
       
'access callback' => TRUE,
       
'type' => MENU_CALLBACK,
    );
   
    return
$items;
}

function

process_144() {
    print
'processing 144';
}
?>

I am trying to find a documentation that explains this behavior. Or am I doing something wrong?

Additionally, you can specify a 'plid' of the parent menu item if you don't want your menu item to appear at the root of the specified menu.

<?php
function mymodule_menu(){
 
$items['mypage'] = array(
   
'title' => 'Page name',
   
'description' => t('Some page'),
   
/* ... */
   
'menu_name' => 'main-menu',
   
'plid' => 1234, // 'mlid' of parent item
    /* ... */
 
);
  return
$items;
}
?>

I found this information on a comment for the D7 hook_menu documentation, but I've tested 'plid' successfully in D6 myself.