This is a case-study of porting a module (Faq_Ask in this case) from Drupal 5 to Drupal 6.

Info file

Two changes had to be made here. First, there was the addition of a core definition:core = 6.x
Secondly, there was a nasty 'gotcha'. The dependencies line needed some square brackets in it: dependencies[] = faqThe D5 version did not have these square brackets in it, whereas D6 requires them. Using D6-RC2, the absence of the square brackets caused the dependencies to be silently ignored, however using D6-dev, this caused a silent parser error, meaning that D6 refused to load the module.

Install file

The only change that had to be made here was to use the database schema itself to create and delete the tables, rather than doing it with SQL. As the schema hook was already written, the install and uninstall hooks became very simple:

/**
 * Implementation of hook_install().
 */
function faq_ask_install() {
  $result = drupal_install_schema('faq_ask');

  if (count($result) > 0) {
    drupal_set_message(t('faq_ask module installed.'));
  }
  else {
    drupal_set_message(t('faq_ask table creation failed. Please "uninstall" the module and retry.'));
  }
}

/**
 * Implementation of hook_uninstall().
 */
function faq_ask_uninstall() {
  drupal_uninstall_schema('faq_ask');

  // ... delete variables ...

  drupal_set_message(t('faq_ask module uninstalled.'));
}

hook_help

The parameters have changed for this hook. Instead a single "$section," there are now two parameters ("$path" and "$arg"). Additionally, placeholders are now available for things that used to be done with the "arg" function.

The old code:

  switch ($section='') {
    case 'admin/help#faq_ask':
      $output .= ...
      return $output;

    case 'faq_ask/'. arg(1):
    case 'faq_ask':
      ...
  }

Is now:

  switch ($path) {
    case 'admin/help#faq_ask':
      $output .= ...
      return $output;

    case 'faq_ask/%':
    case 'faq_ask':
      ...
  }

hook_menu

This hook has changed quite a lot between D5 and D6, so each menu entry had to change from something along the lines of:

   $items[] = array(
      'path' => 'faq_ask/answer',
      'title' => t('Answer a question'),
      'callback' => 'faq_ask_answer',
      'access' => user_access('answer question'),
      'type' => MENU_CALLBACK,
      ); 

To the new D6 way of doing things:

   $items['faq_ask/answer/%node'] = array(
    'title' => 'Answer a question',
    'page callback' => 'faq_ask_answer',
    'page arguments' => array(2),
    'access arguments' => array('answer question'),
    'type' => MENU_CALLBACK,
  ); 

The call to t() has been removed (it is now done automatically by menu), the path has moved to the array key, 'callback' changes to 'page callback', 'access' to 'access arguments', and the new wildcard system is used (the '%node' in the path, and the 'page arguments' entry). The new hook_menu in its entirety follows:

 /**
 * Implementation of hook_menu()
 */
function faq_ask_menu() {
  $items = array();

  $items['admin/settings/faq/ask'] = array(
    'title' => 'Experts',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('faq_ask_settings_form'),
    'access arguments' => array('administer faq'),
    'description' => 'Allows the user to configure the Ask_FAQ module.',
    'type' => MENU_LOCAL_TASK,
    'weight' => -2,
  );

  $items['faq_ask'] = array(
    'title' => 'Ask a question',
    'page callback' => 'faq_ask_page',
    'access callback' => 'user_access',
    'access arguments' => array('ask question'),
    'weight' => 1,
  );

  $items['faq_ask/%'] = array(
    'page arguments' => array(1),
    'access arguments' => array('ask question'),
  );

  $items['faq_ask/answer/%node'] = array(
    'title' => 'Answer a question',
    'page callback' => 'faq_ask_answer',
    'page arguments' => array(2),
    'access arguments' => array('answer question'),
    'type' => MENU_CALLBACK,
  );

  $items['faq_ask/edit/%node'] = array(
    'title' => 'Edit a question',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('faq_ask_form', null, 2),
    'access arguments' => array('answer question'),
    'type' => MENU_CALLBACK,
  );

  $items['faq_ask/more'] = array(
    'title' => 'List more unanswered questions',
    'page callback' => 'faq_ask_list_more',
    'access callback' => 'faq_ask_user_access_or',
    'access arguments' => array('answer question', 'ask question'),
    'type' => MENU_CALLBACK,
  );

  return $items;
} 

