Principles of Configuration Management - Part Three

This is the third in a series of posts about Drupal 8's configuration management system. The Configuration Management Initiative (CMI) was the first Drupal 8 initiative to be announced in 2011, and we've learned a lot during thousands of hours of work since then. These posts share what we've learned and provide background on the why and how. In case you missed them: here is part one and part two.

Configuration management helps to manage complexity

Drupal offers a rich user interface that allows us to build complex websites. With a few clicks we can create views and content entities with custom fields. We can limit a view to only listing a particular content type, and then we can display the view's output in a block that only certain roles can see. By the time we've finished building our site, we've created numerous configurations that have relationships to each other and the system that built them. Drupal 8’s solution to managing such complexity is configuration dependencies.

What is a configuration dependency?

In Drupal you can create a node type, add fields to it and configure how they are shown using an entity display. For example, you can create a 'news' node type and attach an image field called 'main photo' field. By default, Drupal comes with different views modes for content entities such 'teaser', 'rss', 'search result' and 'full content'. You can add custom view modes as well. Each view mode can be configured on each node type to have a different entity display. The entity display in turn configures which fields are displayed for that view mode of that node type and how they are displayed. For example, the 'main photo' field can be configured to be displayed on the 'full content' view mode for the 'news' node type and hidden for the 'search result' view mode.

Creating fields, node types and entity displays through the UI creates configuration entities representing each thing. Fields are a bit more complex than that. Each field consists of two configuration entities. Firstly, it has a field storage, which is unique to an entity type, defines the field’s storage requirements and is shared across all the entity type's bundles. (A node type is a bundle for the node entity type.) Secondly, it has a field, which defines a field's settings for a particular bundle, such as the default value. For example, the 'news' node type could have one default image in the 'main photo' field, while another node type could reuse the same field storage with a different default image.

In order for the entity display to exist, the fields, field storages and node type must exist. In order for the fields to exist, its storage and the node type it is attached to must exist. These are descriptions of configuration dependencies.

The configuration dependencies between node types, field storages, fields and entity displays

What happens when a configuration entity is deleted?

Dependencies allow us to work out what will be affected if we delete a field. When a field is deleted, we list the affected configuration entities that will be deleted or updated on the confirmation form. For example, if you delete the 'Tags' field on the article node type, the configuration system will warn you that all of the article's entity displays are going to be updated.

Deleting a field in Drupal 8 displays configuration that will be updated

We also do this on module uninstallation. The following example is particularly interesting because it shows that blocks that depend on a view will be deleted when uninstalling the Views module.

Uninstalling the Views module in Drupal 8 tells you that it's going to delete a block that depends on a view

This ability to manage dependencies provides a much better picture of the potential consequences of an action. It also makes Drupal 8 significantly more robust because orphaned configuration can't exist.

Drupal decides which configuration entities to update and which to delete based on how and if the configuration entity's code implements the onDependencyRemoval() method. Implementations can update the configuration to remove any dependency passed to it. If the dependency is not removed, then the configuration entity will be deleted instead. In the 'Tags' field deletion example above, by default, the implementation in EntityDisplayBase retains the entity display and removes any configuration it contains for the deleted fields.

How do configuration entities get dependencies?

Configuration entities can depend on four types of things: modules, themes, other configuration entities and content entities. Every configuration entity has an implicit dependency on the module that provides its entity type (that is, the module that contains the actual code for the entity implementing ConfigEntityInterface). For example, roles depend on the User module. Additional dependencies are added based on the logic implemented by the configuration entity. For example, block configuration entities depend on the theme they are placed in.

All configuration entities come with logic (implemented in ConfigEntityBase) that integrates with two systems: third-party dependencies and plugins.

Third-party settings

Third-party settings are settings that modules can add to configuration entities that it does not provide. For example, the Content Translation module includes third-party settings to add per-bundle translation settings to 'Content language settings' configuration entities.

For example if you make the 'news' node type translatable, the corresponding 'Content language settings' configuration entity's 'dependencies' key will look like this:

dependencies:
  config:
    - node.type.news
  module:
    - content_translation

The configuration entity ties the Content Translation and Language modules together with the 'news' node type. The dependency on the Language module is implicit because the configuration entity is called 'language.content_settings.node.news'. The first part of any configuration entity's ID, in this case 'language', always indicates which module provides the code for the configuration entity definition.

