2

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.

  • Are you hardcoding the node id in the "second" method but it won't load the node? Or is the question why you aren't getting that node id in the "second" method? – Chaulky Mar 15 '11 at 2:28
  • apparently I misunderstood the question. seems like @kiamlaluno has a good answer on this one. – Chaulky Mar 15 '11 at 2:57
  • You were close, but no I meant that both manage to retrieve a reference to $node (and it's identical in both cases), however hook_page_preprocess receives that reference only in the first case. Therefore Drupal obviously does something more than just "node_load" when it sees a "%node", and that "something more" is what I'm looking for. – wildpeaks Mar 15 '11 at 3:08
3

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();
    }
    // …
  }
}
  • The problem with your answer is that "123" is just an example for the snipplet, but it's not meant to be a fixed value: it's the page callback who decides what "nid" to use. The question was about "what does Drupal do internally besides node_load when it receives a %node wildcard" given there is an obvious difference – wildpeaks Mar 15 '11 at 3:12
  • I replied to your question when it was titled "hook_preprocess_page: "node" is not defined unless path contains %node". I already replied about why in one case you get a node object, and in one case you don't. – kiamlaluno Mar 15 '11 at 10:08
  • I updated the answer, and explained why Drupal is able to return a node object in one case, but not in the other case, reporting exactly which code is responsible of Drupal behavior. – kiamlaluno Mar 15 '11 at 10:53
1

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

  • node_page_view is the one [meant to] tell Drupal what node to build the page from, not the fact that there is "%node" wildcard in the path. (As proof: imagine you have a path "some/%node/other/%node/path", how would Drupal guess otherwise which one to use ?) – wildpeaks Mar 15 '11 at 3:29
  • have you ever seen a path that does that? – Chaulky Mar 15 '11 at 3:32
  • None come to mind, but multiple wildcards of the same type are allowed afaik. Who knows, maybe they did hardcode to assume the first %node is meant to induce "it's a fullpage view of that node", but that would be inconsistent with the way wildcards are meant to work. – wildpeaks Mar 15 '11 at 3:41
  • Until I find the proper Drupal way of transmitting the node reference, I can use a hack like a static variable because for the real module, I will have only one reference per page anyway, but that's just not clean, so I'd rather find out the real reason than just "get it working one way or another". – wildpeaks Mar 15 '11 at 3:46
  • It is true, multiple placeholders of the same type are allowed, but Drupal will use only the first placeholder to populate $variables['node']. – kiamlaluno Mar 15 '11 at 11:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.