Drupal 9: Changing Config Through Update Hooks

20th June 2021 - 15 minutes read time

Drupal configuration is normally changed or removed through the configuration import and export process. For example, the process I follow is to make the change in the configuration locally, export the configuration into the source code, deploy the source code to a remote server and import the configuration. Using this mechanism, configuration changes that were exported locally are imported into the site and are ready to use.

There are certain situations where using update hooks to update the configuration is necessary. This means that you would change the configuration in your system directly using code in update hooks, rather than following the export and import process. These situations are rare, but necessary from time to time in order to maintain a consistent configuration on your site.

Key to all of this is to always run your update hooks before performing your configuration import. This is important rule to follow as the Drupal update process can alter or tweak the fields and tables that contain Drupal configuration entities. Then, when importing the Drupal configuration the data structure is in the correct state and is ready to accept the changes.

Let's look at a few of the situations that you might encounter and how to approach each of them using update hooks.

Updating Configuration Splits

If you are using configuration splits to manage your different environments then you will encounter an issue where you import your configuration and then realise you need to import the configuration again.

To explain, let's say that you want to include a new environment, we'll call it "preprod". To get this environment set up correctly you will need to create a new configuration split for that environment and then export it to your codebase. After deployment to the preprod environment you will import the config and the new configuration split will have been created. The problem is that the actual items included in the split will not have been imported. In order for new the configuration split settings applied to the preprod environment you need to import the configuration a second time.

Obviously, one way to solve this is to always import the configuration on the site twice. The first one will import the configuration and then second is just in case there is another configuration split available. This might be the solution for you if you have a lot of configuration splits across many multi-site setups. I have seen some deployment setups deliberately importing configuration twice because of this very reason.

In order to import configuration just the once you can force your configuration split to be installed in the update hook so that when you import your configuration everything will be setup correctly the first time. The following update hook will force the preprod configuration split to be installed.

  1. use Drupal\Core\Config\FileStorage;
  2.  
  3. /**
  4.  * Install preprod configuration split.
  5.  */
  6. function mymodule_update_9001()
  7. {
  8. $splitFiles = [
  9. 'config_split.config_split.preprod',
  10. ];
  11.  
  12. $config_path = realpath('../config/sync');
  13. $source = new FileStorage($config_path);
  14. $config_storage = \Drupal::service('config.storage');
  15.  
  16. foreach ($splitFiles as $splitFile) {
  17. $config_storage->write($splitFile, $source->read($splitFile));
  18. }
  19. }

After this, when you run your configuration import it will import the split items straight away.

An alternative approach to this is to simply find and import all configuration split configurations in your config directory. The following update hook will find any configuration split file in your configuration directory and then force import it.

  1. use Drupal\Core\Config\FileStorage;
  2.  
  3. /**
  4.  * Install preprod configuration split.
  5.  */
  6. function mymodule_update_9001() {
  7. $config_path = realpath('../config/sync');
  8.  
  9. $config_storage = \Drupal::service('config.storage');
  10. $source = new FileStorage($config_path);
  11. $config_splits = glob($config_path . '/config_split.config_split.*.yml');
  12.  
  13. foreach ($config_splits as $split) {
  14. $split_name = basename($split, '.yml');
  15. $config_storage->write($split_name, $source->read($split_name));
  16. }
  17. }

Using this method you can force all of your configuration splits to be imported in one go before running your configuration import.

Updating Configuration Ignore Settings

Similar to configuration split, there is an issue with configuration ignore settings where you ignore settings won't be imported first time around.

To reiterate, you will find that when you import your configuration then any ignored items will not be ignored until the next configuration import. The first configuration import will import the configuration ignore settings. It is only on the second configuration import that the ignore settings will be taken into account. This can actually cause problems as you will find that supposedly ignored configuration items will be imported. Only on the second import will the ignored configuration items actually be ignored. You could remove the items you want to ignore, but this would cause those items to be deleted from your site during the first import process.

