9 minute read Published: Author: Derek Laventure
Drupal Planet , Drupal , OpenSocial


In Drupal 7, hook_update()/hook_install() were well-established mechanisms for manipulating the database when installing a new site or updating an existing one. Most of these routines ended up directly running SQL against the database, where all kinds of state, configuration, and content data lived. This worked reasonably well if you were careful and had a good knowledge of how the database schema fit together, but things tended to get complicated.

With the maturing of Features module, we were able to move some of this into configuration settings via the ctools-style export files, making the drush feature-revert command part of standard workflow for deploying new features and updates to an existing site.

In Drupal 8, we’ve made huge strides in the direction of Object Orientation, and started to separate Configuration/State, Content Structure, and Content itself. The config/install directory is often all that’s needed in terms of setting up a contributed or custom module to work out of the box, and with the D8 version of Features, the same is often true of updates that involve straightforward updates to configuration .yml files.

It turns out that both hook_update() and hook_install() are still valuable tools in our box, however, so I decided to compile some of the more complicated D8 scenarios I’ve run across recently.

Drupal 8 Update basics

The hook_update_N API docs reveal that this function operates more or less as before, with some excellent guidelines for how to approach the body of the function’s implementation. The Introduction to update API handbook page provides some more detail and offers some more guidance around the kinds of updates to handle, naming conventions, and adding unit tests to the your update routines.

The sub-pages of that Handbook section have some excellent examples covering the basics:

All of these provided a valuable basis on which to write my own real-life update hooks, but I found I still had to combine various pieces and search through code to properly write these myself.

Context

We recently launched our first complex platform based on Drupal 8 and the excellent OpenSocial, albeit heavily modified to suit the particular requirements of the project. The sub-profile required more extensive customization than simply extending the parent profile’s functionality (as discussed here). Instead, we needed to integrate new functionality into that provided by the upstream distribution, and this often resulted in tricky interactions between the two.

Particularly with a complex site with many moving parts, we take the approach of treating the site as a system or platform, installing and reinstalling regularly via a custom installation profile and set of feature modules. This allows us to integrate:

  • a CI system to build the system repeatedly, proving that everything works
  • a Behat test suite to validate the behaviour of the platform matches the requirements

In the context of a sub-profile of OpenSocial, this became complicated when the configuration we wanted to customize actually lived in feature modules from the upstream profile, and there was no easy way to just override them in our own modules’ config/install directories.

We developed a technique of overriding entire feature modules within our own codebase, effectively forking the upstream versions, so that we could then modify the installed configuration and other functionality (in Block Plugins, for example). The trouble with this approach is that you have to manage the divergence upstream, incorporating new improvements and fixes manually (and with care).

Thus, in cases where there were only a handful of configuration items to correct, we began using hook_install() routines to adjust the upstream-installed config later in the install process, to end up with the setup we were after.

Adjust order of user/register form elements

We make use of entity_legal for Terms of Service, Privacy Policy, and User Guidelines documents. Our installation profile’s feature modules create the 3 entity legal types, but we needed to be able to tweak the order of the form elements on the user/register page, which is a core entity_form_display created for the user entity.

To achieve this using YAML files in the config/install directory per usual seemed tricky or impossible, so I wrote some code to run near the end of the installation process, after the new legal_entity types were created and the core user.register form display was set. This code simply loads up the configuration in question, makes some alterations to it, and then re-saves:

/**
 * Implements hook_install().
 */
function example_install() {
  _example_install_adjust_legal_doc_weights();
}

/**
 * Adjust weights of legal docs in user/register form.
 */
function example_update_8001() {
  _example_install_adjust_legal_doc_weights();
}

/**
 * Ensure the field weights on the user register form put legal docs at the bottom
 */
function _example_install_adjust_legal_doc_weights() {
       $config = \Drupal::getContainer()->get('config.factory')->getEditable('core.entity_form_display.user.user.register');
       $content = $config->get('content');

       $content['private_messages']['weight'] = 0;
       $content['account']['weight'] = 1;
       $content['google_analytics']['weight'] = 2;
       $content['path']['weight'] = 3;
       $content['legal_terms_of_service']['weight'] = 4;
       $content['legal_privacy_policy']['weight'] = 5;
       $content['legal_user_guidelines']['weight'] = 6;
       $config->set('content', $content)->save();
}

Modify views configuration managed by upstream (or core)

A slightly more complicated situation is to alter a views configuration that is managed by an upstream feature module during the installation process. This is not an ideal solution, but currently it’s quite challenging to properly “override” configuration that’s managed by a “parent” installation profile within your own custom sub-profile (although Config Actions appears to be a promising solution to this).

As such, this was the best solution I could come up with: essentially, run some code very nearly at the end of the installation process (an installation profile task after all the contrib and feature modules and related configuration are installed), that again loads up the views configuration, changes the key items needed, and then re-saves it.

In this case, we wanted to add a custom text header to a number of views, as well as switch the pager type from the default “mini” type to “full”. This required some thorough digging into the Views API and related code, to determine how to adjust the “handlers” programmatically.

This helper function lives in the example.profile code itself, and is called via a new installation task wrapper function, which passes in the view IDs that need to be altered. Here again, we can write trivial hook_update() implementations that call this same wrapper function to update existing site instances.

/**
 * Helper to update views config to add header and set pager.
 */
