Problem/Motivation

The decoupled_config module currently uses a @ConfigFilter plugin (DecoupledConfigFilter) to merge YAML fragments during config import and strip them during config export. This operates at the FilteredStorage layer provided by the config_filter module.

However, config_split (the most widely used module for splitting configuration by environment, site, or feature) operates at a completely different layer: Drupal core's StorageTransformEvent (config.transform.import / config.transform.export). These two layers do not interact with each other during the config import/export pipeline.

As a result, decoupled config fragments are only merged into configuration items that exist in config/sync/. Any configuration item that is managed exclusively by a config_split (i.e., it only exists in a split directory and not in config/sync/) will never have decoupled fragments applied to it.

This is a significant limitation because config_split is the standard approach for managing feature-specific or environment-specific configuration. For example, a role defined in a feature split cannot receive permissions from other modules via decoupled_config fragments.

Steps to reproduce

  1. Install decoupled_config and config_split.
  2. Create a config split (e.g., a feature split) and place a role configuration (user.role.example_role) exclusively in that split directory (not in config/sync/).
  3. In a separate module, create decoupled_config/user.role.example_role.yml with additional permissions to inject.
  4. Run drush cim -y.
  5. Expected: The role has the permissions from the decoupled fragment merged in.
  6. Actual: The decoupled fragment is silently ignored. The role does not receive the additional permissions.

Proposed resolution

Replace the @ConfigFilter plugin with an EventSubscriber that listens to Drupal core's StorageTransformEvent. This places decoupled_config on the same layer as config_split, so it can see and modify all assembled configuration — both from config/sync/ and from any active config split.

The subscriber uses two priorities to ensure correct ordering relative to config_split:

  • Import (config.transform.import, priority -100): Runs after config_split (default priority 0), so split configurations are already present in the storage when fragments are merged.
  • Export (config.transform.export, priority 100): Runs before config_split (default priority 0), so injected fragment values are stripped before config_split writes configuration back to split directories. This prevents fragment values from "leaking" into split YAML files on drush cex.

The merge logic (NestedArray::mergeDeep) and strip logic (DecoupledConfigArrayHelper path-based removal) are ported directly from the existing DecoupledConfigFilter::filterRead() and filterWrite() methods.

Changes:

  • New: src/EventSubscriber/DecoupledConfigTransformSubscriber.php — the core subscriber with import merge and export strip handlers.
  • Modified: decoupled_config.services.yml — register the new event subscriber service.
  • Removed: src/Plugin/ConfigFilter/DecoupledConfigFilter.php — the @ConfigFilter plugin is replaced entirely. Keeping both would cause double-merge for config/sync/ items (once by the filter during storage read, again by the subscriber during transform).
  • Modified: decoupled_config.info.yml — remove the config_filter:config_filter dependency, as the module now only uses Drupal core APIs (StorageTransformEvent, ConfigEvents).

No changes to DecoupledConfig, DecoupledConfigYamlDiscovery, DecoupledConfigArrayHelper, or the decoupled_config/ directory convention in modules. Existing module fragments continue to work without modification.

Remaining tasks

  • Review and commit the patch.
  • Verify backward compatibility: modules using existing decoupled_config/ fragments should work without changes.
  • Verify that config_filter can be safely uninstalled after this change if no other module depends on it.

User interface changes

None.

API changes

  • The config_filter:config_filter module dependency is removed. decoupled_config now depends only on Drupal core APIs.
  • The @ConfigFilter plugin decoupled_config_filter is removed. Any code referencing this plugin ID will need to be updated (unlikely, as it was an internal implementation detail).
  • The DecoupledConfig service and DecoupledConfigInterface remain unchanged. The decoupled_config/ directory convention for YAML fragments remains unchanged.

Data model changes

None.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

jansete created an issue. See original summary.

jansete’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.