Drupal 8 API: configuration

Pre-requisites

Configuration is exportable, user-editable state stored as entities

As we previously compared state with configuration, let's compare configuration with state:

Semi-permanent
Configuration is Drupal's fundamental instructions from the site administrators and developers for how to run itself. Unlike state, which Drupal itself is broadly at liberty to change, configuration is only likely to ever change at a human's behest. Examples include the site title, or whether the site is in maintenance mode: only a human can determine these settings; it's a rare use case where Drupal would change its own title, or put itself into maintenance mode.
Complex life cycle
If configuration is going to hang around for longer, it needs better curation of its life cycle. Configuration can also depend on certain modules, or even be made nonsensical or dangerous (and thus a candidate for uninstallation) when some module is uninstalled. Configuration has its own internal complexity above and beyond just the complexity of whatever data values are being stored.

Configuration uses config objects and config entities to support its persistence and complex behaviours, in contrast to state's simple, cached key/value store. The use of entities also permits configuration to be exported and re-imported, implying that an assembled entity's-worth of configuration (unlike state) can be of use to other sites, not just the one it was configured on.

Config objects versus config entities?

We've introduced a subtle divergence in terminology, which is only important, depending on what you're doing with configuration.

A key aspect of content entities is their countability: if your website has N news items, you can easily envisage it having N+1; moreover, if the most recently created node had ID X, and no other nodes had been created since, then the next node will have ID X+1. If we imagine referring to nodes via their long, unique UUID string instead, we could instead say that they weren't so much countable as sequenceable: put all the nodes in some order, and you can always add a new node at the end of the list.

With sequenceability in mind, here's how the two types of configuration differ:

Config entities
These are sequenceable, like content entities. They don't necessarily have a numeric ID, but they do have a unique name and can have a UUID for import and export. Config entities of a particular type must have names that conform to a particular pattern, defined in a .schema.yml file provided by the module responsible. Examples of config entities: a blogpost view; an input format; a field configuration.
Config objects
These are not sequenceable, and (while they can also have a UUID for import and export) are identified solely by their unique name: in this respect they are a kind of singleton. As such, there's no pattern for names that can be extended, although names should begin with the name of the module responsible for the configuration, so they can be uninstalled with the module. Config object structure can also defined in the schema YAML file of the module responsible. Example of config objects: views settings; contact form settings.

Regardless of its type, configuration items all use the same class(es)! Drupal\Core\Config\Config, or more usually an ImmutableConfig to avoid altering configuration by mistake. So in much of your day-to-day use of the configuration API, you'll hardly notice the difference.

If your own configuration's type matters to your own code or to possible future extension by others, it must be defined in a .schema.yml. This file is also used to describe the internal contents of the configuration: for example, the data types of different keys and sub-keys. These provide type hints for the typed data API, which we will discuss later. If you like, you can have a look at the entry for core.date_format.* in the core.data_types.schema.yml file, and compare it to the example YAML file below.

If you're only dealing with other modules' configuration, then the key difference you must remember is that you'll usually add config entities, not config objects. You can imagine adding config entities that other modules would be responsible for: e.g. installing a new view or content type (indeed, we did this previously when discussing entities and fields.) However, there is little point in your own module adding config objects for other modules: because they have no naming pattern, and no sequenceability, they have no discoverability; and the new configuration would therefore not be detectable by the other module.

Steps in the configuration lifecycle

1. Create or install configuration

New configuration should always have an "owner" module, to make clear just what it's configuring. This ownership is reflected in naming conventions:

  1. Configuration created solely for your custom module's own use should have a name beginning with your module's own e.g. d8api.settings. You may (and probably should) provide further information about the configuration's structure and type (object vs entity) in e.g. d8api.schema.yml, but we don't cover that here.
  2. Configuration created to integrate with another pre-existing module should have a name matching a pattern registered in that module's schema file e.g. views.view.customview references the views.view.* pattern found in views.schema.yml. This also implies the configuration must be a config entity, not a config object.
  3. Any other naming convention should be avoided.

