C H A P T E R  29

images

The Menu System and the Path Into Drupal

by Robert Douglass

A versatile and easily understood architecture sets the stage for wide community involvement, as exemplified by Drupal’s menu system—responsible for associating paths on a Drupal site with just what the site returns to the visitor.

Open by design, Drupal’s simple and extensible architecture on the inside equates to high levels of participation on the community side.

This chapter looks at one of the features that leads to Drupal’s openness and flexibility. It is a dispatcher that takes the incoming Drupal path and maps it to a callback function. Along the way it resolves any access considerations and loads the data objects that are needed. Drupal calls this the menu system since the paths can also be used to build the visible navigation structures (i.e., menus) of the web site.1

Every web application solves the problems of dispatching URLs and resolving access in one way or another, so the mere existence of the menu system is not groundbreaking. What makes the menu system beautiful is its low barrier to entry and its power to reach into every corner of the code base to modify, extend, or replace what is already there.

Drupal’s Menu System by Example

A typical first task for a developer starting out with Drupal would be to integrate some existing code and have its output displayed within the context of a Drupal site. The requirements of this task include that the Drupal application gets fully loaded (including user authentication and making the database connection), that the page containing the output has its own Drupal path, and that this page be available in the visible navigation of the Drupal site.

Listing 29–1 shows the code needed to display an outrageous message within the context of a XHTML page. It lives within a hypothetical module called the outrageous module, and it fills the three requirements.

__________

1 The current menu system, first added in Drupal 6, was conceived of and implemented by Károly Négyesi with help from Peter Wolanin. Numerous others have contributed along the way.

Listing 29–1. An Outrageous Message

<?php

function outrageous_menu() {
  $items['outrageous'] = array(
    'title' => 'Outrageous message',
    'access callback' => TRUE,
    'page callback' => 'outrageous_message',
  );

  return $items;
}

function outrageous_message() {
  // Create an outrageous message. Based on a quote by Gill Davies.
  // t() is a wrapper that allows text to be localized.
  $message = t('A teddy bear is a cuddle with four paws on the end.'),

  // Get a formatted date.
  $time = date('M d, Y'),

  $page = array(
    '#markup' => "$time: $message",
  );

  return $page;
}
images

Figure 29–1. The code from Listing 29–1 defined a path, “outrageous,” which displays an outrageous message within a fully bootstrapped Drupal application. A link in the main navigational menu was created to access the “outrageous” page.

In Listing 29–1 the first function, outrageous_menu(), defines the path to the page, the title of the page, the access conditions, and the callback function that will generate the content of the page. This is all in the form of a simple PHP array. The second function, outrageous_message(), generates the output that is displayed in the content area as shown in Figure 29–1.

This code would live in a module that is a file named outrageous.module. The outrageous_menu() function is an implementation of a core Drupal hook called hook_menu().2 The naming convention states that if your module’s name is outrageous and the hook is generically called hook_menu, the function outrageous_menu() is the name of the function that will get called. Implementations of hook_menu() will get called whenever Drupal builds the menu router table in the database that is used when dispatching Drupal paths to callback functions. The pattern of calling functions based on the naming convention “module name” plus “hook name” is used extensively throughout Drupal.

__________

2 For more information on Drupal’s hooks, see api.drupal.org/api/group/hooks. For more information on hook_menu in particular, see api.drupal.org/api/function/hook_menu/6.

DRUPAL’S HOOKS

All incoming Drupal requests are directed at a single point of entry, which is the index.php file that lives in the base Drupal directory. This isn’t apparent in the example or on many Drupal sites, however, because behind the scene the web server is always rewriting the incoming URLs to point to index.php. Thus a URL of http://example.com will get rewritten to http://example.com/index.php. The rest of the URL is what is referred to as the Drupal path. It gets rewritten, too, so that http://example.com/outrageous will be represented internally as http://example.com/index.php?q=outrageous. Drupal refers to the q parameter as the path, and the path in this case is outrageous.

Given the code in Listing 29–1, a URL of http://example.com/outrageous will map to an internal path of outrageous, and this will match the menu item defined in the outrageous_menu() function based on the key of the array, $items['outrageous']. Once the dispatcher has matched the incoming path to the appropriate entry in the menu’s router table, a callback function will be sought. In this case it is defined in the $items array with the key 'page callback', and the outrageous_message() function will be called.

The $items array also defines a title that will be displayed on the page. Here the title is a hardcoded string, but a later example shows how this can be dynamically generated via a callback.

