Automated module conversion

For an automated module conversion solution, visit the Deadwood project. Deadwood will attempt to automatically make many of the changes listed in the conversion guide below and the related roadmaps for the menu, schema, and form APIs.

Some of the new functions are implemented in the Helpers modules and can be used to bridge your re-development.

Video tutorial

To play or download a 16 minute screencast, see Porting Drupal modules (in this video tutorial, from Drupal 5.x to Drupal 6.x using Coder, a developer module that assists with code review and version upgrade).

Tutorial

Updating Drupal modules to D6 in three easy steps, by successively using the Deadwood, Schema, and Coder modules. Not as hard as you might think.

Contributed module status

Contributed modules status - version 6.x is a list of plans for contributed modules being ported to 6.x. It's a wiki page where everyone is encouraged to contribute updates.

Overview of Drupal API changes in 6.x

  1. Entirely new menu system
  2. Major FormAPI improvements
  3. New Schema API
  4. New format for hook_install()
  5. New format for hook_uninstall()
  6. New format for hook_update_N()
  7. The arguments to url() and l() have changed
  8. Variable names can now be 128 characters long
  9. Taxonomy terms are now associated with node revisions, not just nodes
  10. format_plural() accepts replacements
  11. New drupal_alter() function for developers
  12. New module_load_include() and module_load_all_includes() functions for developers
  13. hook_form_alter() parameters have changed
  14. hook_link_alter() parameters have changed
  15. hook_profile_alter() parameters have changed
  16. hook_mail_alter() parameters have changed
  17. $locale became $language
  18. New hook_theme() registry
  19. template_preprocess_* with .tpl.php files
  20. node/add is now menu generated
  21. New watchdog hook, logging and alerts
  22. Parameters of watchdog() changed
  23. new hook_update_N naming convention
  24. New syntax for .info files
  25. Core compatibility now specified in .info files
  26. PHP compatibility now specified in .info files
  27. New db_column_exists() method
  28. cache_set parameter order has changed
  29. Cache set and get automatically (un)serialize complex data types
  30. node_revision_list() now returns keyed array
  31. New operation in image.inc: image_scale_and_crop()
  32. New user_mail_tokens() method
  33. New ip_address() function when working behind proxies
  34. {files} table changed
  35. file_check_upload() merged into file_save_upload()
  36. {file_revisions} table is now {upload}
  37. drupal_add_css() supports automatic RTL CSS discovery
  38. Node previews and adding form fields to the node form
  39. JavaScript behaviors: new approach to attaching behaviors
  40. JavaScript themeing
  41. Translation of JavaScript files
  42. JavaScript aggregation
  43. custom_url_rewrite() replaced
  44. hook_user('view'), hook_profile_alter() and profile theming
  45. Distributed Authentication changes
  46. hook_help() parameters are changed
  47. Change "Submit" to "Save" on buttons
  48. node_feed() parameters changed
  49. hook_nodeapi('submit') has been replaced by op='presave'
  50. taxonomy_get_vocabulary() changed to taxonomy_vocabulary_load()
  51. hook_init() is split up into hook_init() and hook_boot()
  52. Remove db_num_rows() method
  53. Remove $row argument from db_result() method
  54. Block-level caching
  55. Batch operations : progressbar for heavy computations
  56. Node access modules : simplified hook_enable / hook_disable / hook_node_access_records
  57. node_access_rebuild($batch_mode = TRUE) / node_access_needs_rebuild()
  58. Upgraded to jQuery 1.2.3
  59. Removed several functions from drupal.js
  60. The book module has been rewritten to use the new menu system
  61. new helper function: db_placeholders()
  62. Comment settings are now per node type
  63. Check node access before emailing content
  64. form property #DANGEROUS_SKIP_CHECK removed
  65. "Access control" renamed to "permissions"
  66. locale_refresh_cache() has been removed
  67. FormAPI image buttons are now supported
  68. db_next_id() is gone, and replaced as db_last_insert_id()
  69. admin/logs renamed to admin/reports
  70. New helper function: drupal_match_path()
  71. drupal_mail() parameters have changed
  72. New hook: hook_mail
  73. Use drupal_set_breadcrumb() instead of menu_set_location() to set custom breadcrumbs
  74. user_authenticate() changed parameters
  75. Automatically add Drupal.settings.basePath
  76. Get an object relevant on specific paths
  77. hook_access() added parameter
  78. Can't add javascript or CSS to header in hook_footer()
  79. Translations are looked for in ./translations
  80. hook_submit() has been removed
  81. hook_comment() no longer supports the 'form' operation, use hook_form_alter() instead
  82. taxonomy_override_selector variable allows alternate taxonomy form operations

The menu system has been completely re-hauled in 6.x. See the Menu system overview.

Major FormAPI improvements

A number of major improvements have been made to FormAPI in Drupal 6, most specifically intended to improve the consistency and reliability of the API. While each individual change is relatively easy to implement, they will affect ALL form processing code and should be noted by all Drupal developers. For a complete list of these changes and sample code, see drupal.org/node/144132.

New Schema API

The database schema (table creation) for modules has now been abstracted into a Schema API. This means that you no longer need to look up database-specific syntax for doing CREATE TABLE statements, and adding additional database backends is much easier. More detailed documentation is available here: http://drupal.org/node/146843.

This patch caused changes to the format of hook_install(), hook_uninstall(), and hook_update_N(). No longer are switch statements done on $GLOBALS['db_type']; instead, use the variety of schema API functions to perform table manipulation.

New format for hook_install()

5.x:

(in mybooks.install)

/**
 * Implementation of hook_install().
 */
function mybooks_install() {
  switch ($GLOBALS['db_type']) {
    case 'mysql':
    case 'mysqli':
      db_query("CREATE TABLE {book} (
        vid int unsigned NOT NULL default '0',
        nid int unsigned NOT NULL default '0',
        parent int NOT NULL default '0',
        weight tinyint NOT NULL default '0',
        PRIMARY KEY (vid),
        KEY nid (nid),
        KEY parent (parent)
      ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
      break;
    case 'pgsql':
      db_query("CREATE TABLE {book} (
        vid int_unsigned NOT NULL default '0',
        nid int_unsigned NOT NULL default '0',
        parent int NOT NULL default '0',
        weight smallint NOT NULL default '0',
        PRIMARY KEY (vid)
      )");
      db_query("CREATE INDEX {book}_nid_idx ON {book} (nid)");
      db_query("CREATE INDEX {book}_parent_idx ON {book} (parent)");
      break;
  }
}

6.x:
(in mybooks.install)

/**
 * Implementation of hook_install().
 */
function mybooks_install() {
  // Create tables.
  drupal_install_schema('mybooks');
}

(in mybooks.install)

/**
 * Implementation of hook_schema().
 */
function mybooks_schema() {
  $schema['book'] = array(
    'fields' => array(
      'vid'    => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
      'nid'    => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
      'parent' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
      'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny')
    ),
    'indexes' => array(
      'nid'    => array('nid'),
      'parent' => array('parent')
    ),
    'primary key' => array('vid'),
  );

  return $schema;
}

New format for hook_uninstall()

5.x:

/**
 * Implementation of hook_uninstall().
 */
function mybooks_uninstall() {
  db_query('DROP TABLE {book}');
}

6.x:

/**
 * Implementation of hook_uninstall().
 */
function mybooks_uninstall() {
  // Remove tables.
  drupal_uninstall_schema('mybooks');
}

New format for hook_update_N()

5.x:

 /**
 * Update files tables to associate files to a uid by default instead of a nid.
 * Rename file_revisions to upload since it should only be used by the upload
 * module used by upload to link files to nodes.
 */
