Drupal 7 Page Delivery Callbacks

Friday, August 21, 2015 - 11:11

Or, how you can render a Drupal page with an entirely different template.

I recently had a requirement where I needed to get Drupal to render a single page of HTML that was entirely separate from the normal page layout of a site. This was actually part of an API callback, but this got me involved in looking at how delivery callbacks work in Drupal 7. It isn't necessary to create a new theme just for the job of rendering a single page with some custom HTML, especially as Drupal has mechanisms to provide this built in.

If you have done much Drupal programming you have probably used a delivery callback but not actually realised it. Calling functions like drupal_access_denied() and drupal_not_found() will issue a 403 or 404 page respectively, but they work by firing the delivery callback of the page, which renders and then sends the output of the page to the browser. These functions work by calling the drupal_deliver_page() function, which in turn triggers the currently selected delivery callback function to be called. By default, this is drupal_deliver_html_page(), which renders HTML output using the current theme, adds the appropriate headers, and sends this content to the browser.

There are two ways in which the delivery callback can be altered, and which one you use depends on your needs.

Implementing hook_page_delivery_callback_alter()

Just before the drupal_deliver_page() finally calls drupal_deliver_html_page(), installed modules are given the chance to alter the delivery callback via the hook_page_delivery_callback_alter() hook. This hook allows you to alter the delivery callback function depending on certain parameters. The following show the hook being used to alter the delivery callback function for the front page of the site. We'll come onto the contents of custom delivery functions later.

1
2
3
4
5
6
7
8
/**
 * Implements hook_page_delivery_callback_alter().
 */
function mymodule_page_delivery_callback_alter(&$delivery_callback) {
  if (drupal_is_front_page()) {
    $delivery_callback = 'mymodule_deliver_page';
  }
}

This hook can be used to alter more than just the page template. Because it is called after the page callbacks have been run, but before the page (and most, if not all of content on it) has been rendered it is a good way of altering certain things before the page is rendered. For example, I have found that this hook is a good way of changing the menu location of a page. Take the following silly example, this moves the Cron settings page so that it appears to be sat under the Modules page.

1
2
3
4
5
6
7
8
/**
 * Implements hook_page_delivery_callback_alter().
 */
function mymodule_page_delivery_callback_alter(&$delivery_callback) {
  if (implode(arg(), '/') == 'admin/config/system/cron') {
    menu_tree_set_path('management', 'admin/modules');
  }
}

This is not useful in itself, but getting certain pages to appear under certain menu items is one of those common Drupal problems that's hard to solve correctly. This hook provides a good way of doing this.

Menu delivery callbacks

The second method of providing a custom callback is to use the delivery callback attribute of a menu hook item. This option will mean that the output of your menu callback will be passed to this delivery callback automatically without having to also define any hooks. The following example shows this in a menu hook and a very simple page callback function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mymodule_menu() {
  $items['mymodule/page'] = array(
    'title' => 'A Page',
    'page callback' => 'mymodule_return',
    'access callback' => TRUE,
    'delivery callback' => 'mymodule_deliver_page',
    'type' => MENU_CALLBACK,
  );
 
  return $items;
}
 
function mymodule_return() {
  return '<p>' . t('The current timestamp is') . ' : ' . time().'</p>';
}

This menu page callback doesn't have to do anything special, but all of the content it produces is fed through to the delivery callback function. There also doesn't need to be anything special about the page callback function, it can return a string or a Drupal render array in the same way as always. In it's current state the code above code will produce a white screen and the error message "callback mymodule_deliver_page not found: mymodule/page" will be seen in the watchdog tables. This is because the deliver callback function is missing, so let's look at creating one.

Creating a delivery callback function

In order to get the above code examples working you will need to create a delivery callback function. In it's simplest terms a delivery callback function must simply print out the contents of the page. The following, although simplistic, fulfills this requirement.

1
2
3
function mymodule_deliver_page($page_callback_result) {
  print render($page_callback_result);
}