Access to this menu router item is set with the 'access callback' key. The value of this key can either be a Boolean (TRUE in this case means that everybody has access) or the name of a callback function, which must return a Boolean value.

You can see in Figure 29–1 that a visible navigation item appears in the navigation menu on the site. Although not covered here, this and the other navigation items can be configured, renamed, hidden, or moved around using Drupal’s administrative interface. The site administrator, not the coder, has final say in the matter.

Here are all the steps, from URL to Drupal page, laid out in order:

  1. http://example.com/outrageous gets rewritten to http://example.com/index.php?q=outrageous.
  2. The q parameter, outrageous, gets recognized as the Drupal path.
  3. Based on the path, the menu router item originally defined by $items['outrageous'] from the example code, now loaded from the database at runtime, is chosen to handle this page callback.
  4. An access check is done based on the 'access callback' item of the menu router. The value is TRUE, so access is allowed.
  5. The page title is set based on the 'title' item of the menu router. The title is now “Outrageous message.”
  6. The function named in the page callback parameter, outrageous_message, is invoked. This defines what appears in the content area of this particular page.
  7. Drupal builds the rest of the page using the presentation layer and the application’s configurations. This is where the logo, the navigation menu, and the shaded regions seen in Figure 29–1 come from.
images

Figure 29–2. Handling a request from URL to page output

A web developer who is new to Drupal will usually grasp the essence of this example right away and experience satisfaction that he can freely write code that gets included and executed within Drupal at the right time. All that is needed is a simple PHP array and some code to generate the output. The first barrier to entry has been overcome and Drupal has been extended.

MENU ROUTER ITEMS

The Never-ending Path

In Listing 29–1, the Drupal path was outrageous. A practical feature of the menu system is that this path is open ended. The same callback definition will also match and handle the path outrageous/dog/friend. The path gets broken into segments based on the slash, and each segment beyond outrageous will be available to the callback function as arguments. In Listing 29–2, the callback function has been rewritten to accept and use two arguments. If there are no arguments, it works the way it did in Figure 29–1.

Listing 29–2. The Outrageous Message with Two Arguments from the Drupal Path: http://localhost/outrageous/dog/friend

/**
 * Now with arguments that default to the original version of the message.
 */
function outrageous_message($animal = 'teddy bear', $noun = 'cuddle') {
  // Create an outrageous message template. Based on a quote by Gill Davies.
  $message = 'A %animal is a %noun with four paws on the end.';

  // Replace the %animal and %noun placeholders with the $animal and $noun
  // arguments.
  // The t() wrapper not only allows localization, it performs the
  // placeholder replacement.
  // t() also guarantees that $animal and $noun are plain text, thus
  // protecting against XSS attacks.
  $message = t($message, array('%animal' => $animal, '%noun' => $noun));  

  // Get a formatted date.
  $time = date('M d, Y'),

  $page = array(
    '#markup' => "$time: $message",
  );

  return $page;
}
images

Figure 29–3. The message is composed from the path segments in the Drupal path.

Structure of a Path

Being able to accept arbitrary arguments into the callback function is fine for some cases, but usually a more nuanced and exact approach is needed. A typical pattern in Drupal is to refer to data objects (called entities in Drupal-speak) using their primary key identifiers. The main object type for content in Drupal is called a node, which is just a generic term for content such as an article, blog post, image, or calendar event. Nodes have integer primary key identifiers. The paths in Table 29–1 all do various things with the node number 42.

Table 29–1. Various Node Paths and Their Actions

Path Action
node/42 Load and display node #42
node/42/edit Load and display the editing form for node #42
node/42/revisions Show the revision history for node #42

A pattern is emerging:

node + id + action

In each of the cases just shown the second segment of the path signifies the primary key id and the third segment specifies the action (with an implicit “view” action on node/42). One might be tempted to handle this pattern in a single callback function as shown in Listing 29–3.

Listing 29–3. Not Recommended: How Not to Handle the Node Paths

/**
 * $arg1 corresponds to the integer id.
 * $arg2 corresponds to the action.
 */
function node_callback($arg1, $arg2) {
  // If $arg1 is an integer, use it to load the node.
  if (is_numeric($arg1)) {
    $node = node_load($arg1);
  }

  // If we have a node, go about our business.
  if ($node) {
    if ($arg2 == 'edit') ...

    if ($arg2 == 'revisions') ...
  }
}