function system_update_6022() {
  $ret = array();
  switch ($GLOBALS['db_type']) {
    case 'mysql':
    case 'mysqli':
      // Change ownership of files to users rather than nodes and add columns
      // for file status and timestamp.
      $ret[] = update_sql("ALTER TABLE {files} DROP INDEX nid");
      $ret[] = update_sql('ALTER TABLE {files} CHANGE COLUMN nid uid int unsigned NOT NULL default 0');
      $ret[] = update_sql("ALTER TABLE {files} ADD COLUMN status int NOT NULL default 0 AFTER filesize");
      $ret[] = update_sql("ALTER TABLE {files} ADD COLUMN timestamp int unsigned NOT NULL default 0 AFTER status");
      $ret[] = update_sql("ALTER TABLE {files} ADD KEY uid (uid)");
      $ret[] = update_sql("ALTER TABLE {files} ADD KEY status (status)");
      $ret[] = update_sql("ALTER TABLE {files} ADD KEY timestamp (timestamp)");

      // Rename the file_revisions table to upload then add nid column.
      $ret[] = update_sql('ALTER TABLE {file_revisions} RENAME TO {upload}');
      $ret[] = update_sql('ALTER TABLE {upload} ADD COLUMN nid int unsigned NOT NULL default 0 AFTER fid');
      $ret[] = update_sql("ALTER TABLE {upload} ADD KEY nid (nid)");
      break;

    case 'pgsql':
      // @todo test the pgsql queries
      // Change owernship of files to users rather than nodes and add columns
      // for file status and timestamp.
      $ret[] = update_sql("DROP INDEX {files}_nid_idx");
      db_change_column($ret, 'files', 'nid', 'uid', 'int_unsigned', array('default' => '0', 'not null' => TRUE));
      db_add_column($ret, 'files', 'status', 'uid', 'int', array('default' => '0', 'not null' => TRUE));
      db_add_column($ret, 'files', 'timestamp', 'uid', 'int_unsigned', array('default' => '0', 'not null' => TRUE));
      $ret = update_sql("CREATE INDEX {files}_uid_idx ON {files} (uid)");
      $ret = update_sql("CREATE INDEX {files}_status_idx ON {files} (status)");
      $ret = update_sql("CREATE INDEX {files}_timestamp_idx ON {files} (timestamp)");

      // Rename the file_revisions table to upload then add nid column.
      $ret[] = update_sql("DROP INDEX {file_revisions}_vid_idx");
      $ret[] = update_sql('ALTER TABLE {file_revisions} RENAME TO {upload}');
      db_add_column($ret, 'upload', 'nid', 'int unsigned', array('default' => 0, 'not null' => TRUE));
      $ret[] = update_sql("CREATE INDEX {upload}_vid_idx ON {upload} (vid)");
      $ret[] = update_sql("CREATE INDEX {upload}_nid_idx ON {upload} (nid)");
            

      break;
  }
  // The nid column was renamed to uid. Use the old nid to find the node's uid.
  $ret[] = update_sql('UPDATE {files} f JOIN {node} n ON f.uid = n.nid SET f.uid = n.uid');
  // Use the existing vid to find the nid.
  $ret[] = update_sql('UPDATE {upload} u JOIN {node_revisions} r ON u.vid = r.vid SET u.nid = r.nid');
  
  return $ret;
}

6.x:

/**
 * Update files tables to associate files to a uid by default instead of a nid.
 * Rename file_revisions to upload since it should only be used by the upload
 * module used by upload to link files to nodes.
 */
