How to Write Drupal Update Scripts

This is the second part in my two-part series about Drupal update scripts, specifically focusing on using update scripts for your custom modules as part of your deployment process. You can read the first post about Why You Should Spend the Extra Time to Write Drupal Update Scripts. Now that we know "why" lets talk about "how" and what better way to demonstrate how then look at real-world examples.

Quick Intro

Update scripts implement the hook_update_N() hook and should be placed in your modules .install file. The examples I'm about to show you are from a real project, I've trimmed some of them for the sake of being brief and names have been changed to protect the innocent. I've also left out several update scripts for the sake of not being redundant.

Examples

Now to the good stuff, Examples! and plenty of them.

Simple Update Script Using variable_set() and module_enable()

This example is probably one of the simplest and most common types of update scripts that I'll write. When working on a new feature, I'll download a new contrib module or maybe write a new custom module. When we deploy this feature we will have to enable that module and then do any setup and configuration for that. Typically there is a "misc" module of some sort that I use to enable the module using module_enable() and then setup our configuration using variable_set().

/**
* Implements hook_update_N().
* Enables search_restrict and sets default content types to not show in search.
*/
function mysite_misc_update_7000() {
  module_enable(array('search_restrict'));
  $restrict = array(
    'marquee' => array(
      -1 => -1,
      1 => 0,
      2 => 0,
      3 => 0,
    ),
    'news_feed' => array(
      -1 => -1,
      1 => 0,
      2 => 0,
      3 => 0,
    ),
  );
  variable_set('search_restrict_content_type', $restrict);
}

Simple Update Script Using Batch API

This next example takes advantage of the Batch API. In this case we wanted to remove a bunch of url aliases that were created by pathauto for content types that won't need one. This can also be used as an example for a potential helper function, since we are removing a bunch of aliases, a path_delete_multiple() function might be a nice to have, but since that doesn't exist, I copied the code from path_delete() and modified it here to meet my needs.

/**
 * Implements hook_update_N().
 * Removes aliases for content types that do not need them.
 */
function mysite_misc_update_7002(&$sandbox) {
  // Set default patterns to empty so that aliases are not generated for every
  // node. You will need to set the pathauto pattern for every content type that
  // you do want to have a generated alias.
  variable_set('pathauto_node_pattern', '');
  // Remove existing aliases for node types: marquee, news_feed
  $types = array(
    'marquee',
    'news_feed',
  );
 
  if (!isset($sandbox['max'])) {
    $count_query = db_select('node', 'n')
      ->condition('n.type', $types, 'IN');
    $count_query->addExpression('COUNT(n.nid)', 'count');
    $sandbox['max'] = $count_query->execute()->fetchField();
    $sandbox['position'] = 0;
  }
 
  $limit = 200;
  $nids = db_select('node', 'n')
    ->condition('n.type', $types, 'IN')
    ->fields('n', array('nid'))
    ->orderBy('n.nid')
    ->range($sandbox['position'], $limit)
    ->execute()
    ->fetchCol();
 
  // Loop through the node id's and build source paths... this is basically what
  // path_delete_multiple() would look like if it existed
  $sources = array();
  foreach ($nids as $nid) {
    $sources[] = 'node/' . $nid;
  }
  unset($nids);
 
  $paths = db_select('url_alias')
    ->condition('source', $sources, 'IN')
    ->fields('url_alias')
    ->execute();
 
  db_delete('url_alias')->condition('source', $sources, 'IN')->execute();
 
  foreach ($paths as $path) {
    $path = (array) $path;
    module_invoke_all('path_delete', $path);
    drupal_clear_path_cache($path['source']);
  }
 
  $sandbox['position'] += $limit;
 
  if ($sandbox['max'] > 0 && $sandbox['max'] > $sandbox['position']) {
    $sandbox['#finished'] = $sandbox['position'] / $sandbox['max'];
  }
  else {
    $sandbox['#finished'] = 1;
  }
}

Simple Update Script to Create a New Node

This one creates a new node and sets it as our 404 page.

