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

Define menu items and page callbacks.

This hook enables modules to register paths, which determines whose requests are to be handled. Depending on the type of registration requested by each path, a link is placed in the the navigation block and/or an item appears in the menu administration page (q=admin/menu).

Drupal will call this hook twice: once with $may_cache set to TRUE, and once with it set to FALSE. Therefore, each menu item should be registered when $may_cache is either TRUE or FALSE, not both times. Setting a menu item twice will result in unspecified behavior.

This hook is also a good place to put code which should run exactly once per page view. Put it in an if (!may_cache) block.


$may_cache: A boolean indicating whether cacheable menu items should be returned. The menu cache is per-user, so items can be cached so long as they are not dependent on the user's current location. See the local task definitions in node_menu() for an example of uncacheable menu items.

Return value

An array of menu items. Each menu item is an associative array that may contain the following key-value pairs:

  • "path": Required. The path to link to when the user selects the item.
  • "title": Required. The translated title of the menu item.
  • "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.
  • "callback arguments": An array of arguments to pass to the callback function.
  • "access": A boolean value that determines whether the user has access rights to this menu item. Usually determined by a call to user_access(). If omitted and "callback" is also absent, the access rights of the parent menu item will be used instead.
  • "weight": An integer that determines relative position of items in the menu; higher-weighted items sink. Defaults to 0. When in doubt, leave this alone; the default alphabetical order is usually best.
  • "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_ITEM_GROUPING: Item groupings are used for pages like "node/add" that simply list subpages to visit.
    • MENU_CALLBACK: Callbacks simply register a path so that the correct function is fired when the URL is accessed.
    • MENU_DYNAMIC_ITEM: Dynamic menu items change frequently, and so should not be stored in the database for administrative customization.
    • MENU_SUGGESTED_ITEM: Modules may "suggest" menu items that the administrator may enable.
    • MENU_LOCAL_TASK: Local tasks are rendered as tabs by default.
    • MENU_DEFAULT_LOCAL_TASK: Every set of local tasks should provide one "default" task, that links to the same path as its parent when clicked.

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

For a detailed usage example, see page_example.module.

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_menu in modules/book/book.module
Implementation of hook_menu().

... See full list

2 invocations of hook_menu()
_menu_append_contextual_items in includes/menu.inc
Account for menu items that are only defined at certain paths, so will not be cached.
_menu_build in includes/menu.inc
Build the menu by querying both modules and the database.


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