Of special note is 'faq_ask/%', which when paired with the 'faq_ask' above, adds an optional wildcard to the URL. Furthermore, for 'faq_ask/more', the default user_access callback is insufficient, as it needs to grant access to users if they have either of two permissions, hence the extra 'access callback' line, which is a very simple function:

 /**
 * Determines whether the current user has one of the given permissions.
 */
function faq_ask_user_access_or($string1, $string2) {
  return user_access($string1) || user_access($string2);
}

On the note of wildcards, 'faq_ask/answer/%node' now uses an explicit node wildcard, whereas before it used an implicit nid: function faq_ask_answer($nid) { The new D6 version takes a node automatically loaded by the menu system, rather than a node ID from the URL:

 function faq_ask_answer($node) {
  // Change the status to published.
  db_query("UPDATE {node} SET status=1 WHERE nid=%d", $node->nid);

  // Need to invoke node/##/edit.
  drupal_goto('node/'. $node->nid .'/edit');
}

Other pages also had changes to the new wildcard system, which resulted in similar function prototype changes.

Add CSS

In D5, it was common practice to use drupal_add_css (or _js) within the hook_menu code. In D6 this doesn't work.

However, hook_init was also changed and is now a good place to add the CSS. See http://api.drupal.org/api/function/hook_init.

function hook_init() {
  drupal_add_css(drupal_get_path('module', 'faq_ask') .'/faq_ask.css');
}

Forms API

FAPI functions now generally take form values through a form state variable, which is now at the start of the parameter list, so the old D5 function prototype function faq_ask_form($tid, $nid, $form_values = null) { changed to function faq_ask_form($form_state, $tid, $node) { Within the form generation function, $form_state['post'] is now used in place of $form_values. Likewise, the form submission handlers also had to change to use form state. The old D5 prototype was function faq_ask_form_submit($form_id, $form_values) { The new D6 version is

 function faq_ask_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];

For ease of porting, the form values are moved into a variable with the same name as the old prototype. The exact same change was made to the settings form submission handler.

url() and l()

The url and l functions took a long argument list of options under D5. D6 changed this to an array containing the options, so a piece of code before porting: $items[] = l('<strong>'. t('more...') .'</strong>', 'faq_ask/more', array(), null, null, false, true); Was changed to: $items[] = l('<strong>'. t('more...') .'</strong>', 'faq_ask/more', array('html' => TRUE)); And similarly for url, the D5 code: url('faq_ask/answer/'. $node->nid, null, null, true) Which became: url('faq_ask/answer/'. $node->nid, array('absolute' => TRUE))

Miscellaneous changes

The watchdog function also has some changes. Remembering to update this can be difficult, as in normal testing, watchdog will not be called unless you really try and break things, so you may not notice that your calls to it longer work. t() is now automatically called (or not) by the watchdog function, so you should not call t() on the messages you pass. So a simple call under D5: watchdog('FAQ_Ask', t('Expert notification email sent.') .' '. $to, WATCHDOG_NOTICE);
This became: watchdog('FAQ_Ask', 'Expert notification email sent. !to', array('!to' => $to), WATCHDOG_NOTICE); Note the removal of the call to t(), and the addition of a new parameter (the array). This is where placeholders previously passed to t() are passed (or things which were previously concatenated to the translated message). If you specify a watchdog severity, then you need to add this argument (e.g. array()) even if you don't use any placeholders in the message text.

A subtle, but significant change is in the code that creates the suggested taxonomy term programmatically and then provides it for the faq node. The term is originally built as an array; this was fine in 5.x, but changes to the taxonomy module in 6.x cause several problems. The "fix" was to force the term to be recast as an object before creating the node:
$term = (object)$term;

A number of fixes to coding style were made during the porting process. Updating to the new standards is a perfect time to get a refresher on all the standards which didn't change!

One more thing: Use the Coder module to double check things. There were some other changes that could now be done in D6 that were unavailable in D5, such as the "db_placeholders" function. Coder reminded me of these and some other things that should have been done. Don't let your code leave home without it!