Today I have spent a few minutes trying to understand how Drupal renders a Not found page when accessing a node that does not exist. After going through the node.module code I couldn’t find anything.

This is one of the most frustrating things with Drupal, you need to know pretty well how it works internally to understand what is happening and where to look because there is some magic here and there.

It turns out that the menu system is one of the places where some of this magic is implemented to show a Not found page when an object cannot be retrieved from the database. Basically, the menu system implements a way for automatically load objects from the database to display or edit. If the object cannot be retrieved then you get the error. Let’s see how this works.

When you write a module you register the mapping between paths and function calls using the hook_menu function. For instance, for the node module you have

function node_module() {
...
  $items['node/%node'] = array(
    'title callback' => 'node_page_title',
    'title arguments' => array(1),
    'page callback' => 'node_page_view',
    'page arguments' => array(1),
    'access callback' => 'node_access',
    'access arguments' => array('view', 1),
    'type' => MENU_CALLBACK);
  ...
}

This is adding a mapping for the path node/%node to the node_page_view function. The arguments that are passed are defined by the array page_arguments. As you see the array contains one single element: 1.

1 relates to the position 1 in the array generated from the path. In our case:

  • 0 -> node,
  • 1-> %node

% is used as a wildcard, so it will match anything  (except special characters like / that is used as separator) after node/. When instead of  % you use %something some magic is performed: a call to something_load(%something) is performed. Imagine that you tap into the url http://www.example.com/node/45, then instead of passing 45 to the node_page_view function, it will pass node_load(45). Amazing, isn’t it? Furthermore, if node_load(45) fails to load an object from the database because there is no node with a nid (the primary key of the node table) of 45 a Not found page is returned. That is the kind of tricks that are difficult to spot but can increase productivity.

In case you need to pass other parameters to the something_load function, you can add a ‘load arguments’ entry to the hash for a path with the extra arguments to the load function as an array.

More information in the Drupal documentation.

Handling Drupal forms submission is dead simple. You basically write a three functions: a form generator, a form validator and a form submitter.

The form generator is called everytime the form needs to be built and it can be used to populate fields default values or the values previously entered by the user.

In the form generator you can specify the functions that should be called to validate the form and to submit it (to actually do something with the form values, like adding a new row to a table for instance):

$form[‘#validate’][] = ‘car_edit_validate’;
$form[‘#submit’][] = ‘car_edit_submit’;

Where car_edit_validate is the name of the function that will be called to do the validation and car_edit_submit is the function that will be called after the form validates.

Every time you submit the form the validate function is called first and if the validation does not encounter any errors the submit function is called.

The problem I had is that I wanted to return an error and keep the form values if  an error occurred in my submit function. By default, after the submit function is called an empty form will be rendered and user entered values will be lost even if you add an error to the form with form_set_error. The error is displayed but this does not prevent the form values from being lost. If you want to keep the form values you must also add this line:

$form_state[‘redirect’] = FALSE;

Example:

function car_edit_submit($form, &$form_state) {
  global $user;
  $car = (object)$form_state['values'];
  $car->uid = $user->uid;
  if(car_save($car)) {
    drupal_set_message(t('car successfully saved')));
    drupal_goto('car/'.$car->cid);
  } else {
    form_set_error('form', t('The car could not be saved, please try again'));
    $form_state['redirect'] = FALSE; //To prevent Drupal from cleaning the form
  }
}