Drupal 9: Some Strategies For Developing Update Hooks

Drupal's update hook system is a powerful way of updating your site to introduce things that wouldn't be handled using the configuration system.

Modules will use update hooks to bring sites that have the old version of the module in line with the latest additions to the module. For example, if a new field is added to a table that the module uses then an update hook will be needed to add that field to all sites that are current using the old version. This update hook will be in addition to the install hook that would install the table with the added field in the first place.

There are a number of different reasons why you would want to use update hooks on your own site. Normally being stored in either install profiles or custom modules they would be run on deployment in order to update your dev/stage/production site with changes without having to manually apply them. This is a useful way to do one of the following actions.

  • Sometimes corrupted configuration can cause problems on your sites. In order to update your other sites with the fixes to this corrupted configuration you'll need to add an update hook containing those fixes. This can then be deployed to your sites to fix the problem.
  • You can use an update hook to force import configuration. For example, if you are using configuration split and need to add another split you'll probably want to force import the split configuration before the configuration is imported. This way, you can use the configuration split as if it was always present. The alternative is to run configuration import twice.
  • If you have things in your state API that need to be updated on deployment then it's a good idea to use the update hooks to update them. Some modules keep authentication details in the state API so they can be updated using update hooks.
  • As the configuration import happens after the update hooks have been triggered, using update hooks is a good way of adding content to the site that the configuration is added. This might be injecting content blocks or taxonomy terms into the site so that the configuration can be imported correctly. It is considered slightly bad practice to add content in this way as it will trigger a lot of other hooks to fire. If you need to inject a lot of content into your site then you should be using hook_post_update_name() instead.
  • In older versions of Drupal update hooks were used quite a lot to update things like panels configuration or features. Whilst that doesn't necessarily apply for Drupal 9, it was popular in previous versions when reverting certain configuration items needed to happen.

Developing these update hooks can be a little difficult, especially as they are one shot deals. The fact that they are run once means that if you working on a new update hook and it fails without error, you need to reset our Drupal system to be able to run it again.

I thought I would put together some techniques that can be used to work with update hooks.

Running Update Hooks (The Drupal Way)

First, let's look at running update hooks the way that you should be running them.

Using Drush or visiting update.php are the two main ways of running update hooks. Both of these methods will run all of the update hooks that currently need to be processed in the site.

The page at update.php will show you the available updates that need to be processed and will process them using a batch run. This page is protected by access rights so you need to be logged in as a user with the "administer software updates" permission in order to access the page. You can bypass this by setting the update_free_access setting to be TRUE.

$settings['update_free_access'] = FALSE;

Remember to put this back to FALSE when you are finished with it.

Normally though, I tend to run update hooks via Drush. I run updates to modules automatically through my deployment process and so Drush is almost always my tool of choice for running updates. To run updates through Drush use the following command.

drush updatedb

You can optionally pass a "--yes" flag to force the update hooks to run (i.e. without prompting you to say yes first). This command takes a couple of extra options to control cache clearing and other things, but this is generally all that is needed.

Creating An Update Hook

Whilst I won't go into extreme detail on creating an update hook, there are a couple of things that need to be taken into account when creating them.

All update hooks must be present in the .install file of the module or profile. The name of the hook is hook_update_N(), where N consists of three parts. The first one or two numbers are the major version number of Drupal, so this would be 8, 9 or 10. The second number is the major version number of your module and the rest is used as the update number to run. For example, an update hook in version 2 of a Drupal 9 module might be mymodule_update_9201().

If you are writing update hooks in your custom install profile or module in order to facilitate deployment processes then you'll probably write your update hooks as mymodule_update_9000(), mymodule_update_9001(), mymodule_update_9002() etc.

Update hooks should contain the following footprint (in this example the module is called mymodule). The content of the update hook will perform some action.

<?php

/**
 * Some information about the update.
 */
function mymodule_update_9001() {
  // run some update
}

The comment above the function call is actually very important. It is used by Drupal to show you what the update hook is doing as it is being processed. The above example will produce the following output when triggered.

$ drush updatedb
 --------------------- ----------- --------------- ---------------------------
  Module                Update ID   Type            Description
 --------------------- ----------- --------------- ---------------------------
  mymodule              9001        hook_update_n   Some information about the
                                                    update.
 --------------------- ----------- --------------- ---------------------------


 Do you wish to run the specified pending updates? (yes/no) [yes]:

You can optionally include a $sandbox parameter that will allow you to use the update hook as a batch operation. If you do add the $sandbox parameter then you should add it on the assumption that it will be NULL.

<?php

/**
 * Some information about the update.
 */
function mymodule_update_9001(&$sandbox = NULL) {
  // run some update
}

If you are interested in getting this to work then I can recommend looking at the Redirect module install file as this contains a nice example of a batch update hook.

This is a very quick overview of the update hook, if you want to know more the please look at the Drupal documentation page on hook_update_N(), which has lots of detail.

Resetting Drupal To Run An Update Hook A Second Time

Update hooks are idempotent. That is, assuming that the update hook completed successfully, they are run once and once only. If you want to run an update hook a second time then you will need to either restore the database or trick Drupal into the current state of your module update process. Drupal stores the current state of the update hook in the key/value storage (in the key_value table), and this can changed to allow the hook to be run a second time.

