The Configuration API is by far the best surprise I got about Drupal 9. Finally, a core system that is robust enough to hold any configuration set reliably, and extensible enough for contrib modules. Back in Drupal 7, maintaining a consistent configuration across stages had been the bane of my existence, and I was delighted to find it was now a solved problem.

One minor wrinkle I found is related to the scenario of admin users wanting to update the configs that are otherwise stored in source control:

  • Admin changes a permission on stage PROD via Admin UI
  • Devops makes a code deployment on stages DEV => TEST => PROD
  • The permission change is lost, unless Admin exports the updated permission config and hands it to Devops before deployment

To support this scenario, Admin needs to go to Configuration synchronization /admin/config/development/configuration, examine the changed items, then head over to Single export /admin/config/development/configuration/single/export and GUESS how the name that they saw on the previous screen maps to a given configuration type/name pair on this one. User-unfriendly and error-prone!

My quick solution was to add an Export config action for each updated item in the Configuration synchronization screen, as per the attached screenshot. This was feasible to implement because the Single export route actually accepts a specific configuration type/name pair, which my code computes given the configuration item (and that was not terribly straightforward). Now Admin can easily export all modified configuration items without any guesswork!

use Drupal\Core\Config\Entity\ConfigEntityInterface;

/**
 * Implements hook_form_FORM_ID_alter() for config_admin_import_form.
 *
 * Show export link for each modified config item.
 */
function MYMODULE_form_config_admin_import_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $configs = [];
  foreach (\Drupal::service('entity_type.manager')->getDefinitions() as $entity_type => $definition) {
    if ($definition->entityClassImplements(ConfigEntityInterface::class)) {
      $entity_storage = \Drupal::service('entity_type.manager')->getStorage($entity_type);
      foreach ($entity_storage->loadMultiple() as $entity) {
        $configs[$definition->getConfigPrefix() . '.' . $entity->id()] = [
          'config_type' => $entity_type,
          'config_name' => $entity->id(),
        ];
      }
    }
  }

  $collection = '';
  $config_change_type = 'update';
  if (!empty($form[$collection][$config_change_type]['list']['#rows'])) {
    foreach ($form[$collection][$config_change_type]['list']['#rows'] as &$config_change) {
      $config_item = $config_change['name'];

      if (array_key_exists($config_item, $configs)) {
        $config_type = $configs[$config_item]['config_type'];
        $config_name = $configs[$config_item]['config_name'];
      }
      else {
        $config_type = 'system.simple';
        $config_name = $config_item;
      }

      $config_change['operations']['data']['#links']['export'] = [
        'title' => t('Export config'),
        'url' => Url::fromRoute('config.export_single', [
          'config_type' => $config_type,
          'config_name' => $config_name,
        ]),
      ];
    }
  }
}