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.
use Drupal\Core\Config\FileStorage;
/**
* Install preprod configuration split.
*/
function mymodule_update_9001()
{
$splitFiles = [
'config_split.config_split.preprod',
];
$config_path = realpath('../config/sync');
$source = new FileStorage($config_path);
$config_storage = \Drupal::service('config.storage');
foreach ($splitFiles as $splitFile) {
$config_storage->write($splitFile, $source->read($splitFile));
}
}
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.
use Drupal\Core\Config\FileStorage;
/**
* Install preprod configuration split.
*/
function mymodule_update_9001() {
$config_path = realpath('../config/sync');
$config_storage = \Drupal::service('config.storage');
$source = new FileStorage($config_path);
$config_splits = glob($config_path . '/config_split.config_split.*.yml');
foreach ($config_splits as $split) {
$split_name = basename($split, '.yml');
$config_storage->write($split_name, $source->read($split_name));
}
}
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.
use Drupal\Core\Config\FileStorage;
function mymodule_update_9001() {
$ignoreSettingsFile = 'config_ignore.settings';
$config_path = realpath('../config/sync');
$source = new FileStorage($config_path);
$config_storage = \Drupal::service('config.storage');
$config_storage->write($ignoreSettingsFile, $source->read($ignoreSettingsFile));
}
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.
use Drupal\Core\Config\FileStorage;
/**
* Update needed webform.
*/
function mymodule_update_9001() {
$webform = 'webform.webform.contact';
$config_path = realpath('../config/sync');
$source = new FileStorage($config_path);
$config_storage = \Drupal::service('config.storage');
$config_storage->write($webform, $source->read($webform));
}
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.
/**
* Update front page of 'Site Two'.
*/
function mymodule_update_9001() {
$site_name = \Drupal::config('system.site')->get('name');
if ($site_name == 'Site Two') {
$config = \Drupal::service('config.factory')->getEditable('system.site');
$config->set('page.front', '/node/123')->save();
}
}
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.
/**
* Update config items with correct uuid.
*/
function mymodule_update_9001() {
$uuidChanges = [
'user.role.editor' => 'f37429d0-37e7-4131-8cf4-04e6cf1292ad',
];
$configFactory = \Drupal::service('config.factory');
foreach ($uuidChanges as $configItem => $uuid) {
if ($configFactory->loadMultiple([$configItem])) {
$config = $configFactory->getEditable($configItem);
$config->set('uuid', $uuid)->save();
}
}
}
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.
/**
* Remove field_title_with_link as a dependency.
*/
function mymodule_update_9001() {
// Remove the dependency of field_title_with_link field from the
// form_display and view_display configuration items for the grid
// item paragraph.
$removeField = 'field.field.paragraph.grid_item.field_title_with_link';
$removeDependencyFrom = [
'core.entity_form_display.paragraph.grid_item.default',
'core.entity_view_display.paragraph.grid_item.default',
];
$config = Drupal::service('config.factory');
foreach ($removeDependencyFrom as $field) {
$data = $config->getEditable($field)->get();
$dependencies = $data['dependencies'];
if (($key = array_search($removeField, $dependencies['config'])) !== FALSE) {
unset($dependencies['config'][$key]);
$data['dependencies'] = $dependencies;
$config->getEditable($field)->setData($data)->save();
}
}
}
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.
function mymodule_update_9001() {
$block = BlockContent::create([
'info' => 'My Content Module',
'type' => 'content_block',
'langcode' => 'en',
'uuid' => '784c50d7-13bb-4759-9a23-ce4592cdb058',
]);
$block->save();
}
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.
Phil