Hard Things Are Possible: Configuration Management in Drupal 8

We just finished covering how simple configuration is still easy in Drupal 8, but how is Drupal 8 making the hard things possible in a way to justify changing the old variables API? Well, in Drupal 7, when you needed to handle complex configuration, the first step was ditching variable_get(), system_settings_form(), and related APIs. Drupal 8 has improved this situation two ways. First, you don’t have to throw out basic configuration code to handle complex needs. Second, more things are possible.

[Related] Managing Configuration in Code in Drupal 8

The goal of the Configuration Management Initiative was to maintain the declarative nature of configuration even when side-effects or cross-referenced validation are necessary. (Contrast with the Drupal 7 trick of using hook_update_N() as an imperative method.) Specifically, Drupal 8’s configuration management system operates under the perturbation model of constraint programming. That is, modules and core ship with defaults that work with each other, and configuration changes by a site owner create perturbations that either can be satisfied trivially (like the site front page path) or through propagating changes (described below). Sometimes, the constraints can’t be all satisfied, like deleting a field but also using it in a view; Drupal 8 helps here by making dry-run configuration tests possible. At least you can know before deploying to production!

Let’s go on a tour of hard problems in Drupal configuration management by walking through the use cases.

Subscriptions and Side-Effects

Often, configuration takes effect by simply changing the value. One example is the front page path for a site; nothing else needs to respond to make that configuration effective. In Drupal 7, these basic cases typically (and happily) used variables. Things got messy when a module needed to alter the database schema or similar systems to activate the configuration. Drupal 7 didn’t have a answer for this in core, though you could build on top of the Features module.

Anyway, in Drupal 8, side effects happen two ways. You should use the first when possible.

Subscribing

This is the modern and clean method of responding to configuration changes, regardless of whether the changes are to your module’s configuration or not. There are a number of configuration events you can receive. The most basic is ConfigEvents::SAVE, which fires on any change, whether created using the admin GUI, Drush, or a configuration import.

A good example of this approach in core is the system module’s cache tag system. It invalidates rendered cache items when the site theme changes; it does more than that, but we’ll be pulling the examples from there. The foundation for Drupal 8-style event listening is Symfony’s EventSubscriberInterface, which provides an object-oriented way to list the events of interest and set a callback. Drupal 7 developers should think of it like a non-alter hook.

The first step is getting the right files in the right places for the autoloader. You will need ConfigExample.php (assuming you name the class ConfigExample) and a example.services.yml (assuming your module name is “example”). You should end up with something like this, starting from the module root:

  • example/

The second step is to register interest in the appropriate events from your class, which happens by implementing getSubscribedEvents() (which is the only member function required by the EventSubscriberInterface). The following code causes the member function onSave() to run whenever configuration gets saved:

<?php
 public static function getSubscribedEvents() {
    $events[ConfigEvents::SAVE][] = ['onSave'];
    return $events;
}
?>

Third, we need to implement the onSave() callback to invalidate the cache when the appropriate configuration keys change. If system.theme or system.theme.global changes, the code will call the appropriate function to invalidate the cache:

<?php
public function onSave(ConfigCrudEvent $event) {
    if (in_array($event->getConfig()->getName(), ['system.theme', 'system.theme.global'], TRUE)) {
         // Invalidate the cache here.
    }
 }
?>

The example above covers the intermediate “respond to a configuration change” use case. If you’d also like to validate configuration on import, you can see an example in SystemConfigSubscriber. It shows both subscribing to import events and stopping propagation on invalid input.

Using Hooks

This is where things get dirty; we’re now in hook_something_alter() territory. It’s hard to reason about things in this territory, but here we are anyway because it’s necessary for a handful of use cases. To be clear, you’re basically killing kittens when you re-jigger data the way an alter hook can. If you’re doing cleanup, I’d recommend queueing something into the batch system instead using the subscription method, even if your batch job has to attempt things and re-enqueue itself if other batch processing needs to finish first. Anyway, warning over. Here’s your example, shamelessly pulled from the API site. This gets the list of deleted configurations matching the key “field.storage.node.body.” If any exist, it adds a new function call to the end of the steps for propagating the configuration.