/**
 * Implements hook_update_N().
 * Adds a custom 404 page.
 */
function mysite_misc_update_7004() {
  $body = <<<MYSITE_MISC_404_BODY
<div style="text-align:center">
<p>These aren't the droids you're looking for.</p>
 
<p>Perhaps one of these will serve your needs:</p>
 
<a href="/">Home Page</a><br />
<a href="/contact">Contact</a>
</div>
MYSITE_MISC_404_BODY;
 
  $node = (object) array(
    'type' => 'page',
    'language' => 'und',
    'title' => 'Jedi Mind Trick',
    'body' => array(
      'und' => array(
        0 => array(
          'value' => $body,
          'format' => 'advanced_text_editor',
        ),
      ),
    ),
    'path' => array(
      'alias' => '404',
      'pathauto' => 0,
    ),
  );
 
  node_save($node);
 
  variable_set('site_404', '404');
}

Simple Update Script to Replace Default Images

This one is fairly unique. We were changing the default image for several content types which are managed in Features. In order to not override the Features on production and to properly test the changes in a staging environment, I decided that I could upload the new image files as unmanaged files, and then write a script to update the managed files to the new files. This made the most sense to me since Features needs to know what the file.fid is and there is no easy way to be sure of that unless the file already exists on production. I also took the time to remove any default images that were no longer going to be used.

/**
 * Implements hook_update_N().
 * Replaces our default images with the new ones by updating the managed_file,
 * instead of having to upload new files and have overridden features.
 * In order for this to work we need to rsync our new images up to prod as well.
 */
function mysite_misc_update_7005() {
  // Features reports field_article_banner_image 'default_image' => '57'
  // We want to use the 4x3 image here
  $file = file_load(57);
  // Update everything about our file
  $file->filename = '4x3_default.jpg';
  $file->uri = 'public://default_images/4x3_default.jpg';
  $file->filemime = 'image/jpeg';
  $file->filesize = 49937;
  file_save($file);
 
  // Features reports field_blog_image 'default_image' => '84'
  // We want to use the 4x3 image here as well, in this case, we are updating
  // the feature to use 57, and we will delete 84 here.
  $file = file_load(84);
  if (isset($file->fid)) {
    file_delete($file, TRUE);
  }
}

Another Update Script Using Batch API

For this example, we were adding a new field to a content type which was managed in Features, and needed to update some of the nodes to a certain value for this new field. In this case it was all content that was created by a few different users. This example also uses the Batch API.

/**
 * Implements hook_update_N().
 * Updates existing articles that were imported from RSS feeds to check the
 * "Aggregated Content?" checkbox.
 */
function mysite_read_update_7000(&$sandbox) {
  // Users
  $uids = array(
    44,
    45,
  );
 
  if (!isset($sandbox['max'])) {
    $query = db_select('node', 'n');
    $query->addExpression('COUNT(*)', 'count');
    $query->condition('n.uid', $uids, 'IN')
      ->condition('n.type', 'article');
    $sandbox['max'] = $query->execute()->fetchField();
    $sandbox['current_position'] = 0;
  }
 
  if ($sandbox['max'] > 0) {
    $limit = 10;
    $nids = db_select('node', 'n')
      ->fields('n', array('nid'))
      ->condition('n.uid', $uids, 'IN')
      ->condition('n.type', 'article')
      ->orderBy('n.nid')
      ->range($sandbox['current_position'], $limit)
      ->execute()
      ->fetchCol();
    $nodes = node_load_multiple($nids);
    foreach ($nodes as $node) {
      $node->field_aggregated_content[$node->language][0]['value'] = 1;
      node_save($node);
    }
 
    $sandbox['current_position'] += $limit;
    $sandbox['#finished'] = $sandbox['current_position'] / $sandbox['max'];
  }
  else {
    $sandbox['#finished'] = 1;
  }
 
  if ($sandbox['#finished'] >= 1) {
    return format_plural($sandbox['max'], '1 node updated', '@count nodes updated');
  }
}

Simple Update Script that Updates a Block

