There are a few different ways we could approach this (leaving Views aside here). One way is to build a single menu item and return the data based on an argument in the URL.
<?php
function mymodule_menu() {
$items[‘mymodule/report’] = array(
‘title’ => ‘My report’,
‘page callback’ => ‘mymodule_report’,
‘access arguments’ => array(‘view my report’),
);
return $items;
}
/**
* Here is our report function
*
* @return formatted string
*/
function mymodule_report() {
$report = array();
// code to build the report
// check the 2nd argument from the url
$format = arg(2);
switch (strtolower($format)) {
case ‘csv’:
return theme(mymodule_report_csv, $report);
case ‘json’:
return theme(mymodule_report_json, $report);
default:
return theme(mymodule_report_html, $report);
}
}
?>This works well enough; it allows us to get the data we are looking for by heading over to http://example.com/mymodule/report, while allowing for a link to the csv version at http://example.com/mymodule/report/csv, and it allows the machines to get the data at http://example.com/mymodule/report/json. What happens, though, when we want to use the raw, un-themed data from mymodule_report in a different function or module? I could copy the logic and put it in my own custom module (bad), or I could call the mymodule_report function and pass the format as JSON and deal with that formatting (also bad).
Here, let's take the calls to the theme functions out of our report function.
<?php
function mymodule_menu() {
$items[‘mymodule/report’] = array(
‘title’ => ‘My report in HTML’,
‘page callback’ => ‘theme’,
'page arguments' => array('testmod_testpage_html', module_invoke('testmod', 'testpage')),
‘access arguments’ => array(‘view my report’),
);
$items[‘mymodule/report/csv’] = array(
‘title’ => ‘My report in CSV’,
‘page callback’ => ‘theme’,
'page arguments' => array('testmod_testpage_csv', module_invoke('testmod', 'testpage')),
‘access arguments’ => array(‘view my report’),
);
$items[‘mymodule/report/json’] = array(
‘title’ => ‘My report in JSON’,
‘page callback’ => ‘theme’,
'page arguments' => array('testmod_testpage_json', module_invoke('testmod', 'testpage')),
‘access arguments’ => array(‘view my report’),
);
return $items;
}
/**
* Here is our report function
*
* @return array
* A keyed array of data to be formatted later.
*/
function mymodule_report() {
$report = array();
// build the report
return $report;
}
?>There, now we’ve made our report function more reusable by returning just the data we are asking for and allowing the formatting and theming to be handled by the theme layer. This is an acceptable method, but now we’ve added quite a bit of redundant code in building each of those menu items. Let's see about using a single menu item for this report since, really, it's only a single piece of functionality. For this we are going to use the ‘delivery callback’ variable from the hook_menu().
<?php
function mymodule_menu() {
$items['mymodule/report'] = array(
'title' => 'My report',
'page callback' => 'mymodule_report',
'delivery callback' => 'mymodule_deliver',
'access arguments' => array(‘view my report’),
);
return $items;
}
?>There, now our menu item looks very close to what it did at the beginning, with the exception of that ‘delivery callback’ variable we have added in there. The delivery callback tells Drupal what should be done with the data after it has been gathered from the page callback. This is great, because now we don’t have to put calls to the theming functions in our report function and we don’t need all the redundant code in our menu.
So what does the delivery callback look like? Let’s take a look:
<?php
function mymodule_deliver($data) {
// here we could do a number of things including checking for arg(2) to see
// what format the user is looking to deliver this in.
switch(arg(2)) {
case 'json':
drupal_json_output($data);
drupal_exit();
case 'csv':
return theme('mymodule_report_csv', $data);
default:
$output = theme('mymodule_report_html', $data);
return drupal_deliver_html_page($output);
}
}
?>This looks a lot like our mymodule_report function before, where we decide in what format the data should be returned. In this example we are using the second argument from the URL arg(2)to determine this. The rest is pretty self-explanatory; if the argument is JSON, we send the data through drupal_json_output; if it is 'csv', we return the csv theme (like before); and if arg(2) is anything else (including nothing), we return the HTML version. The documentation says that "this function is called even if the access checks fail," which is very cool because it allows us to return an access denied response in the correct format. We've skipped over this in our example, but I recommend looking at the code for drupal_deliver_html_page for ideas on how to handle this case.
So really, what does all this buy us? We've removed the theming from our report function, adding to code reusability, we've eliminated some redundancy through not creating menu items for each themed output, and we've built a second level to the Drupal routing where we are able to decide in what format we would like our response to be.