At the time of writing, it is possible to create configuration with arbitrary names, as long as no dependencies are declared within the configuration's data structure (see below.) Don't do this; at the very least, it will not be uninstalled along with your own (or anyone else's) module, which is bad practice. At worst, it will be an ongoing maintenance headache, as future developers (including your future self) try to work out just how the configuration is meant to work.

Install configuration with YAML files

Any YAML files found in your module's config/install/ folder will be activated in Drupal at the point of module installation, and this is the preferred method of creating new configuration. The file's name (minus the suffix) will become the configuration name: so d8api.settings should live in config/install/d8api.settings.yml.

If your module is already installed, but you want to refresh the configuration, or install some new configuration file you forgot to originally include, you can use the config_devel module to provide a new Drush command for this purpose:

drush cdi1 sites/all/modules/d8api/config/install/d8api.settings.yml

Create configuration using the factory service

You can also create arbitrary new configuration using a Drupal\Core\Config\ConfigFactory object: just use ->getEditable($newConfigName) to retrieve a new mutable object, then ->save() this object with the name provided.

In classes, the factory should always be provided using dependency injection of the config.factory service, as discussed previously and demonstrated in the example class below; at the command line, you could use \Drupal::service('config.factory') directly.

Remember to still respect naming conventions for any new configuration names.

2. Read and update configuration

Configuration can be read and updated through the same config.factory service discussed previously. Note that the simple ConfigFactory::get() method returns an immutable, or read-only, configuration item, using Drupal\Core\Config\ImmutableConfig: as you can see in the example immediately above, getEditable() returns the editable Drupal\Core\Config\Config instead.

Values within the configuration item can be set using a dotted syntax, so that Config::set('key1.key2', $value) creates an array containing key2 => $value, then assigns it to key1.

3. Import and export configuration

A key aspect of configuration is Configuration Management. The configuration of any website is always exportable, and this export can be restored at a later date to reconfigure the site.

There are many ways of exporting configuration; Drush provides a config-export command, to export into a directory defined in your site's settings.php, and the aforementioned config_devel has a command to re-export just the configuration for a particular module.

4. Delete or uninstall configuration

In code, you can explicitly delete configuration items retrieved with the config.factory service using $config->delete(). You can also just unset specific sub-arrays within the configuration, leaving the rest intact, using $config->clear($key)->save(). For sub-subarrays, $key can follow the same dotted syntax mentioned above e.g. 'key1.key2'.

However, you probably never need to delete configuration items explicitly, if you've followed the naming conventions discussed above. When a module is uninstalled, all configuration it "owns" will be uninstalled too. In addition, if you need to uninstall another module's config entity, when some other module is uninstalled—for example, a view depending on a field formatter in some other module—then you can include in the YAML file the dependencies: enforced: - d8api syntax, demonstrated in the example class below.

Examples of working with the configuration life cycle

A. Working with configuration using PHP code

The following PHP class provides examples of how configuration works. You should save it to src/ConfigExample.php in your d8api module:

<?php
 
namespace Drupal\d8api;
 
use Drupal\Core\Config\ConfigFactoryInterface;
 
/**
 * Run some configuration API tests.
 */
class ConfigExample {
 
  /**
   * @var ConfigFactoryInterface
   */
  protected $configFactory;
 
  /**
   * Implements __construct().
   *
   * @param ConfigInterface $configFactory
   *   Config factory service, injected into the new object.
   */
  public function __construct(ConfigFactoryInterface $configFactory) {
    $this->configFactory = $configFactory;
  }
 
  /**
   * Read a configuration value; override it; change it back.
   *
   * Config names are stored in the name column of table config.
   */
  public function temporarilyOverride() {
    $site_info = $this->configFactory->get('system.site');
 
    print "Raw data for system.site:\n";
    var_dump($site_info->getRawData());
    $old_name = $site_info->get('name');
 
    $editableConfig = $this->configFactory->getEditable('system.site');
    $editableConfig->set('name', 'Example for config');
    $editableConfig->save();
 
    $site_info = $this->configFactory->get('system.site');
    print "Site name has been changed to: "
      . $site_info->get('name') . "\n";
 
    $editableConfig->set('name', $old_name);
    $editableConfig->save();
    print "Site name has been changed back to $old_name.\n";
  }
 
  /**
   * Create a new configuration value; save; clear part of it; delete.
   */
  public function createSaveClearDelete() {
    $new_config = $this->configFactory->getEditable('d8api.config');
    print "New config is empty? " . (empty($new_config->getRawData()) ? 'Y' : 'N') . "\n";
 
    // Save new configuration.
    $new_config->set('foo', 1)->set('baz.quux', ['fred' => 'barney']);
    $new_config->save();
 
    // Reload, then clear a value.
    $reloaded_config = $this->configFactory->getEditable('d8api.config');
    $reloaded_config->clear('foo');
    // Look at the entire raw data first.
    print "Config with 'foo' key cleared out: "
      . var_export($reloaded_config->getRawData(), TRUE) . "\n";
    // You can even interrogate partial arrays of content e.g. just "baz".
    print "Config 'baz' data: "
      . var_export($reloaded_config->get('baz'), TRUE) . "\n";
 
    // Delete entirely
    $reloaded_config->delete();
    $new_config = $this->configFactory->getEditable('d8api.config');
    print "Config has been deleted? " . (empty($new_config->getRawData()) ? 'Y' : 'N') . "\n";
  }
 
}

This class consists of three methods:

__construct()
Provides compatibility with dependency injection, to obtain the config factory service.
temporarilyOverride()
Trivially demonstrates $factory->get() and $factory->getEditable() to get configuration items, and then each item's ->set() method to modify a value within the configuration.
createSaveClearDelete()
Shows the full life cycle of both a configuration item and also deleting certain data within it using ->clear().

We'll show below how to use this class, and what results to expect.

B. Working with configuration using YAML files

Let's create three new YAML files in the config/install/ subfolder. These will in turn install configuration with three names corresponding to the naming convention notes above:

  1. For this module: d8api.some.filename.yml
  2. For the core date formatting: core.date_format.d8api_date.yml
  3. Incorrectly namespaced: notamodule.d8api.yml

Depending on what tutorials you've completed previously, you'll probably have other files in config/install/ already.

d8api.some.filename.yml

This will trivially create some configuration, owned by the module:

some_config:
  another_key: [1, 2, 3]

It will uninstall along with d8api module.

views.view.d8api_dependency.yml

This will create a date format, owned by core. Because core defines a core.data_types.schema.yml for these config entities and others, we do need to provide valid data, not just some test keys and values:

langcode: en
status: true
dependencies:
  enforced:
    module:
      - d8api
id: d8api_date
label: 'Example date format'
locked: false
pattern: 'm/d/Y - H:i'

We also want to demonstrate that this configuration can be uninstalled along with the d8api module. Hence the YAML includes dependencies: enforced: module: - d8api: that means, although this configuration "belongs" to core, it will also be uninstalled when d8api is uninstalled.

Dependency enforcement only makes sense for config entities, because config objects are installed and uninstalled only with the module that owns them i.e. shares their namespace.

notamodule.d8api.yml

Finally, let's do what we shouldn't do. Here's a module with a broken namespace, as the filename above shows:

some_config:
  another_key: [1, 2, 3]

Once again, the data provided by the configuration is for illustrative purposes only. Also, because that data doesn't specify any dependencies, installation of it does not (at the time of writing) trigger a dependency check on the presence of the notamodule module. It should be silently installed; as we'll see below, though, it remains even when the module

(By the way, the filename doesn't need to include the snippet .d8api.: we include that so we can keep track of it and delete it in the examples below.)

What you should see

As previously, you should have Drush available, and we don't cover that here. You should also uninstall the d8api module, so that the YAML examples included will work correctly.

Begin by exporting all of your site's current configuration into a folder:

drush config-export

You should see the response message:

 [success] Configuration successfully exported to some/folder.

Now enable the d8api module, and re-export the configuration:

drush en -y d8api
drush config-export

The module will be enabled first:

The following extensions will be enabled: d8api
Do you really want to continue? (y/n): y
 [ok] d8api was enabled successfully.
d8api defines the following permissions: add contact entity, administer contact entity, delete contact entity, edit contact entity, view contact entity

And then config-export will recognize that new configuration exists and ask for confirmation:

Differences of the active config to the export directory:
 
 Collection  Config                                           Operation                
             d8api.contact_type.person                        create 
             d8api.some.filename                              create 
             field.storage.contact.field_jobtitle             create 
             field.field.contact.person.field_jobtitle        create 
             core.entity_view_display.contact.person.default  create 
             core.entity_form_display.contact.person.default  create 
             notamodule.d8api                                 create 
             core.extension                                   update
The .yml files in your export directory (/tmp/sync) will be deleted and replaced with the active config. (y/n):

The only update you should see will be to the core.extension configuration: the list of enabled modules now includes d8api. You can confirm or deny at the "y/n" prompt above: it doesn't matter.

We'll come back to the installed configuration later. Right now, let's look at the code examples. Firstly, run the temporarilyOverride() method as follows:

drush php-eval '(new \Drupal\d8api\ConfigExample(
  \Drupal::service("config.factory")
))->temporarilyOverride();'