function hook_menu($may_cache) {
  global $user;
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'node/add/blog',
      'title' => t('blog entry'),
      'access' => user_access('maintain personal blog'),
    $items[] = array(
      'path' => 'blog',
      'title' => t('blogs'),
      'callback' => 'blog_page',
      'access' => user_access('access content'),
      'type' => MENU_SUGGESTED_ITEM,
    $items[] = array(
      'path' => 'blog/' . $user->uid,
      'title' => t('my blog'),
      'access' => user_access('maintain personal blog'),
      'type' => MENU_DYNAMIC_ITEM,
    $items[] = array(
      'path' => 'blog/feed',
      'title' => t('RSS feed'),
      'callback' => 'blog_feed',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK,
  return $items;


sriramanv’s picture

I am worked with hook_menu but i found out theme is not applying after selecting the menu and my code as follows...

function sample1_menu() {
$items = array();
$items['admin/settings/shop_item'] = array(

'title' => t('Shop Details'),
'description' => t(' Mobile Details.'),
'page callback' => 'shopping1_details',
'access arguments' => array('access contents'),
'type' => 'MENU_CALLBACK',

return $items;

function shopping1_details(){
echo "hello";

nicksanta’s picture

In regard to this specific problem, I think you've made a typo on the access arguments element, I think it should be 'access content'. Also in 'type', MENU_CALLBACK is a constant, and should not be enclosed in quotes.

Simon Naude’s picture

Can somebody please help me in regard to using the 'callback arguments' key in an $items array (D5) ?

What I'm trying to do is reuse a function so that it determines where to direct to, based on an argument/ parameter. (Only 1 line of code needs to change, depending on a string parameter ; I don't want to duplicate the function if possible).

What I want to know is how to access the callback arguments from within the called function.

Here is my D5 code (see comments) :

 * Implementation of hook_menu()
function askasolicitor_menu($may_cache) {
  /* drupal_add_css(drupal_get_path('module', 'askasolicitor') .'/askasolicitor.css'); */
  drupal_add_js(drupal_get_path('module', 'askasolicitor') .'/askasolicitor.js');
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'find-a-solicitor',
      'title' => t('Find a Solicitor'),
      'description' => t('Search Form results for a solicitor'),
      'callback' => 'askasolicitor_fas',
      'callback arguments' => array( 'solicitor' ) ,
      'access' => user_access('view askasolicitor'),
    $items[] = array(
      'path' => 'find-a-surveyor',
      'title' => t('Find a Surveyor'),
      'description' => t('Search Form results for a surveyor'),
      'callback' => 'askasolicitor_fas',
      'callback arguments' => array( 'surveyor' ) , 
      'access' => user_access('view askasolicitor'),
  return $items;

function askasolicitor_fas($name = NULL) {
  if (isset($_GET['name']) AND ($_GET['name'] == 'Any' OR $_GET['name'] == '')
      AND isset($_GET['county']) AND ($_GET['county'] == 'Any' OR $_GET['county'] == '')
      AND isset($_GET['town']) AND ($_GET['town'] == 'Any' OR $_GET['town'] == '')
      AND isset($_GET['postcode']) AND ($_GET['postcode'] == 'Any' OR $_GET['postcode'] == '')
      AND isset($_GET['lawarea']) AND ($_GET['lawarea'] == 'Any' OR $_GET['lawarea'] == '')) {
	// use callback arguments to determine what page to go to (how?) ;
	/* if 'solicitor', go to page 'Law_Firms'
	 * else if 'surveyor' go to page 'surveyors' 
  else {
    $nodetype = 'law_firm_office' ; // solicitors (default)
    // if callback arg is 'surveyor', change node type to 'surveyor'
    $query = "SELECT DISTINCT(n.nid) FROM {node} n
	      LEFT JOIN {content_field_address_line2} town ON town.nid = n.nid
	      LEFT JOIN {content_field_address_line3} county ON county.nid = n.nid
	      LEFT JOIN {content_field_fullpostcode} postcode ON postcode.nid = n.nid
	      LEFT JOIN {term_node} tn ON n.nid = tn.nid
	      WHERE n.type = '$nodetype' AND status = 1";
    if (isset($_GET['name']) AND $_GET['name'] != 'Any') {
      $query .= " AND n.title LIKE '%". check_plain($_GET['name']) ."%'";
    if (isset($_GET['county']) AND $_GET['county'] != 'Any') {
      $query .= " AND county.field_address_line3_value LIKE '%". check_plain($_GET['county']) ."%'";
    if (isset($_GET['town']) AND $_GET['town'] != 'Any') {
      $query .= " AND town.field_address_line2_value LIKE '%". check_plain($_GET['town']) ."%'";
    if (isset($_GET['postcode']) AND $_GET['postcode'] != 'Any') {
      $query .= " AND postcode.field_fullpostcode_value LIKE '%". check_plain($_GET['postcode']) ."%'";
    if (isset($_GET['lawarea']) AND $_GET['lawarea'] != 'Any') {
      $query .= " AND tn.tid = ". check_plain($_GET['lawarea']);
    $query .= " ORDER BY n.title ASC";
    $results = db_query($query);
    $output = '';
    $count = 0;
    while ($data = db_fetch_object($results)) {
      $node = node_load($data->nid);
      $output .= node_view($node, TRUE);
    $output .= theme('pager', NULL, 10);
    $header = '

'. $count .' law firms match your search results.

'; if ($count > 30) $header .= '

You can refine these results further by being more specific in you search.

'; return $header.$output; } }