hook_preprocess_page: “node_load() + node_page_view()” versus “%node + node_page_view()”
Question
A basic node (nid "123") exists and a module registered two paths using hook_menu
:
first/%node
second
Both page callbacks use node_page_view($node)
to display a fullpage view of the node, except the second callback additionally uses node_load(123)
to get a reference to $node
.
Therefore http://localhost/first/123
and http://localhost/second
look the same as far as the user is concerned.
However hook_preprocess_page
receives the node
variable only with the first path, therefore Drupal obviously does something beyond node_load
when it sees a %node
wildcard, any idea what it is so that hook_preprocess_page
receives the reference to $node
in both cases ? (because handling everything internally in the second path produces much more friendly urls than redirecting to the first path from the second).
Example:
<?php
/**
* Implementation of hook_preprocess_page().
*/
function mymodule_preprocess_page(&$vars){
if (@isset($vars['node'])) echo '<h1>[mymodule.module] Node is defined</h1>';
else echo '<h1>[mymodule.module] Node is NOT defined</h1>';
}
/**
* Implementation of hook_menu().
*/
function mymodule_menu(){
return array(
'first/%node' => array(
'title' => 'First callback',
'page callback' => 'mymodule_page_first',
'page arguments' => array(1),
'access callback' => TRUE,
),
'second' => array(
'title' => 'Second callback',
'page callback' => 'mymodule_page_second',
'access callback' => TRUE,
),
);
}
/**
* Page callback for url "http://localhost/first/123".
*/
function mymodule_page_first($node){
echo '<h1>[mymodule.module] Function mymodule_page_first</h1><pre>';
var_dump($node);
echo '</pre>';
return node_page_view($node);
}
/**
* Page callback for url "http://localhost/second".
*/
function mymodule_page_second(){
$node = node_load(123); //the ID of an existing node
echo '<h1>[mymodule.module] Function mymodule_page_second</h1><pre>';
var_dump($node);
echo '</pre>';
return node_page_view($node);
}
(Source of file mymodule.module
)
My best/only hope so far is to dig in the source of node_page_view
and all functions it consequently triggers until I find what happens between the moment node_page_view
is called and the moment the module_invoke_all
which triggers the hook is called.
Solution
When mymodule_preprocess_page()
is invoked for the "first/%node" path, it gets the node object because the path is associated with a node from the module.
Drupal cannot associate a node object with a path like "/admin/content/node" (Drupal 6 path) because the path doesn't contain any reference to a node ID which is marked as node ID. Which node object should Drupal return for such paths, considering a Drupal website can contains hundreds of nodes?
Your question is "how can Drupal associate a node object in one case, but not in the other one?"
The answer is in the template_preprocess_page() code, which the function executed before the template page.tpl.php is rendered.
In that function, you will find the following code:
if ($node = menu_get_object()) {
$variables['node'] = $node;
}
The default parameters for menu_get_object() are $type = 'node', $position = 1, $path = NULL
, which means template_preprocess_page()
is asking for the node object that is associated with the placeholder in position #1, which is the value returned, for example, from arg(1)
.
Looking at the source of menu_get_object()
, you will notice that the code being executed is the following:
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
$router_item = menu_get_item($path);
if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
return $router_item['map'][$position];
}
}
Using the default arguments, the function will verify the loading function associated with the route item #1 is node_load()
. If the loading function is that, then it will return the value returned by the loading function to the calling function (in our case, template_preprocess_node()
).
To automatically get a node object, your menu callback should use a path like "second/%node" (which would be compatible with the default menu_get_object()
arguments). The only problem is that for such paths the node ID needs to be passed; if the URL is http://example.com/second, Drupal will show a 404 error page, as the placeholder require parameter that must be passed.
To resolve this problem, the module should define two menu callbacks: one associated with "second", and one associated with "second/%node". The first menu callback would not get a node object, while the second would get the node object (and mymodule_preprocess_page()
would get it too).
As alternative, you should change the code of mymodule_preprocess_page()
to something similar to:
/**
* Implementation of hook_preprocess_page().
*/
function mymodule_preprocess_page(&$vars){
// Check if the page being served is one of the pages handled by the module.
if (arg(0) == 'first' || arg(0) == 'second') {
if (isset($vars['node'])) {
// The page is a node page.
}
else {
$vars['node'] = mymodule_load_the_default_node();
}
// …
}
}
OTHER TIPS
When Drupal sees the '%node' wildcard in the url it knows the argument is referencing a node id, so automatically sets the $node
variable for you, making it available in hook_preprocess_page
. In your "second" menu callback, you should set the $vars['node']
variable. That way you can use it hook_preprocess_page
.
Reference: http://drupal.org/node/224170