One way around this is with a staged deployment. In this situation you would deploy your configuration ignore settings and then you can deploy you configuration without any fear of the ignored items being changed. This takes a little bit of planning as you'll need to deploy multiple times in quick succession, which can mean some code juggling to get it right.

The solution to this is to use update hooks in a very similar manner to the configuration split solution. We just need to force import the configuration ignore settings before we run the configuration import.

  1. use Drupal\Core\Config\FileStorage;
  2.  
  3. function mymodule_update_9001() {
  4. $ignoreSettingsFile = 'config_ignore.settings';
  5. $config_path = realpath('../config/sync');
  6. $source = new FileStorage($config_path);
  7. $config_storage = \Drupal::service('config.storage');
  8. $config_storage->write($ignoreSettingsFile, $source->read($ignoreSettingsFile));
  9. }

With this in place the configuration ignore settings are applied before you import the configuration so your ignored settings will be correctly ignored.

Importing Configuration Of Ignored Configuration

If you are using configuration ignore to skip over certain parts of your configuration then you might find a situation where you actually need to import certain configuration settings as a one off change to your site.

For example, if you are using Webform then there is a good chance that you are using configuration ignore to ignore the configuration of your Webform entities. This is useful as it allows your clients control over changing webforms and means that those changes aren't reverted when you import your config.

The issue is that sometimes Webforms can form the basis of new features and components on sites, which means that you need to export and import them as configuration. In order to do this you need to run an update hook to import just the configuration items you want.

  1. use Drupal\Core\Config\FileStorage;
  2.  
  3. /**
  4.  * Update needed webform.
  5.  */
  6. function mymodule_update_9001() {
  7. $webform = 'webform.webform.contact';
  8. $config_path = realpath('../config/sync');
  9. $source = new FileStorage($config_path);
  10. $config_storage = \Drupal::service('config.storage');
  11. $config_storage->write($webform, $source->read($webform));
  12. }

When you deploy this change in this scenario the your Webform will be imported once and once only. It will not be imported with your configuration so you don't need to worry about it on your next deployment.

For another example of this, one item of configuration that changes from within a multisite environment (and is often ignored) is the site settings area. The front page of a site is usually unique to individual sites and is therefore included in configuration ignore settings. When rolling out an update to the site one thing that might be changed is the front page setting so you need to force import that item.

This can be changed easily using the config.factory Drupal service in your update hooks to alter part of the configuration.

  1. /**
  2.   * Update front page of 'Site Two'.
  3.   */
  4. function mymodule_update_9001() {
  5. $site_name = \Drupal::config('system.site')->get('name');
  6. if ($site_name == 'Site Two') {
  7. $config = \Drupal::service('config.factory')->getEditable('system.site');
  8. $config->set('page.front', '/node/123')->save();
  9. }
  10. }

Once this update hook is run the homepage of "Site Two" site in the multisite setup will be different.

Changing UUIDs In Active Configuration

This is perhaps an edge case situation, but I have encountered it before on an inherited Drupal 8 project.

What happened was that a Drupal multisite environment was setup, but the configuration was created in such a way that the configuration spit for each site contained an entire copy of the configuration. This caused a knock on effect where any changes that were deployed to one site would have to be re-configured on the other sites and exported separately. The different sites quickly diverged in their configurations and became a nightmare to update. Once this happens it is difficult to merge the configurations back together unless you know configuration very well.

The solution to this was to pick one of the configurations and convert all other sites to use that version of the configuration where possible. To do this I created the configuration split merge script. This tool inspects different configuration directories and will merge together any configuration file that is essentially the same. The tool will consider two configuration items identical if only the UUID of the configuration item is different. In which case what is needed is to pick a master UUID and inform all of the sites that the UUID of these configuration items has changed.

The output of the configuration split merge command is an update hook that can be run to change configuration UUIDs of certain items all sites so that when the configuration is imported the won't be any clashes.