This approach has a lot of drawbacks:

  • It is hard to extend. What if you later want to write a module that handles node/42/send? If all node/integer/action type paths are handled by one function you’d either have to hack that function or replace it altogether.
  • All of the if {…} statements clutter up the code.
  • The whole picture gets more difficult when you consider this path, which Drupal also handles: node/add

This path displays a page with links to add new content. Now the second segment isn’t an integer at all, so clearly we need a way to differentiate between all of the different possibilities in paths.

The menu system handles this wide range of dynamic paths quite elegantly. The use of a wildcard notation simplifies not only the handling of dynamic paths but also the task of loading common objects. Consider the hook_menu implementation in Listing 29–4.

Listing 29–4. Recommended: How Drupal Handles the Node Paths

/**
 * Implementation of hook_menu().
 */
function node_menu() {
  $items['node/add'] = array(
    'title' => 'Add content',
    'page callback' => 'node_add_page',
    'access callback' => '_node_add_access',
  );
$items['node/%node'] = array(
    'title callback' => 'node_page_title',
    'title arguments' => array(1),
    'page callback' => 'node_page_view',
    'page arguments' => array(1),
    'access callback' => 'node_access',
    'access arguments' => array('view', 1),
  );

  $items['node/%node/edit'] = array(
    'title' => 'Edit',
    'page callback' => 'node_page_edit',
    'page arguments' => array(1),
  );

  return $items;
}

This is a slightly simplified version of the actual Drupal implementation for handling the node paths discussed previously. The original code can be found in Drupal’s node.module, and by the naming convention, the node_menu()3 function gets called whenever Drupal needs to build the menu router table.

Callback Functions

There are several things to observe here. The first item maps to the path 'node/add', and it uses the node_add_page() callback to generate a page that displays links to add various content to the system. To access this page at all, however, the user has to have the right access permissions. Whether the user has the right access permissions is decided by the function _node_add_access(), as specified by the 'access callback' key. In Listing 29–1 this bit was 'access callback' => TRUE, which is simply shorthand for saying that everyone has access. Now, in Listing 29–4 there is an example of access control being handed off to a dedicated callback function.

images New in 7 Drupal 7 has some new elements to modify the behavior of your menu items. The “delivery callback” can be defined to let a custom function handle rendering; you will usually be fine with the default, drupal_deliver_html_page. When replacing a default function with your own, you need to ensure that the replacement handles all the cases that the original function takes care of. The delivery callback function handles the cases of access denied, not found, site offline, and rendering the content. Another new element, context, can allow a menu item to be used as a contextual link (see Chapter 24 for an example). The rarely used “theme callback” and “theme arguments” elements allow you to provide a function to specify a different theme to be used when that menu item’s page loads. Read the API introduction for a complete list with brief explanations.4

__________

3 Drupal’s node_menu() function: api.drupal.org/api/drupal/modules--node--node.module/function/node_menu/7

4 api.drupal.org/api/drupal/modules--system--system.api.php/function/hook_menu/7

images

Figure 29–4. The path node/add leads to a page with a list of the various content types that can be created. The content on this page is generated from the function node_add_page().

The menu router item 'node/%node' has a wildcard segment in it, %node, which will match anything found in that segment. This item will match paths node/42, node/foo, and so forth. Although it would normally match node/add as well, it won’t in this example because there is a menu router item that defines node/add exactly, and the exact match takes precedence over wildcard matches.

Loader Functions

Whenever the menu system matches an incoming Drupal path to a wildcard segment with a notation like %node, it attempts to call a special loader function to automatically load an object for later use by any callback functions. The specific loader function is determined by a naming convention; when the percent sign is followed by a string (e.g., %foo), the system will look for a loader function, foo_load(). The loader function for %node is node_load(). Thus the path node/42 will invoke the function node_load(42), sending in the matched integer as an argument. The result is a fully loaded and built $node object with the primary id of 42. This object, in turn, becomes available for use as an argument to the various callback functions. In Listing 29–4 it is being passed on to the page callback that is responsible for generating the content output for this request. In Listing 29–5, look at the page callback keys for the 'node/%node' item.

Listing 29–5. Detail of the node/%node Menu Router Item

$items['node/%node'] = array(
...
  'page callback' => 'node_page_view',
  'page arguments' => array(1),
...
);

