hook_preprocess_page: “node_load() + node_page_view()” versus “%node + node_page_view()”

drupal.stackexchange https://drupal.stackexchange.com/questions/789

  •  16-10-2019
  •  | 
  •  

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.

Was it helpful?

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

Licensed under: CC-BY-SA with attribution
Not affiliated with drupal.stackexchange
scroll top