Here is an example of the configuration split merge tool that merges a user role.

  1. /**
  2.   * Update config items with correct uuid.
  3.   */
  4. function mymodule_update_9001() {
  5. $uuidChanges = [
  6. 'user.role.editor' => 'f37429d0-37e7-4131-8cf4-04e6cf1292ad',
  7. ];
  8.  
  9. $configFactory = \Drupal::service('config.factory');
  10.  
  11. foreach ($uuidChanges as $configItem => $uuid) {
  12. if ($configFactory->loadMultiple([$configItem])) {
  13. $config = $configFactory->getEditable($configItem);
  14. $config->set('uuid', $uuid)->save();
  15. }
  16. }
  17. }

Running the above update hook allowed the configuration import to complete correctly against a single configuration and vastly simplified the maintenance of the environments.

Fixing Corrupted Configuration

Perhaps more difficult to demonstrate is when the configuration is corrupted in some way and needs to be repaired.

I have seen a few situations where Drupal projects (normally inhered from someone else) appear to be fine but will crash when performing a certain action or visiting a certain page. In my experience this sort of problem seems to be most common in the field area, but can come from older configuration not being cleared out correctly.

Tracking down where the problem comes from can be a little difficult (I recommend xdebug to track down the issue) but once the configuration has been identified as being the problem you can use update hooks to solve it.

One example I found was with the configuration on field attached to a paragraph. The paragraph in question once used to contain a field called "title with link". At one point the field was removed from the paragraph but the configuration for the paragraph display still contained a dependency to the field. This meant that when the page containing the paragraph was loaded it attempted to load the missing field and caused an error.

How the configuration was setup in this way was a bit of a mystery. The working hypothesis at the time was that this was due to the Features module being used improperly.

The solution was to load in the configuration for the paragraph display and remove the missing field from the dependencies for those items. This was done in an update hook.

  1. /**
  2.  * Remove field_title_with_link as a dependency.
  3.  */
  4. function mymodule_update_9001() {
  5. // Remove the dependency of field_title_with_link field from the
  6. // form_display and view_display configuration items for the grid
  7. // item paragraph.
  8. $removeField = 'field.field.paragraph.grid_item.field_title_with_link';
  9. $removeDependencyFrom = [
  10. 'core.entity_form_display.paragraph.grid_item.default',
  11. 'core.entity_view_display.paragraph.grid_item.default',
  12. ];
  13.  
  14. $config = Drupal::service('config.factory');
  15.  
  16. foreach ($removeDependencyFrom as $field) {
  17. $data = $config->getEditable($field)->get();
  18. $dependencies = $data['dependencies'];
  19. if (($key = array_search($removeField, $dependencies['config'])) !== FALSE) {
  20. unset($dependencies['config'][$key]);
  21. $data['dependencies'] = $dependencies;
  22. $config->getEditable($field)->setData($data)->save();
  23. }
  24. }
  25. }

Once this update hook was run the configuration was then able to be loaded without error.

As I said, actually tracking down this problem can be difficult, but I highly recommend using update hooks to deploy the fix to this problem.

Creating Content Blocks

A bit of a common problem in Drupal 8 happens when using content blocks. The issue is that you will set up your content blocks locally and place them using block configuration. Then, when you deploy those configuration changes the content blocks will be missing on the site and you will receive this error on your page stating the following message:

This block is broken or missing. You may be missing content or you might need to enable the original module.

The content blocks are missing because they are items of content and not configuration. This is a long standing issue with Drupal, and there are a few solutions to the problem, including the Recreate Block Content module. I could write an entire article about just this problem as there are a few moving parts involved.

One way of fixing this is to create your content blocks entities in update hooks. This means that when you import the site configuration your content blocks will be present in the correct areas. You still need to ensure that you add the content in your content blocks, but at the very least your content blocks and configuration will be consistent.