This will produce the following output:

Raw data for system.site:
array(10) {
  ["uuid"]=>
  string(36) "75cc2103-601c-4eeb-aaeb-ac350d5cb945"
  ["name"]=>
  string(8) "Tutorial"
  # ... More details here....
}
Site name has been changed to: Example for config
Site name has been changed back to Tutorial.

As you can see, system.site is a config object which happens to have a UUID. The output further shows that, when the config object is saved and then reloaded, its title has been changed; and then changed back, so this example doesn't permanently rename your site!

Secondly, we run the createSaveClearDelete() example as follows:

drush php-eval '(new \Drupal\d8api\ConfigExample(
  \Drupal::service("config.factory")
))->createSaveClearDelete();'
New config is empty? Y
Config with 'foo' key cleared out: array (
  'baz' => 
  array (
    'quux' => 
    array (
      'fred' => 'barney',
    ),
  ),
)
Config 'baz' data: array (
  'quux' => 
  array (
    'fred' => 'barney',
  ),
)
Config has been deleted? Y

The output from this shows respectively that: new, as-yet-unsaved config objects have an empty array as their "raw data"; the ->clear() method only unsets specific subarrays within the configuration; we can retrieve sub-arrays of configuration using keys; and that the delete method entirely removes the config object from the site.

Finally, let's look at what our configuration YAML files have created. Firstly, check the config table:

drush sqlq "SELECT name FROM config WHERE name LIKE '%d8api%';"

This should produce output something like:

core.date_format.d8api_date
d8api.contact_type.person
d8api.some.filename
notamodule.d8api

The person contact type is discussed in our previous tutorial on fields API; you can see the other three YAML files have resulted in three items of configuration.

Now let's uninstall our d8api module, and re-examine the config table:

drush pm-uninstall -y d8api
drush sqlq "SELECT name FROM config WHERE name LIKE '%d8api%';"

The result of uninstallation should be that all but one file has been removed:

The following extensions will be uninstalled: d8api
Do you really want to continue? (y/n): y
 [ok] d8api was successfully uninstalled.
notamodule.d8api

You can see that the badly-namespaced configuration has been "orphaned", although the others have been removed: the contact type, because it depends on a bundle defined in the module; the d8api.some.filename, because its namespace shows it is owned by the module; and the date format, because its YAML contained an enforced dependency.

Unfortunately, this orphaned configuration will now prevent d8api from reinstalling itself. Luckily, configuration items all live solely in the config database table, so (just this once, to fix a broken site), run:

drush sqlq "DELETE FROM config WHERE name LIKE '%d8api%';"

You should then also remove the notamodule.d8api.yml file from the module, before reinstalling.

If you can see all of this, congratulations! you have learned what configuration is, how it's stored, and how to manipulate it.

Further reading etc.

Add new comment