To find out the current value of the update state of your module use the following code. This finds out the current schema version of your module and should print out a number. After running the above update hook this will be 9001.

echo \Drupal::keyValue('system.schema')->get('mymodule');

You can reset this value using the following code. This will set the value to 9000, and as the next update hook in the install file is 9001 this will trigger that update hook to run again when the update hooks are next processed.

\Drupal::keyValue('system.schema')->set('mymodule', (int) 9000);

This can be wrapped in a Drush call so that it can be run in the command line to set the system to a particular state.

drush php:eval "\Drupal::keyValue('system.schema')->set('mymodule', (int) 9000)";

There is another way in Drupal to set and get the schema value for the module. This is done using drupal_get_installed_schema_version() to get the value and drupal_set_installed_schema_version() to set the value. In Drupal 9 this is a wrapper around the key/value service above, but the functions have been available in Drupal for a few major versions.

To get the schema setting use the following.

drush php:eval "echo drupal_get_installed_schema_version('mymodule')"

Setting the schema value just accepts an additional parameter.

drush php:eval "drupal_set_installed_schema_version('mymodule', 9000)"

Some Strategies In Developing Update Hooks

If you are working on an update hook it is useful to run the hook whilst you are figuring things out. This is especially the case if the hook has some complex configuration operations as you'll need to find that configuration item in the database before you can alter it. If you run the update hook once using drush updatedb then it will not be triggered again. You can trick Drupal into running it again, but there are a couple of other strategies that can be used to build you update hooks.

Here are some strategies that I use when developing update hooks.

Altering index.php Strategy

Adding a call to the update hook function at the bottom of the main Drupal index.php file will run the update hook. This ensures that the full system has been bootstrapped and ready to run your custom update functions. In order to run the update hook you need to include the file that contains that function. This is done using the module_load_include() function that takes the file type and the module name as the two parameters needed here.

Add the following to the bottom of your index.php file and every time you refresh the page the update hook function will be run.

module_load_include('install', 'mymodule');
mymodule_update_9001();

This has the added benefit of being debuggable using xdebug, which can be really handy in getting things working.

Obviously, this is a temporary measure and should not be committed to your repo!

Event Subscriber Strategy

Another way of doing the same kind of thing is by utilising the page load event subscriber method that I have talked about on another article. This is pretty easy to set up, but it not as simple as just adding the needed files of code to the index.php file.

To implement this strategy you need to register a class with the even subscriber in Drupal. This would be added to your custom module .services.yml file

services:
  mymodule.event_subscriber:
    class: Drupal\mymodule\EventSubscriber\MyModuleEventSubscriber
    tags:
      - { name: event_subscriber }

The class should subscribe to the kernel.controller event, which means that it will be triggered on every page load. All you need to do then is add your update hook callback to the event callback. Here is the event class in full.

<?php
 
namespace Drupal\mymodule\EventSubscriber;
 
use \Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
 
class MyModuleEventSubscriber implements EventSubscriberInterface {
 
  public static function getSubscribedEvents() { 
    $events[KernelEvents::CONTROLLER][] = array('onLoad');
    return $events;
  }
 
  public function onLoad(FilterControllerEvent $event) {
    module_load_include('install', 'mymodule');
    mymodule_update_9001();
  }
 
}

Now, every time you refresh the page the event will trigger and run your custom update hook. This is similar to running the update hook in a hook_init() call in Drupal 7.

Again, this is a temporary measure whilst you are developing the update hook.

Drush Eval Strategy

It is possible to run update hooks through Drush as well. Using the php:eval command in Drush we can include the needed install file and run the update function in the same way as the other strategies I have talked about above.

To run the same update function as before we can do the following.

drush php:eval "module_load_include('install', 'mymodule');mymodule_update_9001();"

As we are running this through Drush it means that Drupal is already bootstrapped and so we have access to all of the functions and services we need. This strategy is less easy to debug through xdebug, but has the benefit of not needing to change the codebase in order to get it working. You can also use the above method to run an update hook on your other environments if you need to repeat one of the already run update hooks and do not want to do another deployment to do this.

Conclusion

There are a few strategies that can be employed when building your update hooks. Tricking Drupal using these strategies or rolling back the schema version is sometimes better than restoring the database, especially if the database is large. I would much rather spend a few moments including the update function call into the index.php file than lose hours re-importing my database over and over again.

Whilst I have specifically addressed Drupal 9 in this post, all of these techniques will work in Drupal 8 and all but the event subscriber will work in Drupal 7.

Using the above strategies should help you ensure that your update hook will run correctly the first time. You should, however, ensure that the update hook runs along with the rest of the update hooks on the system so a final call to drush updatedb will help ensure that your deployment will work correctly.

Do you have any strategies you use when developing update hooks? If so please post them below and let us know.

More in this series

Comments

Nice technique to alter the index.php file though the lines of code need to be above

$kernel->terminate($request, $response);

 

Permalink

...and it's best to use 

module_load_install('my_module');

 

Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
4 + 0 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.