function _settlement_install_activity_view_header($view_id) {
  # First grab the view and handler types
  $view = Views::getView($view_id);
  $types = $view->getHandlerTypes();

  # Get the header handlers, and add our new one
  $headers = $view->getHandlers('header', 'default');

  $custom_header = array(
    'id' => 'area_text_custom',
    'table' => 'views',
    'field' => 'area_text_custom',
    'relationship' => 'none',
    'group_type' => 'group',
    'admin_label' => '',
    'empty' => '1',
    'content' => '<h4>Latest Activity</h4>',
    'plugin_id' => 'text_custom',
    'weight' => -1,
  );
  array_unshift($headers, $custom_header);

  # Add the list of headers back in the right order.
  $view->displayHandlers->get('default')->setOption($types['header']['plural'], $headers);

  # Set the pager type to 'full'
  $pager = $view->getDisplay()->getOption('pager');
  $pager['type'] = 'full';
  $view->display_handler->setOption('pager', $pager);

  $view->save();
}

Of particular note here is the ordering of the Header components on the views. There was an existing Header on most of the views, and the new “Latest Activity” one needed to appear above the existing one. Initially I had tried creating the new custom element and calling ViewExecutable::setHandler method instead of the more complicated $view->displayHandlers->get('default')->setOption() construction, which would work, but consistently added the components in the wrong order. I finally found that I had to pull out a full array of handlers using getHandlers(), then array_unshift() the new component onto the front of the array, then put the whole array back in the configuration, to set the order correctly.

Re-customize custom block from upstream profile.

In most cases we’ve been able to use Simple Block module to provide “custom” blocks as configuration, rather than the core “custom” block types, which are treated as content. However, in one case we inherited a custom block type that had relevant fields like an image and call-to-action links and text.

Here again, the upstream OpenSocial modules create and install the block configs, and we didn’t want to fork/override the entire module just to make a small adjustment to the images and text/links. I came up with the following code block to effectively alter the block later in the installation process:

First, the helper function (called from the hook_install() of a late-stage feature module in our sub-profile), sets up the basic data elements needed, in order to make it easy to adjust the details later (and re-call this helper in a hook_update(), for example):

function _example_update_an_homepage_block() {

  ## CUSTOM ANON HOMEPAGE HERO BLOCK ##
  ## Edit $data array elements to update in future ##

  $data = array();
  $data['filename'] = 'bkgd-banner--front.png'; # Lives in the images/ folder of example module
  $data['textblock'] = '<h2>Example.org is a community of practice site.</h2>'

<p>Sign up now to <b>learn, share, connect </b>and<b> collaborate</b> with leaders and those in related fields.</p>
';
  $data['cta1'] = array(
    'url' => '/user/register',
    'text' => 'Get Started',
  );
  $data['cta2'] = array(
    'url' => '/about',
    'text' => 'More about the Community',
  );

  ## DO NOT EDIT BELOW THIS LINE! ##
  ##################################

The rest of the function does the heavy lifting:

  # This code cobbled together from `social_core.install` and # `social_demo/src/DemoSystem.php`
  // This uuid can be used like this since it's defined
  // in the code as well (@see social_core.install).
  $block = \Drupal::entityTypeManager()->getStorage('block_content')->loadByProperties(['uuid' => '8bb9d4bb-f182-4afc-b138-8a4b802824e4']);
  $block = current($block);

  if ($block instanceof \Drupal\block_content\Entity\BlockContent) {
    # Setup the image file
    $fid = _example_setup_an_homepage_image($data['filename']);

    $block->field_text_block = [
      'value' => $data['textblock'],
      'format' => 'full_html',
    ];

    // Insert image file in the hero image field.
    $block_image = [
      'target_id' => $fid,
      'alt' => "Anonymous front page image homepage'",
    ];
    $block->field_hero_image = $block_image;

    // Set the links.
    $action_links = [
      [
        'uri' => 'internal:' . $data['cta1']['url'],
        'title' => $data['cta1']['text'],
      ],
      [
        'uri' => 'internal:' . $data['cta2']['url'],
        'title' => $data['cta2']['text'],
      ],
    ];

    $itemList = new \Drupal\Core\Field\FieldItemList($block->field_call_to_action_link->getFieldDefinition());
    $itemList->setValue($action_links);
    $block->field_call_to_action_link = $itemList;

    $block->save();
  }
}

The image helper function prepares the image field:

function _example_setup_an_homepage_image($filename) {

  // TODO: use a better image from the theme.
  // Block image.
  $path = drupal_get_path('module', 'example');
  $image_path = $path . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . $filename;
  $uri = file_unmanaged_copy($image_path, 'public://'.$filename, FILE_EXISTS_REPLACE);

  $media = \Drupal\file\Entity\File::create([
    'langcode' => 'en',
    'uid' => 1,
    'status' => 1,
    'uri' => $uri,
  ]);
  $media->save();

  $fid = $media->id();

  // Apply image cropping.
  $data = [
    'x' => 600,
    'y' => 245,
    'width' => 1200,
    'height' => 490,
  ];
  $crop_type = \Drupal::entityTypeManager()
    ->getStorage('crop_type')
    ->load('hero_an');
  if (!empty($crop_type) && $crop_type instanceof CropType) {
    $image_widget_crop_manager = \Drupal::service('image_widget_crop.manager');
    $image_widget_crop_manager->applyCrop($data, [
      'file-uri' => $uri,
      'file-id' => $fid,
    ], $crop_type);
  }

  return $fid;
}

Conclusion

As with most things I’ve encountered with Drupal 8 so far, the Update system is both familiar and new in certain respects. Hopefully these concrete examples are instructive to understand how to adapt older techniques to the new way of managing install and update tasks.


The article Drupal 8 hook_update() Tricks first appeared on the Consensus Enterprises blog.

We've disabled blog comments to prevent spam, but if you have questions or comments about this post, get in touch!