This example updates a blocks configuration to set the pages. It also updates a link in the menu to go to a different page.

/**
 * Implements hook_update_N().
 * Updates a marquee page settings and the link to the main menu.
 */
function mysite_shows_update_7001() {
  // Set the pages the block should be visible.
  $pages = <<<MY_MARQUEE_PAGES_7001
mypage
mypage/latest
mypage/most-popular
MY_MARQUEE_PAGES_7001;
  db_update('block')
    ->fields(array(
      'pages' => $pages,
    ))
    ->condition('module', 'views')
    ->condition('delta', 'marquee-block_2')
    ->execute();
 
  // Update the link to go to latest
  $link = menu_link_get_preferred('mypage', 'main-menu');
  $link['link_path'] = 'mypage/latest';
  menu_link_save($link);
}

Simple Update Script to Automatically "reset" a Menu Link

This is also a-bit of a special use-case. In this example we had a custom module that defined a path in hook_menu(). We needed to change the path and what would happen is that Drupal does not remove the original link but notices that it has changed from what is in the database and adds a "reset" link from the Menu page. Instead of having to remember to login and click that link after deploying to production, this update script automatically finds that link (and any others that were going to the old path) and resets them, removing the duplicate item in our menu.

/**
 * Implements hook_update_N().
 */
function mysite_schedule_update_7000() {
  // Clear the menu cache
  menu_rebuild();
 
  // Find any links that go to the old path
  $mlids = db_select('menu_links', 'l')
    ->fields('l', array('mlid'))
    ->condition('link_path', 'admin/content/import_schedule')
    ->execute()
    ->fetchCol();
  foreach ($mlids as $mlid) {
    $item = menu_link_load($mlid);
    menu_reset_item($item);
  }
 
  return t('Reset schedule import link.');
}
Filed under: 


Heck yeah, update scripts FTW! Update scripts are great and a really good standard to set. One tip I learned is that if you programmatically create a block in an update script it won't actually show up until you visit the block page, so to get around this call _block_rehash() in your update script.


Thanks a lot for sharing this. I finally have some concrete examples to start playing with and learn doing pro-deployment.


This is a great article on a topic that is incredibly important in ensuring the quality of deployments. Without scripted update scripts you can't truly know how things are going to work in your live environment and you are also reducing yourself to having one shot to get it right. Scripted deploys are critical in being able to make test runs so that you know with much more certainty how things are going to work on your live server. Trying to update a live server without update scripts is akin to trying to put on a play without any rehearsals; it's not likely to go very well, and it may be pretty embarrassing. That's ok for hobby sites, but most professional websites are worthy of more attention.


if i create an upload script, which folder in drupal should i place it??
please solve my query
thank you..


Alfred: You'd be better off using Features to export the change, then using an update script to run features_revert() to revert pieces as needed.


Great article with many helpful examples but do you have a particular method to guess all needed functions, variables, db queries and other operations you must whrite in your update script for each change you make on the site via browser? For example, do you have a method to get update/insert queries launched when you change a configuration on a module cause Devel only shows the Select queries launched to display the current page. And how can one be sure not to miss anything?

Thank you very much,


There's no simple answer, it requires detective work. It often helps to read the module's hook_schema(). Another method I use sometimes is `mysqldump --skip-extended-insert`, then look for the record I want. So for the block example this might output:

INSERT INTO `block` VALUES (77,'block','4','metaltoad',1,-7,'content',0,1,'contact','',-1);

Then I would replace the primary key (77) with NULL, and paste into a db_query() statement.


I want to add a block to an existing module and enabled module. How can I do this with hook_update_n()?

About the Author

Jonathan Jordan, Development Team Lead

Jonathan is primarily a backend developer with several years experience with PHP, but also develops a lot in Python and Javascript. He has worked on projects of all sizes from local small businesses to larger sites including InFocus, LA Phil, The Emmys, and FEARnet. His goal in work and at play is to never stop learning and improving.

He enjoys spending his spare time with his wife and two kids. He is also an amateur poker player and bartender.

Interested? Let's talk.