Every configuration entity has the methods to set and get third-party settings. If a module adds a third-party setting to a configuration entity, then the entity is updated to depend on the module. Dependencies due to third-party settings can always be removed when a module is uninstalled, as opposed to requiring that those entities be deleted or leaving orphaned data on them. This happens automatically as part of Drupal 8's configuration system and represents a major improvement. In Drupal 7, modules that added functionality to the field system would often add values to a serialised PHP array stored in the field database table that proved very difficult to clean up.

Plugins

Plugins are Drupal 8's API for combining configurable settings with reusable code. Many core plugins' configurable settings are stored in configuration entities, including block plugins, filter plugins and view display plugins. For more about plugins see Joe Shindelar's excellent blog post.

The interaction of plugins and configuration entities creates a more elaborate chain of dependencies than third-party settings. For example, the Filter module provides both the filter format configuration entity and filter plugins. The filter format configuration entity is used to configure filter plugins together in one text format. Drupal's Filter module ships with a default filter format called 'Plain text'. This configures three filter plugins which are provided by the Filter module: filter_html_escape, filter_url, and filter_autop. See the screenshot below for how this appears on the "Plain text" filter format configuration screen (admin/config/content/formats/manage/plain_text).

Configuring the 'Plain text' filter format

Since the Filter module provides the code for filter formats, their entity IDs begin with 'filter', creating an implicit dependency. Since all three plugins are also provided by the Filter module, there is no need to specify the dependency a second time, so the 'Plain text' filter's 'dependencies' key is empty by default:

dependencies: { }

Filter plugins can also be provided by other modules. For example, the Editor module provides a 'editor_file_reference' plugin. If you tick the box labelled "Track images uploaded via a Text Editor" you add this plugin to the 'Plain text' filter format. The configuration system is able to determine that the filter format has a new dependency on the Editor module. This works because FilterFormat implements EntityWithPluginCollectionInterface. After saving, the 'Plain text' filter format's 'dependencies' key will look like:

dependencies:
  module:
    - editor

Plugins can add dynamic dependencies as well by implementing DependentPluginInterface. This is how views adds a dependency on a role configuration entity when you only allow selected roles to use a view.

Configuration depending on content

The Content Block module provides a 'block_content' content entity. The module has a plugin deriver that creates a block plugin for every 'block_content' content entity that a user creates. When you place one of these blocks, the block system creates a block configuration entity that details which template region the block instance appears in and the instance's visibility settings. Each definition provided by the deriver also contains the content dependency information. This allows the configuration entity to add the content entity to its dependencies.

For example, here are the dependencies of a content block placed in Bartik:

dependencies:
  content:
    - 'block_content:basic:925ba628-dfe7-478a-be3e-6285f89751f0'
  module:
    - block_content
  theme:
    - bartik

Content dependencies are soft dependencies. This means that if the piece of content is missing, the configuration entity can exist without it and it should not break the system. For example, blocks created by the Content Block module will simply display a message to the user if the content for the block does not exist yet on the site.

The ability to determine if configuration depends on content is an exciting new feature. This is used during import to fire an event that allows modules to create or stub the missing content entities.

Enforced dependencies

All the dependencies described above always reflect the current code base and the configuration. We re-calculate configuration entity dependencies on save so that you don’t need to be concerned with adding and removing dependencies when something changes. However, there are situations in which you need to ensure that a configuration entity has a particular dependency that can not be calculated.

An example is the 'book' node type. The code for node types generally is provided by the Node module, so all node types need the Node module in order to function. However, other modules can also create node types, and those other modules are not required for the node types they create to function. So, the Book module creates the 'book' node type which does not have any calculable dependency on the Book module; only the Node module is actually needed. In this case, however, we still want to enforce that the 'book' node type be removed from the active configuration when Book is uninstalled, so we have to add an enforced dependency. This is one of the few times where a module developer would have to edit an exported configuration entity directly.

The module author adds the enforced dependency to the module's default configuration file like so:

dependencies:
  module:
    - book
  enforced:
    module:
      - book

'book' is added both as an enforced dependency and as the initial value for the calculated dependency since dependencies are not calculated on install (this behaviour might change). Note again that the implicit dependency on the Node module is not saved here, because it is automatically included at the beginning the entity ID itself, 'node.type.book'.

You can depend on configuration entities

Configuration entity dependencies allow us to manage our configuration more effectively. It's not uncommon to forget about an old view, make what you think are unrelated changes to another part of the site and experience unintended side effects. This can lead to broken block placements, missing handlers in views, or even disclosing information that was intended to be private. With Drupal 8 we can build tools to manage complexity more easily and with more flexibility and integrity.

This post has had input from: catch, mtift, and susanmccormick. And thanks to xjm for extensive comments, questions, and help without which this post would be much harder to understand.