With that in place the above code examples will now work. Although the pages will be quite blank as this just delivers the content and no other HTML or styles.

Before creating your own delivery callback it's important to understand how Drupal works in this regard. The delivery callback function for HTML pages in Drupal 7 is called drupal_deliver_html_page(). This function is a little too long to reproduce here, but it works by adapting to different types of variables that Drupal returns during the page rendering process. If the page callback returns an integer then the function will see this as an error and will use the error code to return the correct header and page for that error. This is a useful effect as it means that to produce an access denied page you just need to return the MENU_ACCESS_DENIED constant from your page callback. The MENU_ACCESS_DENIED is an integer constant that means Drupal will return a 403 HTTP status code and an access denied page. If the page callback returns a string or an array then this is passes to the drupal_render_page() function, which renders the page as normal.

We can reduce the drupal_deliver_html_page() function down to the most basic of requirements and render our page content in a single template. The following will issue a couple of headers before rendering the content of the page within a simple HTML structure. The drupal_page_footer() function at the end is a Drupal utility function that writes sessions and calls any shutdown functions, which should always be called at the end of the page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function mymodule_deliver_page($page_callback_result) {
  if (is_null(drupal_get_http_header('Content-Type'))) {
    drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
  }
 
  // Send appropriate HTTP-Header for browsers and search engines.
  global $language;
  drupal_add_http_header('Content-Language', $language->language);
 
  // Render page and content
  print '<html><head></head><body>';
  print render($page_callback_result);
  print '</body></html>';
 
  drupal_page_footer();
}

To make this more maintainable you would obviously wrap this in a theme function, but this example works as a simple example. I used a version of the above code (which passed output through a template file) to generate the output needed for the API callback. The upshot here is that if the output needs to change for whatever reason then all that needs altering is the template files, everything else is handled by the module.

Also remember that we are essentially bypassing the Drupal access denied logic here so you need to make very sure that the output you return is appropriate. This is especially important if you are using the hook_page_delivery_callback_alter() hook to alter the page delivery. You can quite easily open up unpublished content or user profile details so anonymous views, so be careful!

Returning JSON Output

Perhaps the most useful thing to do with the delivery callbacks is to return output in different formats. This can be easily done by slightly adjusting the mymodule_deliver_page() function so that it listens out for different types of headers, adapting the output slightly for each mime type encountered.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
function mymodule_deliver_page($page_callback_result) {
  $content_type = drupal_get_http_header('Content-Type');
 
  if (is_null($content_type)) {
    drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
  }
 
  // Send appropriate HTTP-Header for browsers and search engines.
  global $language;
  drupal_add_http_header('Content-Language', $language->language);
 
  switch ($content_type) {
    case 'application/json':
      print json_encode($page_callback_result);
      break;
    case 'application/xml':
      print '<?xml version="1.0" encoding="UTF-8"><root>' . render($page_callback_result) . '</root>';
      break;
    default:
      print '<html><head></head><body>';
      print render($page_callback_result);
      print '</body></html>';
  }
  drupal_page_footer();
}

All we need to do now in order to generate JSON output is to ensure that the page callback sets an 'application/json' header.

1
2
3
4
function mymodule_return() {
  drupal_add_http_header('Content-Type', 'application/json');
  return array(t('The current timestamp is') . ' : ' . time());
}

This will generate the following output.

["The current timestamp is : 1440147347"]

We can also switch over to XML output with a couple of simple changes.

1
2
3
4
function mymodule_return() {
  drupal_add_http_header('Content-Type', 'application/xml');
  return t('The current timestamp is') . ' : ' . time();
}

This makes generating simple API services in Drupal quite easy. Drupal assumes that your content will be HTML, but with a few lines of code you can make Drupal return content in any format you like.

Category: 
philipnorton42's picture

Philip Norton

Phil is the founder and administrator of #! code and is an IT professional working in the North West of the UK.
Google+ | Twitter

Comments

Great post. Nice example. Clear and clean explanation.
Thx

Add new comment