function system_update_6022() {
  $ret = array();

  // Rename the nid field to vid, add status and timestamp fields, and indexes.
  db_drop_index($ret, 'files', 'nid');
  db_change_field($ret, 'files', 'nid', 'uid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0));
  db_add_field($ret, 'files', 'status', array('type' => 'int', 'not null' => TRUE, 'default' => 0));
  db_add_field($ret, 'files', 'timestamp', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0));
  db_add_index($ret, 'files', 'uid', array('uid'));
  db_add_index($ret, 'files', 'status', array('status'));
  db_add_index($ret, 'files', 'timestamp', array('timestamp'));

  // Rename the file_revisions table to upload then add nid column. Since we're
  // changing the table name we need to drop and re-add the vid index so both
  // pgsql ends up with the correct index name.
  db_drop_index($ret, 'file_revisions', 'vid');
  db_rename_table($ret, 'file_revisions', 'upload');
  db_add_field($ret, 'upload', 'nid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0));
  db_add_index($ret, 'upload', 'nid', array('nid'));
  db_add_index($ret, 'upload', 'vid', array('vid'));

  // The nid column was renamed to uid. Use the old nid to find the node's uid.
  $ret[] = update_sql('UPDATE {files} f JOIN {node} n ON f.uid = n.nid SET f.uid = n.uid');
  // Use the existing vid to find the nid.
  $ret[] = update_sql('UPDATE {upload} u JOIN {node_revisions} r ON u.vid = r.vid SET u.nid = r.nid');

  return $ret;
}

Arguments changed for url() and l()

The arguments to url() and l() have changed. Instead of a long line of single arguments (the exact options and order of which were hard to remember), both of these functions now take an array to specify the optional arguments. The new function signatures are as follows:

l($text, $path, $options = array());
url($path = NULL, $options = array());

$options is an associative array that contains 'query', 'fragment', 'absolute', 'alias' for both, and 'html' and 'attributes' for l(). Thus, the $attributes array that was a parameter in 5.x will now be passed in as $options['attributes'].

Additionally, the 'query' argument can now be passed as either a serialized string (as before), or as an array. Treating the query arguments as an array allows these functions to automatically urlencode() and format them with drupal_query_string_encode(). Furthermore, the query elements are just name/value pairs, so an array is the most logical data structure to store and manipulate them with.

A basic example to demonstrate the new syntax. 5.x and before:

url("user/$account->uid", NULL, NULL, TRUE)

6.x:

url("user/$account->uid", array('absolute' => TRUE)).

See http://api.drupal.org/api/function/url/6 and http://api.drupal.org/api/function/l/6 for more information.

A conversion script is available at http://drupal.org/files/issues/replace.php_.txt -- it converts all files recursively from the directory in which it is placed.

Variables can have names 128 characters long

The names of the variables accessed via variable_set() and variable_get() can now be 128 characters long.

Taxonomy terms are associated with node revisions

Taxonomy terms are now associated with specific node revisions, not just the node itself. This allows modules to know if the taxonomy terms have changed from one revision to the next. The developer-visible changes as a result of this are:

  • The {term_node} table now has a vid (version id) column, and (tid, nid, vid) is now the primary key. Any direct queries against {term_node} should be checked, and probably modified to use the node revision (vid) field instead of nid.
  • taxonomy_node_get_terms() and taxonomy_node_get_terms_by_vocabulary() now take a full $node object, not just a nid (node id).
  • taxonomy_node_save() and taxonomy_node_delete() now take a full $node object, not just a nid (node id); and the latter now deletes all taxonomy terms for all revisions of the specified node.
  • taxonomy_node_delete_revision() has been added to delete all taxonomy terms from the current revision of the given $node object.

format_plural() accepts replacements

An argument for replacements has been added to format_plural(), escaping and/or theming the values just as done with t(). The new function signature is as follows:

format_plural($count, $singular, $plural, $args = array());

For example, 5.x and before:

strtr(format_plural($num, 'There is currently 1 %type post on your site.', 'There are currently @count %type posts on your site.'), array('%type' => theme('placeholder', $type)));

6.x:

format_plural($num, 'There is currently 1 %type post on your site.', 'There are currently @count %type posts on your site.', array('%type' => $type));

New drupal_alter() function for developers

Developers building and manipulating standard Drupal data structures in their modules can expose those structures for manipulation by other modules using the drupal_alter() function. For example:

$data = array();
$data['#my_property']       = 'Some data!';
$data['#my_other_property'] = 'More data...';
drupal_alter('my_data', $data);

// Do your stuff here...

The above code would allow any other module to implement a hook_my_data_alter() to manipulate the $data array before any further processing is done. The majority of Drupal's internal data structures are stored and altered in this fashion, including the familiar Form API arrays and hook_form_alter().

New module_load_include() and module_load_all_includes() functions for developers

Include files in Drupal 6 are now conditionally loaded as a performance enhancement.

Developers needing to access functions and other information contained in module include files can now use module_load_include() and module_load_all_includes().

module_load_include() accepts the following three parameters.

  • $type specifies the desired file's extension. For the below example case 'inc'
  • $module specifies the module that the desired include file is a member of. In the below example case we are wanting to include an include file from the node.module
  • $name (optional) specifies the name of the file to be included minus the file's extension (that was provided in the first argument, $type). If this parameter is not given the module's name will be assumed as the file's name

module_load_all_includes() accepts the following two parameters.

  • $type specifies the desired file's extension.
  • $name (optional) specifies the name of the file to be included minus the file's extension (that was provided in the first argument, $type). If this parameter is not given the module's name will be assumed as the file's name

module_load_all_includes()'s primary difference from module_load_include() is that module_load_all_includes() loads all of the modules include files.

A common use for this implementation is when a custom module needs to load a node form. In order to do this page.node.inc will need to be loaded, modules can do this by calling module_load_include() in their custom module.

<?php
  module_load_include('inc', 'node', 'node.pages');
?>

This is a necessary step to load node forms by means of drupal_get_form(), node_page_edit(), and other node form loading functions.

Modules that try to load a node form without the proper include being included may result in a call_user_func_array() error stating that a "valid callback argument was expected".

hook_form_alter() parameters have changed

The order of the parameters for hook_form_alter() has changed for greater consistency with the rest of core, and compatibility with the new drupal_alter() function.
Additionally the new $form_state variable can be used as parameter (see the section Major FormAPI improvements and detailed information on the separate document FormAPI changes).

Drupal 5:

function my_module_form_alter($form_id, &$form) {
  // Alter the form...
}

Drupal 6:

function my_module_form_alter(&$form, &$form_state, $form_id) {
  // Alter the form...
}

The order of the parameters for hook_link_alter() has changed for greater consistency with the rest of core, and compatibility with the new drupal_alter() function.

Drupal 5:

function my_module_link_alter($node, &$links) {
  // Alter the links...
}

Drupal 6:

function my_module_link_alter(&$links, $node) {
  // Alter the links...
}

hook_profile_alter() parameters have changed

The parameters for hook_profile_alter() have changed for compatibility with the new drupal_alter() function. As result of restructured profile rendering the $fields are now part of the $account object.

Drupal 5:

function my_module_profile_alter($account, &$fields) {
  // Alter the fields...
  $fields['abc'] = ...
}

Drupal 6:

function my_module_profile_alter(&$account) {
  // Alter the profile...
  $account->content['abc'] = ...
}

hook_mail_alter() parameters have changed

The order and makeup of the parameters for hook_mail_alter() has changed for greater consistency with the rest of core, and compatibility with the new drupal_alter() function. Rather than being passed in as individual parameters, each piece of data is a separate property of a standard Drupal data array.

Drupal 5:

function my_module_mail_alter(&$mailkey, &$to, &$subject, &$body, &$from, &$headers) {
  // Alter the individual params...
  $to = 'mail@example.com';
}

Drupal 6:

function my_module_mail_alter(&$message) {
  // Alter the mail message...
  $message['to'] = 'mail@example.com';
}

$locale become $language

With the improved language subsystem in Drupal, the global $locale variable (which contained a language code) is replaced with the global $language object (which contains properties for several language details). As with $locale, the new $language object gives information about the language chosen for the current page request. This change also affects themes, because Drupal now knows about the directionality (left to right or right to left) of the language used, and themes can make use of this information to output proper stylesheets.

New hook_theme registry

See the Drupal 6 theming for module developers documentation for a complete description of these new features!

Modules need to register all their theme functions via the new hook_theme(). This hook returns an array of theme functions, and arguments.

The theme_something() functions remain mostly unchanged, but you need to add a hook_theme(), e.g. as something_theme() like so:

function something_theme() {
  return array(
    'something_format' => array(
      'arguments' => array('content' => NULL),
    ),
  );
}

This registers a theme function called something_format, which can be implemented by a theme_something_format($content) or via having .tpl.php files.

There are two extra keywords that can be placed within the registry.

file
This will allow your module to use a separate include file where either the default theme function or preprocess function exists. e.g. theme_something_format() or template_preprocess_something_format(). You are not confined to keeping them inside the main .module file due to this option.
template
To implement the theme function as a template, you must define the name of the template name sans the extension. Typically, the .tpl.php extension is will be appended when invoked. Note that underscores must always be changed to dashes.

For example:

function something_theme() {
  return array(
    'something_format' => array(
      'file'      => 'something.inc',
      'template'  => 'something-format',
      'arguments' => array('content' => NULL),
    ),
  );
}

The above will register something-format.tpl.php (which should reside in your module's directory) as the default implementation of your theme.

Your theme may also implement hook_theme() to register additional theme functions. See Converting 5.x themes to 6.x.

Also see hook_theme documentation for more information.

template_preprocess_HOOK

When registering theme implementations that are .tpl.php files, you may use template_preprocess_HOOK(). This is very similar to the _phptemplate_variables() function of template.php in Drupal 5, but is on a PER THEME HOOK basis. For example, core uses this for nodes:

/*
 * Prepare the values passed to the theme_node function to be passed
 * into standard template files.
 */
function template_preprocess_node(&$variables) {
  $node = $variables['node'];
  if (module_exists('taxonomy')) {
    $variables['taxonomy'] = taxonomy_link('taxonomy terms', $node);
  }
  else {
    $variables['taxonomy'] = array();
  }

  if ($variables['teaser'] && $node->teaser) {
    $variables['content'] = $node->teaser;
  }
  elseif (isset($node->body)) {
    $variables['content'] = $node->body;
  }
  else {
    $variables['content'] = '';
  }

  $variables['date']      = format_date($node->created);
  $variables['links']     = !empty($node->links) ? theme('links', $node->links, array('class' => 'links inline')) : '';
  $variables['name']      = theme('username', $node);
  $variables['node_url']  = url('node/'. $node->nid);
  $variables['terms']     = theme('links', $variables['taxonomy'], array('class' => 'links inline'));
  $variables['title']     = check_plain($node->title);

  // Flatten the node object's member fields.
  $variables = array_merge((array)$node, $variables);

  // Display info only on certain node types.
  if (theme_get_setting('toggle_node_info_' . $node->type)) {
    $variables['submitted'] = theme('node_submitted', $node);
    $variables['picture'] = theme_get_setting('toggle_node_user_picture') ? theme('user_picture', $node) : '';
  }
  else {
    $variables['submitted'] = '';
    $variables['picture'] = '';
  }

  $variables['template_files'][] = 'node-'. $node->type;
}

This hook is passed a reference to an array of variables. Every item in this array becomes a real variable in the associated .tpl.php file. You may also use the special keys 'template_file' and 'template_files' to provide alternate .tpl.php files to load. Though you may find it easier to use wildcard theming. (See below).

Dynamic theming with wildcards

When registering a theme function via hook_theme, you may specify a regex pattern. This means that a theme can define any function or template that fits the pattern and it will be registered to that theme implementation. For example, the Views module might register the theme hook "views_view" with the pattern "views_view__". Because this is a regular expression, .* is essentially assumed to be on both the front and back of this pattern.

Views can then call a theme function like this:

  theme(array('views_view__'. $view->name, 'views_view'), $view);

If the name of my view happens to be 'foobar', and the theme has registered 'views_view__foobar', it will use that; otherwise it will look for 'views_view' and use that. All theme functions registered to a pattern will take the exact same arguments.

node/add is now menu generated

The node/add/$type menu items are now auto-generated by the menu system. You should not declare them in your menu hook.

This means that you can use hook_menu_alter to change the visibility of an item or change the access callback.

New watchdog hook, logging and alerts

There is now a new hook_watchdog in core. This means that contributed modules can implement hook_watchdog to log Drupal events to custom destinations. Two core modules are included, dblog.module (formerly known as watchdog.module), and syslog.module. Other modules in contrib include an emaillog.module, included in the logging_alerts module. See syslog or emaillog for an example on how to implement hook_watchdog.

In a nutshell, the watchdog hook takes one argument, which is a keyed array containing the log message, variables to replace placeholders with in the message, severity, the user object, message type, request URI, Referrer, IP address, and timestamp. Messages are translatable with t(), passing on the message and variables, or strtr() can be used to replace placeholders with values from the variables array to get an English message.

Modules can route the messages to custom destination, based on severity, or other criteria.

Here is a hypothetical example:

function mysms_watchdog($log = array()) {
    if ($log['severity'] == WATCHDOG_ALERT) {
      mysms_send($log['user']->uid,
        $log['type'],
        $log['message'],
        $log['variables'],
        $log['severity'],
        $log['referer'],
        $log['ip'],
        format_date($log['timestamp'])));
}

As well, the severity levels have been expanded to confirm to RFC 3164. This means there are new severity levels that your module can log to. For example, alert and critical can go to a pager, or instant messaging, while debug can be dumped to a file.

The severity levels are as follows.

define('WATCHDOG_EMERG',    0); // Emergency: system is unusable
define('WATCHDOG_ALERT',    1); // Alert: action must be taken immediately
define('WATCHDOG_CRITICAL', 2); // Critical: critical conditions
define('WATCHDOG_ERROR',    3); // Error: error conditions
define('WATCHDOG_WARNING',  4); // Warning: warning conditions
define('WATCHDOG_NOTICE',   5); // Notice: normal but significant condition
define('WATCHDOG_INFO',     6); // Informational: informational messages
define('WATCHDOG_DEBUG',    7); // Debug: debug-level messages

As a module developer, take special care not to flood destinations with high priority messages, such as critical or alert. In other word, use sparingly.

Module authors who use the watchdog() function with $type = 'debug' are strongly encouraged to replace that with $severity WATCHDOG_DEBUG for consistency.

So instead of:
watchdog('debug', 'My debug message here');

You should use (see also below for the parameter changes of watchdog()):
watchdog('modulename', 'My debug message here', array(), WATCHDOG_DEBUG);

Parameters of watchdog() changed

The watchdog() function was improved to support translation of log messages later (or skip translation), depending on the logging method used (see the watchdog hook above). The core dblog.module translates log messages to the language used by the administrator when viewing the log messages, while the syslog.module passes on English log messages to the system log.

In Drupal 5 and before, you used to call watchdog with:

watchdog('user', t('Removed %username user.', array('%username' => $user->name)));

In Drupal 6 however, t() is called later (or not at all), so you should not call t() on the message, but keep the message and parameters separated. Just remove t() and leave the parameter order intact:

watchdog('user', 'Removed %username user.', array('%username' => $user->name));

If you have a message which has no placeholders to replace with values, pass on an empty array() - which is also the default value for the variable array, so you can omit it, if you have no more parameters to pass. If you have a dynamically generated message (a PHP error message, a message coming from a remote service or some other type of message, whose literal text cannot be included in the second parameter), please always pass on NULL in place of the variables parameter, so Drupal knows that the message is not for translation.

// Translatable message, no placeholders to replace, and default watchdog type used, so third parameter can be omitted.
watchdog('soap', 'Remote host seems to be slow.');

// Translatable message, no placeholders to replace, but watchdog type different from default
watchdog('soap', 'Soap message sent.', array(), WATCHDOG_DEBUG);


// A message that should not be translated. In this case, pass NULL in the third parameter!
watchdog('soap', $remote_soap_message, NULL);

Please also take the time to review your watchdog() calls for the correct use of the first parameter. Some contributed modules used to have that wrapped in t(), but that was and still is improper use of watchdog()!

New hook_update_N naming convention

Starting in Drupal 6.x, hook_update_N follows a naming convention of updates starting with the Drupal core compatibility version number, major version number, and then a sequential number indicating which update is taking place. For example:

/**
 * Change the severity column in the watchdog table to the new values.
 */
function system_update_6007() {
  $ret = array();
  $ret[] = update_sql("UPDATE {watchdog} SET severity = %d WHERE severity = 0", WATCHDOG_NOTICE);
  $ret[] = update_sql("UPDATE {watchdog} SET severity = %d WHERE severity = 1", WATCHDOG_WARNING);
  $ret[] = update_sql("UPDATE {watchdog} SET severity = %d WHERE severity = 2", WATCHDOG_ERROR);
  return $ret;
}

This indicates that this is the eighth update for system.module in version Drupal 6.

6.x-1.x versions of contributed modules should name their first update function as follows:

function example_update_6100() {
 // Updates.
}

6.x-2.x versions of the same contributed module would name their third update function as follows:

function example_update_6202() {
  // Updates.
}

New syntax for .info files

The syntax used for the .info files that contain metadata about modules (name, description, version, dependencies, etc) now supports nested data. This change was necessary when themes got .info files.

Arrays are created using a GET-like syntax:

key[] = "numeric array"
key[index] = "associative array"
key[index][] = "nested numeric array"
key[index][index] = "nested associative array"

For more details, see the comment at the top of drupal_parse_info_file() in includes/common.inc.

At this time, the only visible change for modules is how they specify the list of dependencies on other modules. Previously, the "dependencies" value had special-case handling to treat the value as a list. Now, the .info file just explicitly defines the dependencies as a list using the new syntax.

For example, in 5.x:

name = Forum
description = Enables threaded discussions about general topics.
dependencies = taxonomy comment
...

6.x:

name = Forum
description = Enables threaded discussions about general topics.
dependencies[] = taxonomy
dependencies[] = comment
...

Core compatibility now specified in .info files

As of version 6.x, Drupal core will refuse to enable or run modules and themes that aren't explicitly ported to the right version of core. For 5.x, this was implicitly true for modules, due to the existence of the .info files. For 6.x and beyond, the .info file must specify which Drupal core compatibility any module or theme has been ported to. This is accomplished by means of the new core attribute in the .info files.

6.x:

core = 6.x

Please note that the drupal.org packaging script automatically sets this value based on the Drupal core compatibility setting on each release node, so people downloading packaged releases from drupal.org will always get the right thing. However, for sites that deploy Drupal directly from git, it helps if you commit this change to the .info files for your modules.

PHP compatibility now specified in .info files

As of version 6.x, module and themes may specify a minimum PHP version that they require. They may do so by adding a line similar to the following to their .info file:

php = 5.1

That specifies that the module/theme will not work with a version of PHP earlier than 5.1. That is useful if the module makes use of features added in later versions of PHP (improved XML handling, object iterators, JSON, etc.). If no version is specified, it is assumed to be the same as the required PHP version for Drupal core. Modules should generally not specify a required version unless they specifically need a higher later version of PHP than is required by core. See the PHP Manual for further details on PHP version strings.

New db_column_exists() method

The db_column_exists($table, $column) method was added to the database abstraction layer in 6.x core. This allows developers to find out if a certain column exists in a given table, regardless of the underlying database management system being used (MySQL, PostgreSQL, etc).

Changes to cache_set parameter order

The parameter order of cache_set has been changed to confirm better to the Drupal coding standards, where optional variables should always come after all required variables. Since $data is required and has no default value, it should occur before $table.

5.x

cache_set('example_cid', 'cache', $data);

6.x

cache_set('example_cid', $data);

Cache set and get automatically (un)serialize complex data types

cache_set now automatically serializes arrays and objects passed to it before saving them to the cache table. When retrieving data from the cache, cache_get now automatically unserializes the data when necessary. Simple datatypes such as strings and integers do not need to and will not be serialized before being stored.

5.x:

$simple = 'Simple text';
$complex = array( 'one', 'two' );

cache_set('simple_cid', 'cache', $simple);
cache_set('complex_cid', 'cache', serialize($complex));
//..
$simple = cache_get('simple_cid');
$cache = cache_get('complex_cid');
$complex = unserialize($cache->data);

6.x:

$simple = 'Simple text';
$complex = array( 'one', 'two' );

cache_set('simple_cid', $simple);
cache_set('complex_cid', $complex);
//..
$simple = cache_get('simple_cid');
$cache = cache_get('complex_cid');
$complex = $cache->data;

node_revision_list() now returns keyed array

In previous versions of core, the node_revision_list() method returned an ordered array, but there were no meaningful keys to index the data. If you wanted to find information about a specific revision, you had to iterate through the whole array until you found the revision ID you were looking for. Now, the array is indexed by the revision ID (the vid column from the {node_revisions} table) so if you're looking for information about a specific revision, you can find it immediately.

New image.inc function: image_scale_and_crop()

image_scale_and_crop() scales an image to the exact width and height given. The required aspect ratio is maintained by cropping the image equally on both sides, or equally on the top and bottom. No image toolkit changes are required.

New user_mail_tokens() method

user.module now provides a user_mail_tokens() function to return an array of the tokens available for the email notification messages it sends when accounts are created, activated, blocked, etc. Contributed modules that wish to make use of the same tokens for their own needs are encouraged to use this function.

New ip_address() function when working behind proxies.

There is a new function, ip_address() that should be used instead of the superglobal $_SERVER['REMOTE_ADDR']. When Drupal is run behind a reverse proxy, the address of the proxy server will be in this superglobal for all users, and hence many parts of Drupal will log the wrong IP address for the client. This function makes deducing the client IP address transparent, whether a proxy is used or not. It is recommended that you replace all $_SERVER['REMOTE_ADDR'] with ip_address().

{files} table changed

The files table has been changed to make it easier to preview uploaded files. Two fields, status and timestamp, have been added so that temporary files can be stored and cleaned automatically removed during a cron job. Use file_set_status() to change a files status and make it a permanent file.

file_check_upload() merged into file_save_upload()

To simply the process of uploading files file_check_upload() has been merged into file_save_upload(). file_save_upload() now takes an array of callback functions to validate the upload and saves the files as temporary files in the {files} table. This cleans up a lot of duplicative code in core's upload processing and makes file previews much simpler.

  • file_validate_extensions() checks that the file extension in the given list
  • file_validate_size() checks for maximum file sizes and against a user's quota
  • file_validate_is_image() checks that the upload is an image
  • file_validate_image_resolution() checks that images meets maximum and minimum resolutions requirements

Drupal 5:

  if ($file = file_check_upload('picture_upload')) {
    // A whole lot of code to check the image dimensions and file size goes here
    //  ...
    $info = image_get_info($file->filepath);
    $destination = 'files/picture.'. $info['extension'];
    $file = file_save_upload('picture_upload', $destination, FILE_EXISTS_REPLACE);
  }

Drupal 6:

  $validators = array(
    'file_validate_is_image' => array(),
    'file_validate_image_resolution' => array('85x85'),
    'file_validate_size' => array(30 * 1024),
  );
  if ($file = file_save_upload('picture_upload', $validators)) {
    // All that validation is taken care of... but image was saved using
    // file_save_upload() and was added to the files table as a
    // temporary file. We'll make a copy and let the garbage collector
    // delete the original upload.
    $info = image_get_info($file->filepath);
    $destination = 'files/picture.'. $info['extension'];
    file_copy($file, $destination, FILE_EXISTS_REPLACE);
  }

The {file_revisions} table is now {upload}

In order to reduce confusion about the role that the {file_revisions} table played in Drupal's file handling, it's been renamed to {upload} to make it clear that it is purely for the use of the upload module, and for modules that would like that module to manage their files.

If you're writing a module that links files to nodes you need to create a table to maintain this relation. Creating your own table gives you the ability to do store additional information about the file relation, and is much cleaner that than trying to repurpose the upload module's table.

drupal_add_css() supports automatic RTL CSS discovery

When displaying the page in a right to left language, the drupal_add_css() function now automatically searches for CSS files named with -rtl.css suffixes, and adds them to the list of CSS files used to display the page. Drupal core comes with a set of right to left CSS files. Contributed modules and themes have the possibility to include right to left CSS overrides. More information available in the theme update guide.

Node previews and adding form fields to the node form

There is a subtle but important difference in the way node previews (and other such operations) are carried out when adding or editing a node. With the new Forms API, the node form is handled as a multi-step form. When the node form is previewed, all the form values are submitted, and the form is rebuilt with those form values put into $form['#node']. Thus, form elements that are added to the node form will lose any user input unless they set their '#default_value' elements using this embedded node object.

JavaScript behaviors

Behaviors are event-triggered actions that attach to page elements, enhancing default non-Javascript UIs.

Prior to Drupal 6, behaviors were usually added through attach functions which were registered to the jQuery ready object. Now behaviors are registered in the Drupal.behaviors object. All registered behaviors are run on ready, so they don't need the old if (Drupal.jsEnabled) test.

Behaviors should use a class in the form behaviorName-processed to ensure the behavior is attached only once to a given element.

Old code:

Drupal.myBehaviorAttach = function () {
  $('.my-class').each(function () {
    // Process...
  });
}

if (Drupal.jsEnabled) {
  $(document).ready(Drupal.myBehaviorAttach);
}

New code:

Drupal.behaviors.myBehavior = function (context) {
  $('.my-class:not(.myBehavior-processed)', context).addClass('myBehavior-processed').each(function () {
    // Process...
  });
};

Code should also be updated if it adds AJAX/AHAH loaded content. Developers implementing AHAH/AJAX in their solutions should call Drupal.attachBehaviors() after new page content has been loaded, feeding in an element to be processed, in order to attach all behaviors to the new content. Examples:

$(targetElt).html(response.data);
Drupal.attachBehaviors(targetElt);

or

var newContent = $(response.data).appendTo(elt);
Drupal.attachBehaviors(newContent[0]);

JavaScript themeing

There is now a themeing mechanism for JavaScript code. Together with the automatically included script.js, this allows theme developers more freedom in the domain of scripted events on Drupal webpages. Often, JavaScript code produces markup that is inserted into the page. However, this HTML code has usually been hardcoded into the script, which did not allow alteration of the inserted code.

Modules provide default theme functions in the Drupal.theme.prototype namespace. Themes should place their override functions directly in the Drupal.theme namespace. Scripts call Drupal.theme('function_name', ...) which in turn decides whether to call the function provided by the theme (if present) or the default function.

JavaScript theme functions are entirely free in their return value. It can vary from simple strings, up to complex data types like an object containing in turn several jQuery objects which are wrapped around DOM elements.

Translation of JavaScript files

There is now a client-side version of the t() function used for translating interface strings. That means that you should wrap your strings in JavaScript files into Drupal.t(). There is also an equivalent to format_plural(), named Drupal.formatPlural(). The parameter order is exactly like their server-side counterpart.

JavaScript aggregation

This update applies only to modules that include JavaScript code.

JavaScript aggregation was introduced, parallel to the existing CSS approach.

Aggregation should not be used for every JS file, since this can lead to redundant caches. Aggregation is appropriate for files that are used on many or most pages of a site. If your .js file is used only occasionally on a site (for example, only on certain administration pages), call drupal_add_js() with the $preprocess argument set to FALSE.

custom_url_rewrite() replaced

In place of that multi-purpose function, use custom_url_rewrite_inbound() and/or custom_rewrite_url_outbound(). The outbound function gained the ability to operate on the querystring and fragment parts of links.

hook_user('view'), hook_profile_alter() and profile theming

The return value of hook_user('view') has changed, to match the process that nodes use for rendering. Modules should add their custom HTML to $account->content element. Further, this HTML should be in the form that drupal_render() recognizes. Here is the conversion for blog.module

function blog_user($type, &$edit, &$user) {
   if ($type == 'view' && user_access('edit own blog', $user)) {
-    $items['blog'] = array('title' => t('Blog'),
-      'value' => l(t('View recent blog entries'), "blog/$user->uid", array('title' => t("Read @username's latest blog entries.", array('@username' => $user->name)))),
-      'class' => 'blog',
+    $user->content['summary']['blog'] =  array(
+      '#type' => 'user_profile_item',
+      '#title' => t('Blog'),
+      '#value' => l(t('View recent blog entries'), "blog/$user->uid", array('title' => t("Read @username's latest blog entries.", array('@username' => $user->name)))),
+      '#attributes' => array('class' => 'blog'),
     );
-    return array(t('History') => $items);
   }
 }

Use hook_profile_alter() to change any profile fields injected by other modules.

See this theme update page to learn about changes in theming of user profile view page.

Distributed Authentication changes

Past Drupal versions had two hooks which helped modules integrate their external authentication services into Drupal. Those were hook_info() and hook_auth(). Those hooks no longer exist. Instead, these modules are encouraged to use hook_form_alter() to swap in their own validation handler for the one provided by core Drupal - user_login_authenticate_validate(). See drupal_form_alter() in site_network.module (in git only) for an example of swapping in a custom handler. The validation handler should set an error if the credentials are not valid. Otherwise, just do nothing and the login will proceed. See this issue for the history.

hook_help() parameters are changed

The arguments to hook_help have changed

For example, in 5.x:

function node_help($section) {
  switch ($section) {
    case 'admin/help#node':

...

6.x:

function node_help($path, $arg) {
  switch ($path) {
    case 'admin/help#node':

...

Most modules will work with just this simple change to the 2 lines. Note, however, that the second parameter $arg is an array that (when appropriate) will hold the current Drupal path as would be returned from function arg(). This should be faster to use $arg[3] rather than arg(3), and also allows hook_help to be called with an arbitrary path and return the desired output. In addition, except for the special 'pseudo' path admin/help#modulename, the $path variable hold the router path as defined in the new menu system. Thus, you would change this code:

5.x:

  if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'revisions') {
    return '<p>'. t('The revisions let you track differences between multiple versions of a post.') .'</p>';
  }

make it part of the switch in 6.x:

    case 'node/%/revisions':
      return '<p>'. t('The revisions let you track differences between multiple versions of a post.') .'</p>';

See this issue for more details.

Change "Submit" to "Save" on buttons

It has been agreed on that the description "Submit" for a button is not a good choice since it does not indicate what actually happens. While for example on node editing forms, "Preview" and "Delete" describe exactly what will happen when the user clicks on the button, "Submit" only gives a vague idea.

Additionally, the term "Submit" can be at times hard to translate into other languages. Since the term is used for quite different actions, this can result in inappropriate translations because it is not possible to separate two exact same strings in a translation.

When you are labeling your buttons, make sure that it is clear what this button does when the user clicks on it.

node_feed() parameters changed

node_feed() now accepts an array of nids (i.e. integers), and not a database result object. this is more flexible for callers but sometimes slightly less efficient.

hook_nodeapi('submit') has been replaced by op='presave'

There is no longer a 'submit' op for nodeapi. Instead you may use the newly created 'presave' op. Note, however, that this op is invoked at the beginning of node_save(), in contrast to op='submit' which was invoked at the end of node_submit(). Thus 'presave' operations will be performed on nodes that are saved programmatically via node_save(), while in Drupal 5.x op='submit' was only applied to nodes saved via the node form. Note that the node form is now, in effect, a multistep form (for example when previewing), so if you need to fix up the data in the node for re-building the form, use a #submit function added to the node form's $form array.

taxonomy_get_vocabulary() changed to taxonomy_vocabulary_load()

The taxonomy_get_vocabulary function is now taxonomy_vocabulary_load and works the same way.

hook_init() is split up into hook_init() and hook_boot()

In Drupal 6, there are now two hooks that can be used by modules to execute code at the beginning of a page request. hook_boot() replaces hook_init() in Drupal 5 and runs on each page request, even for cached pages. hook_init() now only runs for non-cached pages and thus can be used for code that was previously placed in hook_menu() with $may_cache = FALSE:
5.x:

  mymodule_init() {
    // Code that is executed on each page request, even for cached pages.
  }
  mymodule_menu($may_cache) {
    if (!$may_cache) {
      // Code that is executed on page requests for non-cached pages only.
    }
  }

6.x:

  mymodule_boot() {
    // Code that is executed on each page request, even for cached pages.
  }
  mymodule_init() {
    // Code that is executed on page requests for non-cached pages only.
  }

Remove db_num_rows() method

The db_num_rows() method was removed from the database abstraction layer in 6.x core, as it was a database dependent method. Developers need to use other handling to replace the needs of this method. Here are 2 examples:

For checking number of rows within result set, in case of 5.x:

<?php
  $num_nodes = db_num_rows(db_query("SELECT * FROM {node} WHERE type = '%s'", $type->type));
?>

6.x:

<?php
  $num_nodes = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = '%s'", $type->type));
?>

For checking if any row exists within result set, in case of 5.x:

<?php
function taxonomy_render_nodes($result) {
  if (db_num_rows($result) > 0) {
    while ($node = db_fetch_object($result)) {
      $output .= node_view(node_load($node->nid), 1);
    }
    $output .= theme('pager', NULL, variable_get('default_nodes_main', 10), 0);
  }
  else {
    $output .= '<p>'. t('There are currently no posts in this category.') .'</p>';
  }
  return $output;
}
?>

6.x:

<?php
function taxonomy_render_nodes($result) {
  $output = '';
  $num_rows = FALSE;
  while ($node = db_fetch_object($result)) {
    $output .= node_view(node_load($node->nid), 1);

    $num_rows = TRUE;
  }
  if ($num_rows) {
    $output .= theme('pager', NULL, variable_get('default_nodes_main', 10), 0);
  }
  else {
    $output .= '<p>'. t('There are currently no posts in this category.') .'</p>';
  }
  return $output;
}
?>

Remove $row argument from db_result() method

The $row argument of db_result() was removed from the database abstraction layer in 6.x core, as it was a database dependent option. Developers need to use other handling to replace the needs of this method.

Block-level caching

In addition to the page cache, that improves performance when serving pages to anonymous users only, Drupal 6 now has a block-level cache, which improves performance for pages served to logged-in users as well. Site administrators can turn on block caching on the 'Performance' settings page (admin/settings/performance).

When declaring their blocks in hook_block($op = 'list'), modules can specify how each block should be cached:

<?php
/**
* Implementation of hook_block().
*/
function profile_block($op = 'list', $delta = 0, $edit = array()) {
  if ($op == 'list') {
    $blocks[0]['info'] = t('Author information');
    $blocks[0]['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE;
    return $blocks;
  }
  ...
}
?>

The following constants are available:

  • BLOCK_CACHE_PER_ROLE (default)
    The block can change depending on the roles the user viewing the page belongs to.
    This is the default setting, so your module's blocks will be cached 'per role' without any change in your code.
  • BLOCK_CACHE_PER_USER
    The block can change depending on the user viewing the page.
    This setting can be resource-consuming for sites with large number of users, and thus should only be used when BLOCK_CACHE_PER_ROLE is not sufficient.
  • BLOCK_CACHE_PER_PAGE
    The block can change depending on the page being viewed.
  • BLOCK_CACHE_GLOBAL
    The block is the same for every user on every page where it is visible.
  • BLOCK_NO_CACHE
    The block should not get cached. This setting should be used:
    - for simple blocks (notably those that do not perform any db query), where querying the db cache would be more expensive than directly generating the content.
    - for blocks that change too frequently.

The block cache is cleared in cache_clear_all(), and uses the same clearing policy than page cache (node, comment, user, taxonomy added or updated...). Blocks requiring more fine-grained clearing might consider opting-out of the built-in block cache (BLOCK_NO_CACHE) and roll their own.

Note that :
- user 1 is excluded from block caching.
- block caching is disabled when node access modules are used.

Batch operations: Progressbar for heavy computations

The new D6 'batch engine' generalizes the mechanism used by the update.php script (ensuring db updates do not get interrupted by a PHP timeout), and makes it available in the realm of 'regular' Drupal pages for modules that need to perform possibly time-consuming operations.

The processing is automatically spawned over separate requests through AJAX (or HTTP refresh when JS is disabled), while a progressbar informs the user about the completion level.

Typical use cases include: perform a task on every node, import/export data from/to a CSV or XML file, bulk create nodes, etc...

The API is primarily designed to integrate nicely with the Form API workflow, but can also be used by non-FAPI scripts (like update.php), or even simple page callbacks (which should probably be used sparingly for mere user-friendliness).

Modules can set up their batches for processing with the batch_set() function:

$batch = array(
  'title' => t('Exporting'),
  'operations' => array(
    array('my_function_1', array($account->uid, 'story')),
    array('my_function_2', array()),
    // ...

  ),
  'finished' => 'my_finished_callback',
);
batch_set($batch);
// When not in a form submit handler (which are the 'natural' places 
// to set up a  batch processing), you additionally have to manually 
// trigger redirection to the processing / progress bar page, using:
batch_process();

See http://api.drupal.org/api/group/batch/6 for details and advanced uses, and http://drupal.org/node/180528 for an example module.

Node access modules : simplified hook_enable / hook_disable / hook_node_access_records

In D5, modules defining node access permissions via hook_node_grants / hook_node_access_records had to do a bit of hoop jumping : call node_access_rebuild() in their hook_enable / hook_disable, check they're not being disabled before actually returning their grants in hook_node_access_records()...

This is not required in D6 anymore : the system ensures the node grants are rebuilt if needed when modules are enabled or disabled. Therefore, node access modules should *not* call node_access_rebuild themselves on enable/disable.

5.x :

in example_module.install:
==========================
/**
 * Implementation of hook_enable().
 *
 * A node access module needs to force a rebuild of the node access table
 * when it is enabled to ensure that things are set up.
 */
function node_access_example_enable() {
  node_access_rebuild();
}

/**
 * Implementation of hook_disable().
 *
 * A node access module needs to force a rebuild of the node access table
 * when it is disabled to ensure that its entries are removed from the table.
 */
function node_access_example_disable() {
  node_access_example_disabling(TRUE);
  node_access_rebuild();
}

in example_module.module:
=========================
/**
 * Simple function to make sure we don't respond with grants when disabling
 * ourselves.
 */
function node_access_example_disabling($set = NULL) {
  static $disabling = false;
  if ($set !== NULL) {
    $disabling = $set;
  }
  return $disabling;
}

/**
 * Implementation of hook_node_access_records().
 */
function node_access_example_node_access_records($node) {
  if (node_access_example_disabling()) {
    return;
  }
  
  // Actual node access logic.
  // (...)
  
  return $grants;
}

6.x:

in example_module.install:
==========================
Nothing specific (aside from the usual db install and update functions).

in example_module.module:
=========================

/**
 * Implementation of hook_node_access_records().
 */
function node_access_example_node_access_records($node) {
  // Actual node access logic.
  // (...)
  
  return $grants;
}

node_access_rebuild($batch_mode = TRUE) / node_access_needs_rebuild()

To avoid PHP timeouts when rebuilding content access permissions (which can critically leave the site's content half open), node_access_rebuild()now accepts a $batch_mode Boolean parameter (defaults to FALSE), letting it operate in 'progressive' mode using the new D6 batch processing (progressbar processing, à la update.php).

Due to the way the batch API alters the regular page execution workflow (basically ends the current request and redirects to the processing page), the batch mode is not suitable in every execution context. hook_update_N functions and any form submit handlers are safe places to use node_access_rebuild(TRUE). Other contexts (like hook_user, hook_taxonomy, ...) might consider sticking to the 'old style' non-batch mode (node_access_rebuild()).
See http://api.drupal.org/api/function/node_access_rebuild/6

Alternatively, module authors can also use the new node_access_needs_rebuild() function, which does not immediately triggers a rebuild, but simply displays a warning on every page to users with 'access administration pages' permission, pointing to the 'rebuild' confirm form. This, for instance, lets administrators fine-tune their permissions settings and actually go through the (possibly long) rebuild once they're done, instead of rebuilding after each step.
When unsure if the current user is an administrator, node_access_rebuild should be used instead.
See http://api.drupal.org/api/function/node_access_needs_rebuild/6

Upgraded to jQuery 1.2.3

The JavaScript library jQuery has been updated to version 1.2.3. Most likely your scripts will need updating as well. The jQuery documentation contains further information on that.

Deprecated Selectors

$(”div//p”) XPath Descendant Selector  -- replaced with CSS: $(”div p”)
$(”div/p”) XPath Child Selector -- replaced with CSS:  $(”div > p”)
$(”p/../div”) XPath Parent Selector -- replaced with: $(”p”).parent(”div”)
$(”div[p]”) XPath Contains Predicate Selector -- replaced with: $(”div:has(p)”) 
$(”a[@href]”) XPath Attribute Selector -- replaced with CSS: $(”a[href]”)

Deprecated DOM Manipulation

$(”div”).clone(false) -- replaced with: $(”div”).clone().empty()

Deprecated DOM Traversal

$(”div”).eq(0) -- replaced with either: $("div").slice(0,1); -- $("div:eq(0)") -- $("div:first")
$(”div”).lt(2) -- replaced with either: $("div").slice(0,2); -- $("div:lt(2)")
$(”div”).gt(2) -- replaced with either: $("div").slice(3); -- $("div:gt(2)")

Deprecated Ajax functions

$(”#elem”).loadIfModified(”some.php”) -- replaced with: 
    $.ajax({url: "some.php", ifModified: true, success: function(html){$("#elem").html(html);}});
$.getIfModified(”some.php”) -- replaced with: $.ajax({url: "some.php", ifModified: true});
$.ajaxTimeout(3000) -- replaced with: $.ajaxSetup({timeout: 3000});
$(…).evalScripts() -- is no longer needed at all

On the release notes page for jQuery 1.2, there is a more extensive list about removed functionality

Removed several functions from drupal.js

  • Drupal.extend(): this function has been removed in favor of jQuery.extend(Drupal, ...).
  • Drupal.absolutePosition(elem): this functionality is now in jQuery 1.2 in $(elem).offset().
  • Drupal.dimensions(elem): these values can be gathered by calling $(elem).width() or $(elem).height().
  • Drupal.mousePosition: You most likely need these information at a mouse* event. e.pageY resp. e.pageX contain the mouse position relative to the page (e is the first parameter of an event callback).
  • Drupal.parseJson: jQuery supports all kinds of functionality around JSON. If you’re performing an AJAX callback with jQuery.ajax(), set dataType: 'json' and the returned data will be treated as JSON.

    If, for some reason, you still need this function, use data = eval('('+ data +')') to parse data into a JSON object.

The book module has been rewritten to use the new menu system

The book module now makes use of the new Drupal 6.x menu system (e.g. the {menu_links} table) to store and render the book hierarchy. Any modules that previously interfaced with the book module will need to be re-written. All the data loaded onto a node by the book module is now found in the $node->book attribute.

Many of the book module API function have changed. For example, function book_recurse has been removed. For most use cases, this should be replaced by book_export_traverse, but it no longer has a $depth parameter.

new helper function: db_placeholders

Some queries in core were handling data in a potentially insecure way by using the %s placeholder without wrapping it in single quotes - for example:

db_query('UPDATE {node} SET status = 1 WHERE nid IN(%s)', implode(',', $nodes));

all such constructs have been eliminated in core code, and may be considered a potential route for SQL injection in contrib code if they are found.

To facilitate removal of any such IN(%s) usage, the function db_placeholders() is available in 6.x. The default placeholder is for int data, so the above code has been replaced by:

db_query('UPDATE {node} SET status = 1 WHERE nid IN('. db_placeholders($nodes) .')', $nodes);

Comment settings are now per node type

The global comment settings previously located at /admin/content/comment/settings have become per node type settings, and are now located on the content type editing page for each node type (ex. /admin/content/types/page)

Modules that either referenced or set these values now need to append the node type to the setting name:

5.x:

<?php
  $form_location = variable_get('comment_form_location', COMMENT_FORM_SEPARATE_PAGE);
?>

6.x:

<?php
  $form_location = variable_get('comment_form_location_'. $node->type, COMMENT_FORM_SEPARATE_PAGE);
?>

Check node access before emailing content

Modules like Organic Groups and Project Issue send the same content as an email notifications to many users. They should now be using the new 3rd parameter to node_access() to check access on the content before emailing it. Note that db_rewrite_sql() provides no protection because the recipient is not the logged in user who is receiving the content.

Form property #DANGEROUS_SKIP_CHECK removed

Despite pleas from the Drupal For Evil group., this property has been removed. If you needed that flag, the workarounds are listed in this issue. Note that Drupal6 has an AHAH framework which works really nicely with Forms API. Get to know it.

Access control renamed to permissions

The "administer access control" permission has been renamed to "administer permissions"

5.x:

if (user_access('administer access control')) {
  // ...
}

6.x:

if (user_access('administer permissions')) {
  // ...
}

Also admin/user/access has changed to admin/user/permissions, this needs to be updated in help texts.

locale_refresh_cache() has been removed

After the cleanup in locale caching, the function locale_refresh_cache() - previously sometimes used (inconsistently) to flush the cached UI translations - is removed. The cache may be flushed by a simple cache_clear_all() call instead:

Drupal 5:

locale_refresh_cache();

Drupal 6:

cache_clear_all('locale:', 'cache', TRUE);

FormAPI image buttons are now supported

FormAPI now offers the 'image_button' element type, allowing developers to use icons or other custom images in place of traditional HTML submit buttons.

$form['my_image_button'] = array(
  '#type'         => 'image_button',
  '#title'        => t('My button'),
  '#return_value' => 'my_data',
  '#src'          => 'my/image/path.jpg',
);

db_next_id() is gone, and replaced as db_last_insert_id()

Since db_next_id() introduce some problems, and the use of this function can be replaced by database level auto increment handling, the sequences table and db_next_id() is now gone and replaced as db_last_insert_id() with help of serial type under Schema API (check out http://drupal.org/node/149176 for more details). Please refer to drupal_write_record() as demonstration or Taxonomy module.

admin/logs renamed to admin/reports

admin/logs has been renamed to admin/reports, including all subpages. Reports is a more intuitive name because it includes "Status report" and "Available updates", which do not fall under the "logs" category but do fall under "reports". Modules which reference admin/logs or any subpages will need to update their links accordingly.

This change is discussed at #165140.

New helper function: drupal_match_path()

The drupal_match_path() function checks if a path matches any pattern in a set of patterns. The first parameter is the path, the second contains the patterns, separated by \n, \r or \r\n. The wildcard sign is the asterisk.
Note that these "patterns" are not regular expressions. Example set of patterns, separated by newlines:

node/*/edit
foo/bar

Currently in D5, menu_set_location() is 'misused' in several modules to set a custom breadcrumb, drupal_set_breadcrumb() should be used instead, as discussed in #177497.
5.x:

$breadcrumb[] = array('path' => 'blog', 'title' => t('Blogs'));
$breadcrumb[] = array('path' => 'blog/'. $node->uid, 'title' => t("@name's blog", array('@name' => $node->name)));
$breadcrumb[] = array('path' => 'node/'. $node->nid);
menu_set_location($breadcrumb);

6.x:

$breadcrumb[] = l(t('Home'), NULL);
$breadcrumb[] = l(t('Blogs'), 'blog');
$breadcrumb[] = l(t("@name's blog", array('@name' => $node->name)), 'blog/'. $node->uid);
drupal_set_breadcrumb($breadcrumb);

Note, when using drupal_set_breadcrumb(), you need to include "home" but not the current page.

Alternatively, if you do want to set the current location in the menu tree as well as affect breadcrumbs, use menu_set_item().

user_authenticate changed parameters

The parameters of user_authenticate() were changed to unify the invocation of security related functionality. Before this change, user login tasks were not performed consistently. You should at least use the 'name' and 'pass' array keys. You get the user object back if the user can be logged in with the provided name and password. You can also use more keys in the passed array, as this array is used to invoke hook_user op 'login'.

Drupal 5:

if (user_authenticate($name, $pass)) {
  // user logged in
}

Drupal 6:

if (user_authenticate(array('name' => $name, 'pass' => $pass)) {
  // user logged in
}

Automatically add Drupal.settings.basePath

In Drupal 5, you would have to add the base path to Drupal.settings yourself if you needed it (it's needed for just about every AHAH/AJAX enabled module if you did it right).
Now in Drupal 6, it's added automatically. You can always find it at Drupal.settings.basePath (actually, as soon as drupal_add_js() is called at least once, so this is similar to the way we automatically add drupal.js and jquery.js.

In Drupal 5 you had code like

if (arg(0) == 'node' && is_numeric(arg(1)) && ($node = node_load(arg(1)))) {
  // Do something with the node
}

This is buggy because on node/1/revisions/48 it'll load the current revision not the specified revision. Also, it goes against encapsulation and reuse. Now you can have:

if ($node = menu_get_object()) {
  // Do something with the node
}

See API documentation on how to use this for users, feeds etc.

Note that the above example is not exactly the same as the Drupal 5 code, because it will fire on something/%node not just on node/%node. Adding back arg(0) == 'node' fixes this, however most of the time the code example will be what you wanted: while in Drupal 5 you had no way to express "I want to fire every time the second argument is a node id" so we checked for the node/nid path, in Drupal 6 you have a way to do this as described.

hook_access() added a parameter

hook_access() added the $account parameter. In Drupal 5, node modules did this:

function mymodule_access($op, $node) {
  global $user;
  ...
}

In Drupal 6, the $account parameter is passed:

function my_module_access($op, $node, $account) {
 ...
}

It's no longer possible to add javascript to the header using drupal_add_js() from within an implementation of hook_footer(). Adding CSS to the header using drupal_add_css() also doesn't work in hook_footer(). Developers wishing to do this should consider using hook_init() instead.

Translations are looked for in ./translations

Previously, there was a convention of putting .pot and .po files for translations under each module's and theme's ./po subdirectory. Although this was just a common convention, and only Autolocale module was dependent on it, Drupal 6 includes most of Autolocale's functionality and for user friendliness renamed the directory to ./translations. Modules and themes should have their translation templates and translations under the ./translations directory, so that Drupal 6 imports them automatically.

hook_submit() has been removed

In Drupal 5.x, hook_submit() allowed node modules to alter the node before saving it to the database (it was run between hook_validate() and hook_insert()/hook_update()). In Drupal 6.x this hook has been removed. Instead, you should attach a submit handler to the form created in hook_form(), and then alter the $form_state['values'] array as needed.
Drupal 5.x:

<?php
function mymodule_submit(&$node) {
$node->new_field = $node->old_field_1 + $node->old_field_2;
$node->some_field = $node->some_field * 2;
}
?> 

Drupal 6.x:

<?php
function mymodule_form(&$node, &$param) {
$form = array();

// ...

$form['#submit'] = array('mymodule_node_form_submit_handler');

return $form;
}

function mymodule_node_form_submit_handler($form, &$form_state) {
$form_state['values']['new_field'] = $form_state['values']['old_field_1']
+ $form_state['values']['old_field_2'];
$form_state['values']['some_field'] = $form_state['values']['some_field'] * 2;
}
?>

hook_comment() no longer supports the 'form' operation, use hook_form_alter() instead

In Drupal 6.x, hook_comment() no longer supports the 'form' operation. If you want to inject elements into the comment form, use hook_form_alter() instead.

Drupal 5.x:

function mymodule_comment(&$arg, $op) {
  switch ($op) {
    case 'form':
      $form = array();
      $form['field'] = array(
        '#type' => 'checkbox',
        '#title' => t('Some checkbox'),
        ...
      );
      return $form;
    ...
  }
}

Drupal 6.x (Note: this example also makes use of the form_id specific form_alter callback support):

function mymodule_form_comment_form_alter(&$form, &$form_state) {
  $form['field'] = array(
    '#type' => 'checkbox',
    '#title' => t('Some checkbox)',
    ...
  );
}

taxonomy_override_selector variable allows alternate taxonomy form operations

New variable taxonomy_override_selector allows contrib modules to prevent taxonomy.module from loading heavy structures, as in the taxonomy term edit form and the taxonomy fieldset on node edit forms. These modules should then implement alternate versions of these features using hook_form_alter(). There is no UI for this variable.

This change is discussed at #194277: taxonomy select override - split from menu choosers

Comments

tangent’s picture

The nodeapi in node_feed() to add a namespace to feeds is changed.

5.x (note that this implementation contained a bug causing the namespace to be added multiple times. #157709)

function example_nodeapi(&$node, $op) {
  if ($op == 'rss item') {
    return array(array('namespace' => array('xmlns:wfw="http://wellformedweb.org/CommentAPI/"')));
  }
}

6.x

function example_nodeapi(&$node, $op) {
  if ($op == 'rss item') {
    return array(array('namespace' => array('xmlns:wfw' => 'http://wellformedweb.org/CommentAPI/')));
  }
}
Walt Esquivel’s picture

I've created an informal list of plans for contributed modules being ported to 6.x..

The list consists of informal guesstimates by contributors.

ANYONE can easily make updates because each list was created on a wiki page! EVERYONE is encouraged to contribute updates. Simply click on the "Edit" tab (you should see 3 tabs - View, Edit, and Revisions) and then fill in the proper columns with information that others using drupal.org and groups.drupal.org site can depend upon. Please be as accurate as possible.

Thanks in advance for any contributions!!!

Walt Esquivel, MBA; MA; President, Wellness Corps; Captain, USMC (Veteran)
$50 Hosting Discount Helps Projects Needing Financing

beginner’s picture

In hook_form() , the way the body textarea should be declared has changed.
See example here:
http://api.drupal.org/api/function/node_example_form/6

NancyDru’s picture

The node object (or array) that is passed to node_save has a 'taxonomy' attribute array where the vid is the key and the term object is the value. In 5.x term could be an array; in 6.x it MUST be an object.

Nancy W.
Drupal Cookbook (for New Drupallers)
Adding Hidden Design or How To notes in your database

starbow’s picture

When creating your JavaScript behaviors, be sure not to attach events to elements outside of the context. Otherwise the events can pile up every time Drupal.attachBehaviors is called.

Arto’s picture

Haven't seen this change documented yet: hook_submit() is gone, as per http://drupal.org/node/104047#comment-480563. Presumably the current practice is to fold any code that used to live in hook_submit() into either hook_validate() or hook_insert().

The API documentation at http://api.drupal.org/api/function/hook_submit/6 should probably be updated.

emjayess’s picture

_submit functions for non-node forms still apparently fire without attaching a $form['#submit'] handler, can anyone shed some light on what seems like an inconsistency compared to the node hooks?

--
Matthew J. Sorenson (emjayess)
d.o. | g.d.o.

gpk’s picture

In the absence of a submit handler being defined the form submit processing will always try for a form_id_submit() handler. Actually the new approach is I think intended to be more consistent (it could be ambiguous as to whether something_submit() is a hook_submit() implementation or a form submit handler), which I imagine is why the change was made.

In the case of the node form different submit handlers are defined for each button. See http://api.drupal.org/api/function/node_form/6.

rainer_f’s picture

It's mentioned above - but not prominent enough for me.
The files table lost it's nid column. That means there is no link between files and nodes anymore. There's a new upload table - but usage for contributed modules is discouraged.

That's a major change for node module developers - if their modules support fileuploads without using the upload module (there are several reasons why I am doing that).

While this page states that file_check_upload() is kind of deprecated, it's still used in the fileupload example (here).

If your module allows fileuploads, you need to introduce a new table for the nid relation!
Just to point that out.
--
Rainer

NancyDru’s picture

The files table now has a status column to indicate that files are permanent or temporary. Cron will now delete temporary files that are over a day old. Modules need to add file_set_status($file, FILE_STATUS_PERMANENT); to make their uploaded files permanent.

Nancy W.
Drupal Cookbook (for New Drupallers)
Adding Hidden Design or How To notes in your database

jalpa_bhalara’s picture

There is one mistake in function name.

Title : hook_mail_alter() parameters have changed,
In drupal 5 : function my_module_mail_alter.
In drupal 6 : function my_module_profile_alter.

Need to recorrect it.

aaustin’s picture

As I just realized trying to use it, module_load_all_includes does not load all include files, only ones that are named with the convention of the module name or ones that are named by the second argument but not both and definitely not all include files.

I would suggest changing the line:

module_load_all_includes()'s primary difference from module_load_include() is that module_load_all_includes() loads all of the modules include files.

To something more like this:

module_load_all_includes()'s primary difference from module_load_include() is that module_load_all_includes() loads all module include files that share the file name with the module unless another name is specified by the second ($name) argument.

The example of module_load_include('inc', 'node', 'node.pages'); is excellent, as I realized that was exactly what I was trying to do.

NancyDru’s picture

Rather than defaulting to 'files' it is now better to code variable_get('file_directory_path', file_directory_path()).

Nancy W.
Drupal Cookbook (for New Drupallers)
Adding Hidden Design or How To notes in your database

jpulles’s picture

file_directory_path() is sufficient, no need to do another variable_get. See http://api.drupal.org/api/function/file_directory_path/6.

Liam McDermott’s picture

When making the changes in: http://drupal.org/node/114774#theme_registry

Note that the registry is stored in the database cache. So any changes you make to your module's hook_theme() will not be reflected until you TRUNCATE TABLE cache. :)

----
Web Design, GNU/Linux and Drupal.

NancyDru’s picture

Even clearing every cache I have, I find that the only way I can get it to update is to disable and re-enable the module.

add1sun’s picture

All I've done is go to admin/settings/performance and hit the clear cache button. Has worked every time for me.

Lullabot loves you | Be a Ninja, join the Drupal Dojo

Drupalize.Me, The best Drupal training, available all the time, anywhere!

moshe weitzman’s picture

hook_submit and nodeapi submit should be performed in nodeapi(pre_save)

markj’s picture

I didn't see anything mentioned here, but it appears the sequences table has been deprecated in 6.x.

Liam McDermott’s picture

Was wondering the same thing. Have tried asking a couple of times in IRC, but no-one seems to know. Certainly the db_next_id() function no longer exists. node_save() in six appears to do a normal INSERT.

----
Web Design, GNU/Linux and Drupal.

aufumy’s picture

Yes, sequences table has been deprecated, with the help of schema API and the 'serial' data type or auto-increment for mysql.

An example of how to replace db_next_id with db_last_insert_id is shown in the Taxonomy module

Since the id is auto-increment now, it it removed from the insert statement, and if the value is needed, db_last_insert_id is used to retrieve the auto-incremented value.

philipnet’s picture

system_settings_form overrides $form['#theme'] and $form['#submit'][]

In 5.x you would have:

function mymodule_myfunction() {
  $form = array();
  $form['#submit'][] = 'mymodule_myfunction_submit';
    ... //form definition
  return system_settings_form($form);
}

In 6.x you now have:

function mymodule_myfunction() {
  $form = array();
    ...  //form definition
  $form=system_settings_form($form);
  
  $form['#submit'][] = 'mymodule_myfunction_submit';
  $form['#theme'] = 'mymodule_myfunction';
  return $form;
}

function mymodule_theme() {
  return array(
    'mymodule_myfunction' => array(
      'arguments' => array('form' => NULL)
    )
  );
}
NancyDru’s picture

I do it this way with no trouble:

<?php
function mymodule_myfunction() {
  $form = array();
    ...  //form definition
 
  $form['#submit'][] = 'mymodule_myfunction_submit';
  $form['#theme'] = 'mymodule_myfunction';

  return system_settings_form($form);
}

You just need to realize that your submit handler will fire before the system's will.

rmiddle’s picture

taxonomy_node_get_terms and taxonomy_node_get_terms_by_vocabulary no longer use the (nid/vid) instead it uses a full blown node or actually it is node->vid needs to be there.

aufumy’s picture

Diff between drupal5 version and drupal6 version

-    $locales = locale_supported_languages();
-    $locales = $locales['name'];

+    $locales = locale_language_list();
dww’s picture

While working on upgrading one of my modules to D6, I discovered an undocumented API change in hook_nodeapi() during the 'update' op. In D5 (at least), the $node object you get has been fully loaded via a node_load() call in node_save(). Starting in revision 1.889 of node.module (due to #169982: Missing feature from schema API: load/save records based upon schema), this node_load() is now gone from node_save(). So, in D6, the $node object passed in to hook_nodeapi('update') is basically just $form_state['values'] from the node form, not the fully loaded $node object.

Not sure if this is worth adding another point to the list above or not, so I figured I'd at least post it as a comment here and solicit feedback about what to do with this information...

___________________
3281d Consulting

dww’s picture

in D6, system.css now provides a style for anything with the "js-hide" class. This is useful for JS UI where something is conditionally hidden/shown based on another element on the page. It only hides the thing in question when JS is enabled, so it's perfect for degradable UI -- hide it in CSS when JS is enabled, and then show/hide it using JS as needed.

In D5, contrib modules trying to do this had to implement the class themselves in their own .css files. Now, they can just use the "js-hide" class from system.css. See #144919: use jQuery to hide user picture settings when disabled for an example of how this is used in core.

___________________
3281d Consulting

ball.in.th’s picture

Note that in Drupal 6.8, there's a bug in Block-level caching resulting in changes to block caching mode not caught. This means if block cache setting is changed -- say from BLOCK_CACHE_PER_ROLE to BLOCK_NO_CACHE -- during development, you might need to update the block table manually. It took me quite some time to figure this out.
--
http://ball.in.th, http://บอล.th - ชุมชนคอบอลอันดับ 1 (ยังไม่ใช่ แต่เราจะไปให้ถึง 555)

anrikun’s picture

Should Drupal.behaviors be used inside theme's JS too?
I don't think so because the script gets called again at each AJAX request.
Your opinion?