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.