These keys tell the menu system to use node_page_view() as the main page callback and to pass in a single argument. The argument is addressed with the 'page arguments' key, and the array contains the segment from the path that should be used as the argument. This is array(1) in the example, so path segment 1 should be used. Path segment numbering starts with zero, so array(1) refers to the %node part of the path. As was just shown, this is a fully loaded and built $node object, and it is this object that will be passed into the callback function. The code that executes for path node/42 is functionally equivalent to this snippet:

// Invoke the _load function for node with the argument 42.
$node = node_load(42);
// Invoke the page callback function with the built $node object.
return node_page_view($node);

Let’s look again at the full definition for the 'node/%node' menu router, which is shown in Listing 29–6.

Listing 29–6. node/%node Menu Router Item

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

In Listing 29–1 our menu router item had a key, 'title', in which the title was set directly. The 'node/%node' menu router item has 'title callback' and 'title arguments' keys instead. These allow for the title of the page to be dynamically set. The callback function responsible for setting the title is node_page_title(), and it receives the same loaded $node object as node_page_view(), as specified by 'title arguments' => array(1). In this way the title of this page can be formed using dynamic information from the $node object.

In the same fashion, this menu router item’s access callback function takes some parameters, one of which is the dynamically loaded $node object. Based on the 'access callback' and 'access arguments' keys, code similar to the following snippet will be executed to determine whether the user issuing the request can access the 'node/4711' path:

// Segment 1 gets passed into the loader function for %node.
$node = node_load(4711);
// The loaded $node object gets passed to the access callback function.
return node_access('view', $node);

The 'node/%node' menu router item is a great example of how much functionality can be wired together with a relatively simple array of metadata to specify callback functions and their parameters. The menu system handles the loading of data objects and the invocation of callbacks to set the page title, checks if the current user is allowed to access this path, and generates the page content.

The developer uses the menu router item definitions to describe to the system what is supposed to happen, but how it all actually ends up happening is handled behind the scenes. The advantage of doing things this way—using data arrays to describe desired behavior—as opposed to simply writing the few lines of code to load and display the node directly, will be seen later when hook_menu_alter is discussed. The ability for developers to rewire menu router items, even those described in Drupal’s core, is fundamental to allowing contributed modules the chance to change anything and everything, if desired. Remember, “Do it in contrib” has to be a viable option, even for the most far-fetched ideas that come along.

Fitness

What happens when a path comes in that can be matched by more than one menu router item? How is the one true item chosen? Which item is the most fit to handle any given request? Consider the path node/12345/edit. This path can be matched by both of the following menu router items from Listing 29–4:

$items['node/%node']
$items['node/%node/edit']

The node/%node item is considered to be an ancestor of node/%node/edit because they have all but the last segment, edit, in common. When searching for a menu router item to handle a path, Drupal starts by calculating all of the possible ancestors of the path. Here is the complete theoretical ancestry of node/12345/edit:

node/12345/edit
node/12345/%
node/%/edit
node/%/%
node/12345
node/%
Node

Wildcard segments are simplified and represented by the % placeholder. There is not any guarantee that any of these menu router items are actually defined, but by generating the list of ancestors Drupal at least knows which router items to look for. This translates roughly into SQL like this:

SELECT * FROM menu_router
  WHERE path IN
  ('node/12345/edit',
  'node/12345/%',
  'node/%/edit',
  'node/12345',
 'node/%',
 'node')

This will find all of the possible menu router items that can handle a request, but which one is the best one to handle node/12345/edit? Here is where the concept of fitness comes in. Any menu router item path is broken down by segment and converted into a series of 1s or 0s. Discreet segments (such as node or edit) become 1s and wildcards become 0s. The resultant string of 1s and 0s is then interpreted as a binary integer to calculate that menu router item’s fitness. Applying these rules to the ancestors of path node/12345/edit produces the list in Table 29–2.

Table 29–2. Drupal Path Ancestry and Fitness. From drupal.org/node/109134

Path Fitness Base 10 Fitness Binary
node/12345/edit 7 111
node/12345/% 6 110
node/%/edit 5 101
node/%/% 4 100
node/12345 3 11
node/% 2 10
Node 1 1

The base ten fitness value is always saved along with any path in the menu router database table. Fitness is used for ordering the paths as shown in the following SQL query. Out of any set of menu router items, the one with the highest fitness will be used to handle a given request. The actual SQL generated by node/12345/edit thus becomes:

SELECT * FROM menu_router
  WHERE path IN
    ('node/12345/edit',
    'node/12345/%',
    'node/%/edit',
    'node/12345',
    'node/%',
    'node')
  ORDER BY fit DESC
  LIMIT 0, 1