The following update hook will create a content block with a specific UUID that has been taken from the block placement configuration file.

  1. function mymodule_update_9001() {
  2. $block = BlockContent::create([
  3. 'info' => 'My Content Module',
  4. 'type' => 'content_block',
  5. 'langcode' => 'en',
  6. 'uuid' => '784c50d7-13bb-4759-9a23-ce4592cdb058',
  7. ]);
  8. $block->save();
  9. }

Once this has been run you can import the configuration and your content blocks will be placed into the correct regions.

Have I missed any examples here? Comment below with any situations where you have used update hooks to update your configuration outside of your import process.

Comments

Permalink

Nice article, thanks!

> Have I missed any examples here?

The thing I was hoping to see was a way of actually performing the import of a specified config item in an update hook.  One example of why this is needed is adding a new field and wanting to perform processing with that field during an update hook in the same deployment in which the new field config is added.

Most of your examples are ultimately doing this:

    $config_storage->write($name, $source->read($name));

In the "create a field" scenario, this does not result in the field being created; that part clearly happens at some later point.

I found examples covering this *specific* case in a couple of places:

* https://www.metaltoad.com/blog/programmatically-importing-drupal-8-fiel…
* https://blokspeed.net/2019/creating-fields-and-other-configuration-duri…

But what I'm really looking for is the generic way to say "import this named config item" -- no matter *what* it is -- using whatever process would take place for that item during a regular config import.  Field creation is undoubtedly only one case of many possibilities where a simple read/write is insufficient, and I've yet to find anything showing how to do this for the general case (which I find baffling).

Phil (Wed, 06/23/2021 - 07:01)

Permalink

Thanks for the comment Phil :)

An interesting thought you have there. What I was trying to avoid in the article was talking about ways to simply alter the config. The above update hooks are intended to change the config in certain ways so that it doesn't break the config import process and can't be done in other ways. I think no one is talking about how to do what you are asking because it is just bad practice. You should really be relying on the configuration import process to create fields as doing it otherwise changes the configuration and means you need to do a synchronisation process to move the config into code again.

You can do what you suggest by exporting the config for the field and adding it to custom module configuration (see https://www.drupal.org/docs/creating-custom-modules/include-default-con…). This configuration will be imported when the module is enabled.

Aside from that, I suppose it could be possible to reverse engineer the configuration import code in Drupal to do what you want. I think the Drupal 8 version of Features does something that might help. Although I really wouldn't advise using that module.

Permalink

You should probably look into Config Enforce, as it makes this whole process much easier (i.e. without having to write update hooks).

Colan Schwartz (Tue, 06/29/2021 - 22:29)

Permalink

> I think no one is talking about how to do what you are asking because it is just bad practice.

I agree that running database updates ahead of config import is the correct sequence in general, but the kind of dependency issue I'm talking about can arise no matter which way around you do it. It has nothing to do with good or bad practice -- you have to do one of those things before the other (assuming a standardised deployment process), and you can therefore run into situations where something in step 1 depends on something that isn't going to happen until step 2, at which point you need to either modify the normal sequence (ideally doing the advance processing for only the things which need it), or else resort to a series of multiple deployments (which is dramatically more cumbersome to prepare and test).

> You should really be relying on the configuration import process to create fields

Yes; that's exactly what I want to do -- ask the config system to import a specific config to create the field, rather than doing it manually.

> You can do what you suggest by exporting the config for the field and adding it to custom module configuration ... This configuration will be imported when the module is enabled.

I do appreciate the idea, and I expect that it would work, but adding and enabling a new special-purpose module any time this situation arose would be a pretty crazy way to work.

Phil (Thu, 07/01/2021 - 07:08)

Permalink

I've had a close look at the Config Enforce module. It does not cover any of the use cases I have outlined above. That module is more about enforcing static config for certain parts of the system and is more related to config read only than it is to update hooks.

Add new comment

The content of this field is kept private and will not be shown publicly.