Search by Content Type in Drupal 8 - a custom module tutorial
At the time of this writing, the Search API module is still in alpha phase. A note from the module maintainers warn that it is unstable. This blog...
This is the second part in my two-part series about Drupal update scripts, specifically focusing on using update scri
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.
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.
Now to the good stuff, Examples! and plenty of them.
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); }
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; } }
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'); }
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); } }
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'); } }
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); }
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.'); }
At the time of this writing, the Search API module is still in alpha phase. A note from the module maintainers warn that it is unstable. This blog...
I'm a big fan of having an automated deployment process. It's really the web development analog to the "one step build process", as described in the...
Wait, is it "$node->title" or "$node->title->value"? How do I write an EntityQuery again? Yeah, I can never remember, either.
Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.