Exactly one item from the ancestry of the actual path will be selected—the one with the highest fitness. If querying the menu_router table produces no results, a 404 Not Found page is generated. Understanding how fitness is calculated is usually not necessary for Drupal module developers, but it is one of the aspects of the menu system that makes it a beautiful architecture.

Hopefully this exercise of deconstructing Drupal’s menu router paths has illustrated some of the tools available to developers for extending a Drupal application. The ability to define new paths, wire them to callbacks, preload objects, and manage access, all within the Drupal application, gives one enough power to add virtually any new feature. In the next section, I’ll show you how existing features can be modified using hook_menu_alter.

Modifying Existing Router Items

The examples so far have been focused on extending Drupal. What does one do to modify or replace existing functionality? How does a developer alter core Drupal behavior without resorting to changing the core Drupal code itself? Drupal lays bare its entire menu router to any module to be able to change. The mechanism for doing this is called hook_menu_alter. Modules may implement their own hook_menu_alter function and simply change any of the defined menu router items.

Let’s go back to the first example in this chapter, the outrageous message. If someone comes up with a custom implementation for developing outrageous messages, but doesn’t want to or isn’t able to convince the maintainer of the outrageous module that the new implementation is better, he can implement their own module that alters the way outrageous messages get created (see Listing 29–7).

Listing 29–7. Review of the Outrageous Module

<?php

function outrageous_menu() {
  $items['outrageous'] = array(
    'title' => 'Outrageous message',
    'access callback' => TRUE,
    'page callback' => 'outrageous_message',
  );

  return $items;
}

function outrageous_message() {
  // Create an outrageous message. Based on a quote by Gill Davies.
  // t() is a wrapper that allows text to be localized.
  $message = t('A teddy bear is a cuddle with four paws on the end.'),

  // Get a formatted date.
  $time = date('M d, Y'),

  $page = array(
    '#markup' => "$time: $message",
  );

  return $page;
}

The moreoutrageous_menu_alter() function in Listing 29–8 implements hook_menu_alter by following the naming convention of modulename + hook name, and it receives the entire menu router item table in the form of an $items array.

Listing 29–8. The Moreoutrageous Module

<?php
function moreoutrageous_menu_alter(&$items) {
  // Change the callback function for the path 'outrageous'.
  $items['outrageous']['page callback'] = 'moreoutrageous_message';
}

function moreoutrageous_message() {
  // Juicy (mis)quote by Károly Négyesi.
  $message = t('I am a machine for turning orange juice into Drupal patches.'),

  // Get a formatted date.
  $time = date('M d, Y'),
$page = array(
    '#markup' => "$time: $message",
  );

  return $page;
}

The $items array is the sum total of all menu router items returned by all of the hook_menu implementations from all enabled modules. Since the array is passed in by reference any alterations made to it will persist beyond the scope of the function. The code changes the 'page callback' value that was designated in the outrageous_menu() function in the outrageous module and specifies that it should instead be handled by the moreoutrageous_message() function in the moreoutrageous module.

images

Figure 29–5. A more outrageous message, thanks to hook_menu_alter

Building the menu router table is a relatively expensive operation. First hook_menu is invoked, which results in all modules being checked for implementing functions. Once the menu router items from all of the modules are collected and concatenated into a large array, hook_menu_alter is invoked, affording each module the chance to alter that array. Fortunately this is not done on every page request. The menu router table is rebuilt whenever something about the fundamental state of the Drupal application changes. Examples include installing new modules, defining new content types, or creating new views of content. Once the final array of all menu router items is built it is persisted in the menu_router database table. Subsequent page requests are able to query this table to find callbacks and router items until the next occasion that necessitates a total rebuild occurs.

Summary

The menu system is a tool that offers a great deal of control and opportunity. It is easy to grasp and developers with a wide range of skills and abilities are able to use it effectively. It isn’t the only tool available for extending or altering Drupal. Other tools exist if your goal is to alter a form or extend the definition or behavior of nodes or content types. Used together, these tools allow for the enhancement and alteration of Drupal’s core functionality and behavior while avoiding the need to hack the core code. By embracing this need for an architecture that is open and accessible, Drupal supports a very significant amount of community participation, has become the choice of platforms for hundreds of thousands of people and organizations, and has enabled thousands of developers to become open source contributors.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.99.174