Converting 6.x modules to 7.x
Overview of Drupal API changes in 7.x
- Permissions are required to have titles and descriptions
- Permissions are no longer sorted alphabetically
- _comment_load() is now comment_load()
- Module .info files must now specify all loadable code files explicitly.
- The hook_menu() and hook_theme() "file" and "file path" keys have been removed.
- New permission tables.
- Use
'#markup'not'#value'for markup. - Comment status values in the database have flipped so they match node status
- Rebuild functions have changed names
- Use defined constant REQUEST_TIME instead of time()
- referer_uri() has been removed
- Some
#processfunctions have been renamed - A completely new database API has been added
- file_validate_extensions() enforces check for uid=1
- file_scan_directory() and drupal_system_listing() use preg regular expressions
- Simpler checking for the node form during hook_form_alter()
- Update functions in .install files must include a Doxygen style comment
- New #input_format to assign input format selection to fields. Changes 'body' field location in node, comment, block, etc.
- Replace drupal_clone() with clone
- Remove $op from hook_nodeapi and hook_user
- In hook_node_info() change 'module' back to 'base' and change 'node' to 'node_content'
- Use absolute path (constructed from DRUPAL_ROOT) when including a file
- File operations that don't affect the database have been renamed
- drupal_set_title() uses check_plain() by default
- "administer nodes" permission split into "administer nodes" and "bypass node access"
Permissions are required to have titles and descriptions
(issue) and (issue) In the implementation of hook_perm(), the returned array format changed.
Drupal 6 supported this format:
<?php
function example_perm() {
return array('administer my module', ...);
}
?>Drupal 7 code should use the following format:
<?php
function example_perm() {
return array(
'administer my module' => array(
'title' => t('Administer my module'),
'description' => t('Perform maintenance tasks for my module'),
),
...
);
}
?>Previously, the values were the permission names; now permission names are the keys and the corresponding value is an array with permission title and description. If you happen to have composite permission names, which contain dynamic values such as content type names, look at the array format generated by node_list_permissions() which is reused by node_perm() and hook_perm() implementations of other core node modules.
Permissions are no longer sorted alphabetically
(issue) Permissions are no longer sorted alphabetically, but are displayed in the order defined in your hook_perm().
The recommended order for permissions is:
- administer-related permissions
- access-related permissions
- other global-level permissions
- create-related permissions
- edit-related permissions
- delete-related permissions
- other
_comment_load() is now comment_load()
(issue) _comment_load() was renamed to comment_load() as it is an external-facing API function.
In addition to the function name itself, any menu items that are using the %_comment wildcard will need to change to %comment.
Module .info files must now specify all loadable code files explicitly.
(issue) Drupal now supports a dynamic-loading code registry. To support it, all modules must now declare their code files in the .info file like so:
name = Node
description = Allows content to be submitted to the site and displayed on pages.
...
files[] = node.module
files[] = content_types.inc
files[] = node.admin.inc
files[] = node.pages.inc
files[] = node.installWhen a module is enabled, Drupal will rescan all declared files and index all functions and classes it finds. That means any class or function that is to be called indirectly (such as a hook) can live outside the .module file, and it will be included on-demand. Classes will be loaded automatically by PHP when they are first accessed. For functions, replace all calls to function_exists() with drupal_function_exists(), which will load the necessary file and return TRUE or FALSE depending on whether the function now exists. In the case of hooks or any code called via module_invoke*(), node_invoke(), etc. that will be done automatically.
The hook_menu() and hook_theme() "file" and "file path" keys have been removed.
In Drupal 6, menu items and theme elements could declare an include file to be loaded on-demand for that page request. In Drupal 7, that is no longer necessary as such files will be loaded automatically by the registry. The file and file path entries should be removed from both hook_menu() and hook_theme() implementations.
New permission tables.
(Issue) The {permission} table is gone, instead we have a {role_permission} which stores (role ID, permission string) pairs. Thus, each permission granted for a given role ID is a separate row in the table. This update is in system.install so that it will already be complete when any other core or contributed module update is running. This should make any alteration of existing permissions much easier.
Use '#markup' not '#value' for markup.
(Issue) The default type for items in forms or other structured array data (e.g. node content) passed to drupal_render() is '#type' => 'markup' In Drupal 6 and earlier, the HTML content was added to the array using the #value attribute. In Drupal 7, this needs to be changed to #markup. This change also applies to those form elements of '#type' => 'item'. This change reduces the confusion between form values and markup and allows the code in drupal_render() to be simplified.
Example 1, from system.admin.inc
In Drupal 6:
<?php
$screenshot ? theme('image', $screenshot, t('Screenshot for %theme theme', array('%theme' => $theme->info['name'])), '', array('class' => 'screenshot'), FALSE) : t('no screenshot');
$form[$theme->name]['screenshot'] = array('#value' => $screenshot);
?>In Drupal 7:
<?php
$screenshot ? theme('image', $screenshot, t('Screenshot for %theme theme', array('%theme' => $theme->info['name'])), '', array('class' => 'screenshot'), FALSE) : t('no screenshot');
$form[$theme->name]['screenshot'] = array('#markup' => $screenshot);
?>Example 2, from contact.pages.inc
In Drupal 6:
<?php
$form['from'] = array(
'#type' => 'item',
'#title' => t('From'),
'#value' => check_plain($user->name) . ' <' . check_plain($user->mail) . '>',
);
?>In Drupal 7:
<?php
$form['from'] = array(
'#type' => 'item',
'#title' => t('From'),
'#markup' => check_plain($user->name) . ' <' . check_plain($user->mail) . '>',
);
?>Comment status values in the database have flipped so they match node status
This handy table shows the status indicator and what the numeric values are:
| Version | Type | Published | Unpublished |
|---|---|---|---|
| 6.x and before | Nodes | 1 | 0 |
| 6.x and before | Comments | 0 | 1 |
| 7.x | Nodes | 1 | 0 |
| 7.x | Comments | 1 | 0 |
The major change is to queries and code that interacts with comments.
If you had code that looked something like this in Drupal 6.x or before:
<?php
// Get a list of all unpublished comment IDs.
$results = db_query("SELECT cid FROM {comments} WHERE status = 1");
?>Here's what it should be in 7.x
<?php
// Get a list of all unpublished comment IDs.
$results = db_query("SELECT cid FROM {comments} WHERE status = 0");
?>You may also want to use the constants that are defined in the comment module for increased code clarity and future-proofing:
<?php
// Get a list of all unpublished comment IDs.
$results = db_query("SELECT cid FROM {comments} WHERE status = %d", COMMENT_NOT_PUBLISHED);
?>Rebuild functions have changed names
6.x:
<?php
drupal_rebuild_theme_registry();
drupal_rebuild_code_registry();
?>7.x:
<?php
drupal_theme_rebuild();
registry_rebuild();
?>Use defined constant REQUEST_TIME instead of time().
(Issue) For improved performance, it is highly recommended that any calls to time() are replaced with REQUEST_TIME, a defined constant which will always return the UNIX timestamp from the start of the current request. If you absolutely need to get the current time, you can still use time() but it is not recommended.
referer_uri has been removed.
(Issue) referer_uri() has been removed and replaced with the PHP-provided global variable $_SERVER['HTTP_REFERER']. If there is no referrer, Drupal will automatically set $_SERVER['HTTP_REFERER'] to an empty string.
Some #process functions have been renamed
(Issue) Some functions used for processing form elements (specified by the #form item in the Forms API) have been renamed to unify their overall naming. The follow changes occured:
| Old Name | New Name |
|---|---|
expand_password_confirm |
form_process_password_confirm |
expand_date |
form_process_date |
expand_radios |
form_process_radios |
form_expand_ahah |
form_process_ahah |
expand_checkboxes |
form_process_checkboxes |
process_weight |
form_process_weight |
If your module defines a new element using hook_elements(), you have to update your #process callbacks.
A completely new database API has been added
(Issue) Drupal 7 introduces a completely new database API, utilizing a number of dynamic query builders and formal prepared statements. The following conversion guide should cover most basic cases, but reading the full documentation is recommended.
Normal Select queries:
<?php
// Drupal 6
$result = db_query("SELECT nid, title FROM {node} WHERE uid=%d AND created < %d", 5, REQUEST_TIME);
// Drupal 7
$result = db_query("SELECT nid, title FROM {node} WHERE uid = :uid AND created < :created", array(
':uid' => 5,
':created' => REQUEST_TIME,
));
?>Iterating a result set from db_query()
<?php
// Drupal 6
while ($record = db_fetch_object($result)) {
// Do stuff with $record, which is an object
}
// Drupal 7
foreach ($result as $record) {
// Do stuff with $record, which is an object
}
?>Insert statements
<?php
// Drupal 6
db_query("INSERT INTO {mytable} (intvar, stringvar, floatvar) VALUES (%d, '%s', %f)", 5, 'hello world', 3.14);
$id = db_last_insert_id();
// Drupal 7
$id = db_insert('mytable')
->fields(array(
'intvar' => 5,
'stringvar' => 'hello world',
'floatvar' => 3.14,
))
->execute();
?>Update statements
<?php
// Drupal 6
db_query("UPDATE {node} SET title='%s', status=%d WHERE uid=%d", 'hello world', 1, 5);
// Drupal 7
db_update('node')
->fields(array('title' => 'hello world', 'status' => 1))
->condition('uid', 5)
->execute();
?>Delete statements
<?php
// Drupal 6
db_query("DELETE FROM {node} WHERE uid=%d AND created < %d", 5, time() - 3600);
// Drupal 7
db_delete('node')
->condition('uid', 5)
->condition('created', time() - 3600, '<')
->execute();
?>For more examples, see the unit tests.
file_validate_extensions() enforces check for uid=1
(issue) In 6.x the function for validating file extensions would bypass this check for the uid=1 user. This has been removed in 7.x. If your module depended on this behavior you need to check the user's uid and conditionally specify the file_validate_extensions() validator.
file_scan_directory() and drupal_system_listing() use preg regular expressions
(issue) The file_scan_directory() and drupal_system_listing() function now use preg_match() rather than ereg() which allows case-insensitive matching and speed improvements. As a result the formats of the regular expressions these functions accept as parameters have changed.
For most modules the change is as simple as adding a leading and trailing / (forward slash).
Drupal 6.x:
<?php
// Get current list of modules
$files = drupal_system_listing('\.module$', 'modules', 'name', 0);
?>Drupal 7.x:
<?php
// Get current list of modules
$files = drupal_system_listing('/\.module$/', 'modules', 'name', 0);
?>Modules with more complicated regular expressions should review the PHP documentation for the ereg() and preg_match() functions.
Easier check for node form during hook_form_alter()
Modules wishing to alter the node form may now check for !empty($form['#node_edit_form']) instead of the verbose if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id). See #161301: Make checking for node edit forms easier
Update functions in .install files must include a Doxygen style comment
The Doxygen style comment is displayed to administrators on update.php. Background info - #286035: Remove update.php number dropdowns
New #input_format to assign input format selection to fields. Changes 'body' field location in node, comment, block, etc.
(issue) In Drupal 6, you have been assigning input formats to textareas by wrapping the textarea into a parent element, and adding a filter format selector sibling element, such as:
<?php
$form['comment_filter']['comment'] = array(
'#type' => 'textarea',
'#title' => t('Comment'),
'#rows' => 15,
'#default_value' => $default,
'#required' => TRUE,
);
if (!isset($edit['format'])) {
$edit['format'] = FILTER_FORMAT_DEFAULT;
}
$form['comment_filter']['format'] = filter_form($edit['format']);
?>There was no standard for naming, parenting the format selector, so it was impossible to identify textareas with input formats attached (compared to plain text input widgets). It was also common to use this construct with a tree structure, so you'd get ['comment_filter']['comment'] and ['comment_filter']['filter'] values or a non-tree structure so that you get ['comment'] and ['format'].
In Drupal 7, this is all strictly defined and automated with the introduction of the #input_format FAPI property. Just specify the input format used by default for the field and Drupal will automatically expand the element to a 'value' and a 'format' element later, so the above code becomes:
<?php
$form['comment'] = array(
'#type' => 'textarea',
'#title' => t('Comment'),
'#rows' => 15,
'#default_value' => $default,
'#input_format' => isset($edit['format']) ? $edit['format'] : FILTER_FORMAT_DEFAULT,
'#required' => TRUE,
);
?>This is way simpler, and Drupal 7 takes care of the rest. #input_format should have the value of the default input format to use with the selector. Through the form processing, Drupal transform this structure to have 'value' and 'format' children in form_process_input_format(). Although the structure results in ['comment']['value'] and ['comment']['format'] respectively, the $form_values array with the submitted form data contains the comment value in ['comment'] and the format in ['comment_format']. This lets you swap between formatted and non-formatted input easily, without rewriting your code to more complex structures. This new format also lets WYSIWYG editors to attach themselves to widgets with input formats they support.
Replace drupal_clone() with clone
(issue) Since Drupal 7 requires at least PHP 5, it allowed the removal of the drupal_clone function for a direct call to clone.
Drupal 6.x:
<?php
// Make a copy of the node
$cloned_node = drupal_clone($node);
?>Drupal 7.x:
<?php
// Make a copy of the node
$cloned_node = clone $node;
?>Remove $op from hook_nodeapi and hook_user
(hook_nodeapi issue, hook_user issue) Due to the addition of the registry, we can dynamically load a bunch of granular hooks instead of just one huge hook with an operation argument. Gábor posted a note in the developer mailing list about this if you'd like to read more about it.
With this addition, we can remove the $op argument and have hook_hookname_operationname instead...
Drupal 6.x:
<?php
/**
* Implementation of hook_nodeapi().
*/
function book_nodeapi(&$node, $op, $teaser, $page) {
switch ($op) {
case 'load':
// Load a book
}
}
/**
* Implementation of hook_user().
*/
function block_user($type, $edit, &$account, $category = NULL) {
switch ($type) {
case 'form':
// Construct the user form
}
}
?>Drupal 7.x:
<?php
/**
* Implementation of hook_nodeapi_load().
*/
function book_nodeapi_load(&$node, $teaser, $page) {
// Load a book
}
/**
* Implementation of hook_user_form().
*/
function block_user_form(&$edit, &$account, $category = NULL) {
// Construct the user form
}
?>In hook_node_info() change 'module' back to 'base' and change 'node' to 'node_content
(issue) between Drupal 4.7 and Drupal 5, the 'base' attribute in hook_node_info() was changed to 'module'. However, this is confusing for developers, since there is no requirement that this attribute correspond to the name of any module, and there may be multiple node types defined per module. This change reverts to using 'base'.
In addition, the content types managed by the node module had a 'module' (now 'base') attribute of 'node'. However, to prevent conflicts with the node module's own hooks, this was selectively altered when invoking hooks to 'node_content'. For example, they have always used node_content_form(). By simply assigning the 'base' attribute of these types to be 'node_content' in the database, we can eliminate the error-prone coercing of these attributes at each hook invocation.
The helper function _node_type_set_defaults() is now a more useful public function node_type_set_defaults().
Drupal 6.x:
<?php
/**
* Implementation of hook_node_info().
*/
function blog_node_info() {
return array(
'blog' => array(
'name' => t('Blog entry'),
'module' => 'blog',
'description' => t('A <em>blog entry</em> is a single post to an online journal.'),
);
}
?>Drupal 7.x:
<?php
/**
* Implementation of hook_node_info().
*/
function blog_node_info() {
return array(
'blog' => array(
'name' => t('Blog entry'),
'base' => 'blog',
'description' => t('A <em>blog entry</em> is a single post to an online journal.'),
);
}
?>Saving to the DB a node-type to be managed by node module:
Drupal 6.x:
<?php
function _book_install_type_create() {
$book_node_type = array(
'type' => 'book',
'name' => t('Book page'),
'module' => 'node',
'description' => t('A <em>book page</em>.'),
'custom' => TRUE,
'modified' => TRUE,
'locked' => FALSE,
);
$book_node_type = (object)_node_type_set_defaults($book_node_type);
node_type_save($book_node_type);
}
?>Drupal 7.x:
<?php
function _book_install_type_create() {
$book_node_type = array(
'type' => 'book',
'name' => t('Book page'),
'base' => 'node_content',
'description' => t('A <em>book page</em>.'),
'custom' => 1,
'modified' => 1,
'locked' => 0,
);
$book_node_type = node_type_set_defaults($book_node_type);
node_type_save($book_node_type);
}
?>Use absolute path (constructed from DRUPAL_ROOT) when including a file
(issue) When including a file (via include(), include_once(), require() or require_once()) specify the absolute path to the file, using the named constant DRUPAL_ROOT to give the absolute path to the root of the Drupal installation.
Drupal 6.x:
<?php
// Allow specifying special cache handlers in settings.php, like
// using memcached or files for storing cache information.
require_once variable_get('cache_inc', './includes/cache.inc');
?>Drupal 7.x:
<?php
// Allow specifying special cache handlers in settings.php, like
// using memcached or files for storing cache information.
require_once DRUPAL_ROOT . '/' . variable_get('cache_inc', 'includes/cache.inc');
?>In addition, a standalone script that requires access to Drupal will need to define DRUPAL_ROOT before including bootstrap.inc and invoking drupal_bootstrap(), e.g. for a script located in the Drupal root folder:
<?php
/**
* Root directory of Drupal installation.
*/
define('DRUPAL_ROOT', dirname(realpath(__FILE__)));
?>This will need to be modified accordingly for a script located outside the Drupal root folder.
File operations that don't affect the database have been renamed
(issue) The file_copy(), file_move(), file_delete() and file_save_data() functions from Drupal 6 have been renamed to file_unmanaged_copy(), file_unmanaged_move(), file_unmanaged_delete() and file_unmanaged_save_data().
file_copy() and file_move() now return the new path rather than assigning it by reference.
Summary of File API changes
| Drupal 6 | Drupal 7 | Description |
|---|---|---|
| file_copy() | file_unmananged_copy() | Copy a file to a new location without saving a record in the database. |
| n/a | file_copy() | Copies a file to a new location and adds a file record to the database. Also invokes hook_file_copy() so that other modules may act on the copy action. |
| file_move() | file_unmananged_move() | Move a file to a new location but make no changes to the database. |
| n/a | file_move() | Move a file to a new location and update the file's database entry. Also invokes hook_file_move() so that other modules may act on the move action. |
| file_delete() | file_unmananged_delete() | Delete a file. |
| n/a | file_delete() | Delete a file and its database record. Also involes hook_file_delete() to let other modules perform clean-up actions when file is deleted. |
| file_save_data() | file_unmananged_save_data() | Save a string to the specified destination but makes no changes to the database. |
| n/a | file_save_data() | Save a string to the specified destination and create a database file entry. |
| n/a | file_load() | Load a file object from the database. Also invokes hook_file_load() to allow other modules to do things as the file is loaded. |
| n/a | file_validate() | Check that a file meets the criteria specified by the validators. Accepts an associative array of callback functions used to validate the file. Also calls hook_file_validate() to let other modules perform validation on the new file. |
| n/a | file_save() | Save a file object to the database. Calls either hook_file_insert() or hook_file_update(), depending on whether a $file->fid is specified. |
file_copy()
Drupal 6.x:
<?php
file_copy($source, $paths['target'] . $base);
$paths['files'][] = $source;
?>Drupal 7.x:
<?php
$filepath = file_unmanaged_copy($source, $paths['target'] . $base);
$paths['files'][] = $filepath;
?>file_delete()
Drupal 6.x:
<?php
file_delete($file_path);
?>Drupal 7.x:
<?php
file_unmanaged_delete($file_path);
?>file_save_data()
Drupal 6.x:
<?php
if (file_save_data($data, $dest)) {
$language->javascript = $data_hash;
$status = ($status == 'deleted') ? 'updated' : 'created';
}
?>Drupal 7.x:
<?php
if (file_unmanaged_save_data($data, $dest)) {
$language->javascript = $data_hash;
$status = ($status == 'deleted') ? 'updated' : 'created';
}
?>drupal_set_title() uses check_plain() by default
(issue) drupal_set_title() now uses check_plain() on the title text by default. To pass through text that has already been sanitized (e.g. using check_plain(), or the % or @ placeholders in t()), then supply the contstant PASS_THROUGH as the second argument. If you are currently calling check_plain(), that can be removed to avoid double-escaping.
For example:
Drupal 6.x:
<?php
drupal_set_title(check_plain($node->title));
?>Drupal 7.x:
<?php
drupal_set_title($node->title);
?>Drupal 6.x:
<?php
drupal_set_title(t("@name's blog", array('@name' => $account->name)));
?>Drupal 7.x:
<?php
drupal_set_title(t("@name's blog", array('@name' => $account->name)), PASS_THROUGH);
?>"administer nodes" permission split into "administer nodes" and "bypass node access"
(issue) The "administer nodes" permission has been split into two separate permissions, "administer nodes" and "bypass node access".
The "administer nodes" permission now grants the ability to alter all data associated with a given node, such as menu items, publication status and path aliases, provided that the user already has permission to modify the node. Unlike in 6.x and earlier, this permission does not allow the user to view or edit all nodes on a site.
The "bypass node access" permission grants the ability to view, edit or delete all nodes, without regard for any status checks made by hook_access or the node access system. The "bypass node access" check overrides any other checks made by permissions such as "edit page nodes". However, users with this permission will not automatically be able to edit all attributes of a given node (such as menu item, publication status and path alias), unless their other permissions allow them to do so.
When upgrading a site from 6.x to 7.x, roles with the "administer nodes" permission are automatically given the "bypass node access" permission as well, since having the "administer nodes" permission in 6.x is equivalent to having both in 7.x.
For modules that used the old "administer nodes" permission for access checks, you may now need to switch to "bypass node access" or to both permissions, for access checks.

$_SERVER['REQUEST_TIME']
Any module that already requires PHP 5.1+ can go ahead and make this change even in 6.x.
It has now been renamed to a
It has now been renamed to a global constant REQUEST_TIME, therefore people can not change this in D6.
Patch #305566
The 'administer nodes' permission has been split into two separate permissions, 'administer nodes' and 'bypass node access.'
The 'administer nodes' permission now grants the ability to alter all data associated with a given node, such as menu items, publication status and path aliases. However, this permission does not assume that the user can view or edit all nodes on a site.
The 'bypass node access' permission grants the ability to view, edit or delete all nodes, without regard for any status checks made by hook_access or the node access system. The 'bypass node access' check overrides any other checks made by permissions such as 'edit page nodes'.
For sites and modules that used the old 'administer nodes' permission for access checks, you may now need to switch to 'bypass node access' for access checks or both permissions to existing roles.
--
http://ken.therickards.com
I've added
I've added http://drupal.org/node/224333#bypass_node_access, which is a slightly modified/amplified version of your text above. Are my changes/"improvements" correct?
gpk
----
www.alexoria.co.uk