<?php
function example_config_import_steps_alter(&$sync_steps, \Drupal\Core\Config\ConfigImporter $config_importer) {
 $deletes = $config_importer->getUnprocessedConfiguration('delete');
 if (isset($deletes['field.storage.node.body'])) {
    $sync_steps[] = '_additional_configuration_step';
 }
}
?>

Settings Forms

In Drupal 7, have you ever saved a settings form and then gotten a page back with the old setting still in place (usually because something messed with $conf in settings.php)? Never again in Drupal 8! Configuration forms save to the pre-alteration values, and modules can read those “raw” values themselves through Config::getRawData() or ConfigFormBaseTrait::config() to provide custom forms.

However, if you just need a basic form, you can use pre-alteration values automatically with Drupal 8’s ConfigFormBase, which replaces system_settings_form() and seamlessly integrates with everything in Drupal 8’s configuration world from events to imports to version control.

A great example of ConfigFormBase use is in the system module’s LoggingForm.php.

Discovery and Multiple Objects for a Module

Need more than a (possibly nested), single configuration file for your module? If you provide something like views where there are multiple, independent configurations still owned by your module, you need Configuration Entities. These entities allow enforcing a schema, listing the individual configurations (which correspond one-to-one with YAML files). These entities are cleaner than the Drupal 7 approaches of spamming the variable namespace, creating a configuration table, or using some combination of ctools and Features.

Three-Way Merging

I don’t want to go into deep detail because there’s a great blog post on configuration merging already, but I will underscore the importance of three-way merging for configuration. Unless you have a completely linear flow with one development environment and no changes happening in test or live, there will be cases where configuration diverges both on the development branch and in production versus the last common revision. A three-way merge allows safely determining what changed on each side without the hazards of simply comparing development’s configuration to production’s (which can make additions on one side appear to be deletions on the other). You could kind of do this with Features, but the use of PHP arrays and other serialization made the committed configuration unfriendly to diff utilities. Drupal 8 uses canonicalized YAML output, which is both human- and diff-friendly.

Setting Dynamic Options with PHP

In the Style of settings.php

The biggest difference you’ll notice isn’t in settings.php but in the GUI. Values you coerce here will not appear in the GUI in Drupal 8 (for reasons mentioned in the Settings Forms section above). The following example is shamelessly pulled from the Amazee Labs post.

Drupal 7:

<?php
$conf['mail_system']['default-system'] = 'DevelMailLog';
?>

Drupal 8:

<?php
$config['system.mail']['interface']['default'] = 'devel_mail_log';
?>

In a Module

This is back into dirty territory because modules ought to provide higher-level ways of altering their behavior other than expecting other modules to hurriedly erase and change configuration values before the module reads them. If you must go down this path, you need to register the class performing the changes as a service under “config.factory.override” and implement ConfigFactoryOverrideInterface. This happens much as the service entry and class for subscribing happen above.

Configuration Lockdown

The most you could do in Drupal 7 was hard-code many variables in settings.php and periodically audit the site’s configuration with Features. With the transparently named Configuration Read-Only Mode module, you can actually prevent ad hoc changes in, say, your test or live environment. On Pantheon, for example, you could do the following to prevent configuration changes in the production environment:

<?php
if (defined('PANTHEON_ENVIRONMENT') && PANTHEON_ENVIRONMENT == 'live') {
    $settings['config_readonly'] = TRUE;
}

Bundling Related Configuration

This is still Features territory, actually more than ever. Contrary to rumors, Features is alive and well for Drupal 8. Relieved of the burden of extracting and applying configuration, Features is back to its original role of bundling functionality, most often in the form of configuration YAML to be imported by Drupal 8. So, in short, Features is now just for features.

Other Resources


The latest stack + the latest Drupal: See how Drupal 8 reaches its full potential on Pantheon

Topics Development, Drupal, Drupal Hosting, Drupal Planet

Let’s get in touch

855-927-9387