Converting 5.x modules to 6.x
Overview of Drupal API changes in 6.x
- Entirely new menu system
- Major FormAPI improvements
- New Schema API
- New format for hook_install()
- New format for hook_uninstall()
- New format for hook_update_N()
- The arguments to
url()andl()have changed - Variable names can now be 128 characters long
- Taxonomy terms are now associated with node revisions, not just nodes
format_plural()accepts replacements- New drupal_alter() function for developers
- New module_load_include() and module_load_all_includes() functions for developers
- hook_form_alter() parameters have changed
- hook_link_alter() parameters have changed
- hook_profile_alter() parameters have changed
- hook_mail_alter() parameters have changed
- $locale became $language
- New hook_theme() registry
- template_preprocess_* with .tpl.php files
- node/add is now menu generated
- New watchdog hook, logging and alerts
- Parameters of watchdog() changed
- new hook_update_N naming convention
- New syntax for .info files
- Core compatibility now specified in .info files
- PHP compatibility now specified in .info files
- New db_column_exists() method
- cache_set parameter order has changed
- Cache set and get automatically (un)serialize complex data types
- node_revision_list() now returns keyed array
- New operation in image.inc:
image_scale_and_crop() - New user_mail_tokens() method
- New ip_address() function when working behind proxies
- {files} table changed
- file_check_upload() merged into file_save_upload()
- {file_revisions} table is now {upload}
- drupal_add_css() supports automatic RTL CSS discovery
- Node previews and adding form fields to the node form
- JavaScript behaviors: new approach to attaching behaviors
- JavaScript themeing
- Translation of JavaScript files
- JavaScript aggregation
- custom_url_rewrite() replaced
- hook_user('view'), hook_profile_alter() and profile theming
- Distributed Authentication changes
- hook_help() parameters are changed
- Change "Submit" to "Save" on buttons
- node_feed() parameters changed
- hook_nodeapi('submit') has been replaced by op='presave'
- taxonomy_get_vocabulary() changed to taxonomy_vocabulary_load()
- hook_init() is split up into hook_init() and hook_boot()
- Remove db_num_rows() method
- Remove $row argument from db_result() method
- Block-level caching
- db_query() must be used correctly
- Batch operations : progressbar for heavy computations
- Node access modules : simplified hook_enable / hook_disable / hook_node_access_records
- node_access_rebuild($batch_mode = TRUE) / node_access_needs_rebuild()
- Upgraded to jQuery 1.2.3
- Removed several functions from
drupal.js - The book module has been rewritten to use the new menu system
- new helper function: db_placeholders()
- Comment settings are now per node type
- Check node access before emailing content
- form property #DANGEROUS_SKIP_CHECK removed
- "Access control" renamed to "permissions"
- locale_refresh_cache() has been removed
- FormAPI image buttons are now supported
- db_next_id() is gone, and replaced as db_last_insert_id()
- admin/logs renamed to admin/reports
- New helper function: drupal_match_path()
- drupal_mail() parameters have changed
- New hook: hook_mail
- Use drupal_set_breadcrumb() instead of menu_set_location() to set custom breadcrumbs
- user_authenticate() changed parameters
- Automatically add Drupal.settings.basePath
- Get an object relevant on specific paths
- hook_access() added parameter
- Can't add javascript to header in hook_footer()
- Translations are looked for in ./translations
- hook_submit() has been removed
New menu system
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 book.install)
<?php
/**
* Implementation of hook_install().
*/
function book_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 book.install)
<?php
/**
* Implementation of hook_install().
*/
function book_install() {
// Create tables.
drupal_install_schema('book');
}
?>(in book.install)
<?php
/**
* Implementation of hook_schema().
*/
function book_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:
<?php
/**
* Implementation of hook_uninstall().
*/
function book_uninstall() {
db_query('DROP TABLE {book}');
}
?>6.x:
<?php
/**
* Implementation of hook_uninstall().
*/
function book_uninstall() {
// Remove tables.
drupal_uninstall_schema('book');
}
?>New format for hook_update_N()
5.x:
<?php
/**
* 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 owernship 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:
<?php
/**
* 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 corect 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:
<?php
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:
<?php
url("user/$account->uid", NULL, NULL, TRUE)
?>6.x:
<?php
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()andtaxonomy_node_get_terms_by_vocabulary()now take a full $node object, not just a nid (node id).taxonomy_node_delete()takes a full $node object, not just a nid (node id) and 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:
<?php
format_plural($count, $singular, $plural, $args = array());
?>For example, 5.x and before:
<?php
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:
<?php
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:
<?php
$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.
$typespecifies the desired file's extension. For the below example case'inc'$modulespecifies 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 thenode.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.
$typespecifies 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:
<?php
function my_module_form_alter($form_id, &$form) {
// Alter the form...
}
?>Drupal 6:
<?php
function my_module_form_alter(&$form, $form_state, $form_id) {
// Alter the form...
}
?>hook_link_alter() parameters have changed
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:
<?php
function my_module_link_alter($node, &$links) {
// Alter the links...
}
?>Drupal 6:
<?php
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:
<?php
function my_module_profile_alter($account, &$fields) {
// Alter the fields...
$fields['abc'] = ...
}
?>Drupal 6:
<?php
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:
<?php
function my_module_mail_alter(&$mailkey, &$to, &$subject, &$body, &$from, &$headers) {
// Alter the individual params...
$to = 'mail@example.com';
}
?>Drupal 6:
<?php
function my_module_profile_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:
<?php
function something_theme() {
return array(
'something_format' => array(
'arguments' => array('content'),
),
);
}
?>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:
<?php
function something_theme() {
return array(
'something_format' => array(
'file' => 'something.inc',
'template' => 'something-format',
'arguments' => array('content'),
),
);
}
?>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:
<?php
/*
* 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:
<?php
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, Referer, IP address, and timestamp. Messages are translateable 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:
<?php
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 messagesAs 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:
<?php
watchdog('debug', 'My debug message here');
?>You should use (see also below for the parameter changes of watchdog()):
<?php
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:
<?php
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:
<?php
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.
<?php
// 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:
<?php
/**
* 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.
Version 1.5 of a contributed module compatible with Drupal 6.x would name its first update function like so:
<?php
function example_update_6100() {
// Stuff.
}
?>Version 2.4 of this same contributed module would name its third update function like so:
<?php
function example_update_6204() {
// Stuff.
}
?>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 incldues/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 existance of the .info files. For 6.x and beyond, the .info file must specify which Drupal core compatiblity 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.xPlease 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 CVS, it helps if you commit this change to the .info files for your modules. This is also a good way to indicate to users of each module what version of core the HEAD of CVS is compatibile with at any given time.
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.1That 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 conform 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
<?php
cache_set('example_cid', 'cache', $data);
?>6.x
<?php
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:
<?php
$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');
$complex = unserialize(cache_get('complex_cid'));
?>6.x:
<?php
$simple = 'Simple text';
$complex = array( 'one', 'two' );
cache_set('simple_cid', $simple);
cache_set('complex_cid', $complex);
//..
$simple = cache_get('simple_cid');
$complex = cache_get('complex_cid');
?>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 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:
<?php
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:
<?php
$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 ther 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 CVS 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:
<?php
function node_help($section) {
switch ($section) {
case 'admin/help#node':
...
?>6.x:
<?php
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 paramter $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:
<?php
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:
<?php
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 labelling 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 slighly 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 programatically 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:
<?php
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:
<?php
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 = 