103

What are the fundamentals of implementing hook_menu()?

I would like to see the basics covered in a single question, to avoid having to answer the same similar but different questions over and over.

148

This information is valid for Drupal 6 and 7. In Drupal 8, hook_menu() has been replaced by new routing system. Below we implement hook_menu() in three simple steps.

Step one

Create an empty module following the instructions in How to create an empty module. In the code shown here, it's assumed the module is named helloworld.

Step two

Add the following code to the module file.

/**
 * Implements hook_menu().
 */
function helloworld_menu() {
  $items['hello'] = array(
    'title' => 'Hello world!',
    'page callback' => 'helloworld_page',
    'access callback' => TRUE,
  );

  return $items;
}

/**
 * Page callback for /hello.
 */
function helloworld_page() {
  return 'Hello world!';
}

Step three

Enable the module, and visit http://example.com/hello. (Replace example.com with the domain name for your server.)
You should see the message "Hello world!". That's it! You have a fully working hook_menu() implementation. What follows are various more advanced topics regarding hook_menu(). In particular, you may want to read about permissions, as the page above will be viewable by anyone.

Arguments

If you want to pass more data to the page callback, you can use page arguments to achieve this. Page arguments should be an array of arguments to pass on to the page callback. If an integer is used as an argument it will represent a part of the URL, starting from 0, incremented once for each slash (/). In the following example, this means the 0 will be turned into 'hello'.

function helloworld_menu() {
  $items['hello'] = array(
    'page callback' => 'helloworld_page',
    'page arguments' => array(0),
  );

  return $items;
}

function helloworld_page($argument1) {
  return $argument1;
}

Strings will be sent on verbatim, so array(0, 'world') could be used to get hello world out again.

function helloworld_page($argument1, $argument2) {
  return $argument1 . ' ' . $argument2;
}

"Wildcards" can be used to accept arbitrary data from the URL.

function helloworld_menu() {
  $items['hello/%'] = array(
    'page callback' => 'helloworld_page',
    'page arguments' => array(1),
  );

  return $items;
}

function helloworld_page($argument1) {
  return $argument1;
}

Visiting hello/world, $argument1 will equal world.

Argument autoloading

Often a URL argument will be the number identifying, for example, an entity. To avoid duplicating code that converts this ID into its corresponding object, Drupal support autoloading for "named" wildcards. When a named wildcard is used, Drupal will check for a function with the same name as the wildcard, suffixed by _load. If such a function is found, it will be called with the value of the value in the URL, and whatever is returned by the loader function will be passed to the page callback in place of the original value. Since Drupal already has such a function for loading nodes, node_load(), we can get nodes autoloaded and passed to the page callback.

function helloworld_menu() {
  $items['hello/%node'] = array(
    'page callback' => 'helloworld_page',
    'page arguments' => array(1),
  );

  return $items;
}

function helloworld_page($node) {
  return t('Hello node (ID = !nid)', array('!nid' => $node->nid));
}

Advanced autoloading

Sometimes, it will be necessary to auto-load more based on more than one argument. Since only the named argument is passed to the loader by default, one needs to explicitly tell Drupal which extra load arguments should be passed on to the loader. For example, to load a specific revision of a node, it is necessary to pass to node_load() a node ID and a revision ID. That can be accomplished by the following code.

function helloworld_menu() {
  $items['hello/%node/revision/%'] = array(
    'page callback' => 'helloworld_page',
    'page arguments' => array(1),
    'load arguments' => array(3),
  );

  return $items;
}

function helloworld_page($node) {
  return t('Hello node (ID = !nid, revision ID = !rid)', array('!nid' => $node->nid, '!rid' => $node->vid));
}

Permissions

'access callback' => TRUE, is necessary to make the simple example above viewable at all, but it's hardly ideal, as it allows no control what so ever. Anyone that tries to visit /hello will be granted access. The easiest way to provide some measure of control, is to provide a access callback, much like the page callback from above. The following code still allows access to anyone, but shows how to move the logic to a function called at access time, thereby allowing more complex logic.

/**
 * Implements hook_menu().
 */
function helloworld_menu() {
  $items['hello'] = array(
    'page callback' => 'helloworld_page',
    'access callback' => 'helloworld_access',
  );

  return $items;
}

/**
 * Access callback for /hello.
 */
function helloworld_access() {
  return TRUE;
}

This isn't necessarily the best way, as using a custom function will often needlessly duplicate code. A better way will be, most of the time, to use user_access(). Together the access callback it is possible to set access arguments. It is possible to require the page to be viewable from the users with the access user profiles permission with the following code.

/**
 * Implements hook_menu().
 */
function helloworld_menu() {
  $items['hello'] = array(
    'page callback' => 'helloworld_page',
    'access callback' => 'user_access',
    'access arguments' => array('access user profiles'),
  );

  return $items;
}

Since the access callback by default is user_access, it can be left out, as in the above code.

More advanced topics

The official hook_menu() documentation provides a lot more information about the most complex use-cases for the hook.

  • 3
    Awesome! Just for the sake of completeness, the title property is required for all items returned from hook_menu() – Clive Feb 16 '13 at 1:12
  • 1
    I know that the docs say so, but when I tested in D7, that was not the case. I could add it to the first example I guess, but I wanted to keep things to absolute minimum to make it as easy as possible. – Letharion Feb 16 '13 at 8:58
  • 1
    I'd say adding a title to a page is one of the absolute minimum things you could want to do with hook_menu() but it's your post :P Might be worth fixing up the syntax errors though (code examples 2, 4, 5 & 6), so people can copy and paste – Clive Feb 16 '13 at 10:11
  • 1
    Argh, I was initially diligent in actually running the code, but as time went I got sloppier and started copy pasting. Fixed the syntax errors, at least those I saw ;) Well, you're right that there should be a title of course, so I did add one to the initial example. – Letharion Feb 16 '13 at 11:44
  • 1
    How could I register a php file to a path using hook_menu? It is a custom written php file which includes drupal bootstrap, uses drupal session variables and accepts an argument which is a user id. – Елин Й. Mar 5 '13 at 11:55

Not the answer you're looking for? Browse other questions tagged or ask your own question.