diff --git a/config/install/config_ignore.settings.yml b/config/install/config_ignore.settings.yml
index 629b342..c89e103 100644
--- a/config/install/config_ignore.settings.yml
+++ b/config/install/config_ignore.settings.yml
@@ -1 +1,2 @@
ignored_config_entities: { }
+ignored_config_collections: { }
diff --git a/config/schema/config_ignore.schema.yml b/config/schema/config_ignore.schema.yml
index 9930f0d..889c34c 100644
--- a/config/schema/config_ignore.schema.yml
+++ b/config/schema/config_ignore.schema.yml
@@ -7,3 +7,8 @@ config_ignore.settings:
label: 'List of ignored configurations'
sequence:
type: string
+ ignored_config_collections:
+ type: sequence
+ label: 'List of ignored configuration storage collections'
+ sequence:
+ type: string
diff --git a/config_ignore.api.php b/config_ignore.api.php
index 89e9f91..1054e84 100644
--- a/config_ignore.api.php
+++ b/config_ignore.api.php
@@ -18,6 +18,13 @@ function hook_config_ignore_settings_alter(array &$settings) {
$settings[] = 'field.*';
}
+/**
+ * Alter the list of config entities that should be ignored.
+ */
+function hook_config_ignore_collections_alter(array &$collections) {
+ $collections[] = 'language.*';
+}
+
/**
* @} End of "addtogroup hooks".
*/
diff --git a/config_ignore.install b/config_ignore.install
index cf8f3a4..b602315 100644
--- a/config_ignore.install
+++ b/config_ignore.install
@@ -29,3 +29,11 @@ function config_ignore_update_8301() {
\Drupal::cache('discovery')->delete('config_filter_plugins');
}
+/**
+ * Update schema with storage collection ignore value.
+ */
+function config_ignore_update_8302() {
+ $config = \Drupal::configFactory()->getEditable('config_ignore.settings');
+ $config->set('ignored_config_collections', [])->save();
+}
+
diff --git a/src/EventSubscriber/ConfigIgnoreEventSubscriber.php b/src/EventSubscriber/ConfigIgnoreEventSubscriber.php
index 70c9162..3a14812 100644
--- a/src/EventSubscriber/ConfigIgnoreEventSubscriber.php
+++ b/src/EventSubscriber/ConfigIgnoreEventSubscriber.php
@@ -131,10 +131,26 @@ class ConfigIgnoreEventSubscriber implements EventSubscriberInterface, CacheTags
$collection_names = array_unique(array_merge($transformation_storage->getAllCollectionNames(), $destination_storage->getAllCollectionNames()));
array_unshift($collection_names, StorageInterface::DEFAULT_COLLECTION);
+ $ignored_collections = array_keys($this->getIgnoredCollections($collection_names));
+
foreach ($collection_names as $collection_name) {
$destination_storage = $destination_storage->createCollection($collection_name);
$transformation_storage = $transformation_storage->createCollection($collection_name);
+ if (in_array($collection_name, $ignored_collections)) {
+ foreach ($this->configFactory->listAll() as $config_name) {
+ $collection_source_data = $destination_storage->read($config_name);
+
+ if (is_array($collection_source_data) && count(array_filter($collection_source_data)) != 0) {
+ $transformation_storage->write($config_name, $destination_storage->read($config_name));
+ }
+ else {
+ $transformation_storage->delete($config_name);
+ }
+ }
+ continue;
+ }
+
// Loop over the ignored config in the destination.
// We need to do this inside of the collection loop because some config
// to be ignored may only be present in some collections.
@@ -247,6 +263,77 @@ class ConfigIgnoreEventSubscriber implements EventSubscriberInterface, CacheTags
return array_diff_key($ignored_configs, array_flip($exceptions));
}
+ /**
+ * Returns the list of all ignored collections by expanding the wildcards.
+ *
+ * @param array $collection_names
+ * An array of existing collection names.
+ *
+ * @return array
+ * An associative array keyed by collection name and having the values either
+ * NULL, if the whole config is ignored, or an array of keys to be ignored.
+ * Each key is an array of parents:
+ * @code
+ * [
+ * 'system.site' => NULL,
+ * 'user.settings' => [
+ * ['notify', 'cancel_confirm'],
+ * ['password_reset_timeout'],
+ * ],
+ * ]
+ * @endcode
+ */
+ protected function getIgnoredCollections(array $collection_names) {
+ /** @var string[] $ignored_collections_patterns */
+ $ignored_collections_patterns = $this->configFactory->get('config_ignore.settings')->get('ignored_config_collections');
+ $this->moduleHandler->invokeAll('config_ignore_collections_alter', [&$ignored_collections_patterns]);
+
+ // Builds ignored collections exceptions and remove them from the pattern list.
+ $exceptions = [];
+ foreach ($ignored_collections_patterns as $delta => $ignored_collection_pattern) {
+ if (strpos($ignored_collection_pattern, '~') === 0) {
+ if (strpos($ignored_collection_pattern, '*') !== FALSE) {
+ throw new \LogicException("A collection ignore pattern entry cannot contain both, '~' and '*'.");
+ }
+ $exceptions[] = substr($ignored_collection_pattern, 1);
+ unset($ignored_collections_patterns[$delta]);
+ }
+ }
+
+ $ignored_collections = [];
+ foreach ($collection_names as $collection_name) {
+ foreach ($ignored_collections_patterns as $ignored_collection_pattern) {
+ if (strpos($ignored_collection_pattern, ':') !== FALSE) {
+ // Some patterns are defining also a key.
+ [$collection_name_pattern, $key] = explode(':', $ignored_collection_pattern, 2);
+ $key = trim($key);
+ if (strpos($key, '*') !== FALSE) {
+ throw new \LogicException("The key part of the config ignore pattern cannot contain the wildcard character '*'.");
+ }
+ }
+ else {
+ $collection_name_pattern = $ignored_collection_pattern;
+ $key = NULL;
+ }
+ if ($this->wildcardMatch($collection_name_pattern, $collection_name)) {
+ if ($key) {
+ $ignored_collections[$collection_name][] = explode('.', $key);
+ }
+ else {
+ $ignored_collections[$collection_name] = NULL;
+ // As this pattern has no key we continue with the next config. Any
+ // subsequent pattern with the same config but with key is covered
+ // by this ignore pattern.
+ break;
+ }
+ }
+ }
+ }
+
+ // Extract the exceptions from the ignored configs.
+ return array_diff_key($ignored_collections, array_flip($exceptions));
+ }
+
/**
* Checks if a string matches a given wildcard pattern.
*
diff --git a/src/Form/Settings.php b/src/Form/Settings.php
index d5b6baa..d1bf521 100644
--- a/src/Form/Settings.php
+++ b/src/Form/Settings.php
@@ -54,6 +54,16 @@ Examples:
'#default_value' => implode(PHP_EOL, $config_ignore_settings->get('ignored_config_entities')),
'#size' => 60,
];
+
+ $form['ignored_config_collections'] = [
+ '#type' => 'textarea',
+ '#rows' => 10,
+ '#title' => $this->t('Configuration storage collections to ignore'),
+ '#description' => $this->t('Use the same format for including/excluding/wildcarding as above (e.g. language.* for ignoring all language collections).'),
+ '#default_value' => implode(PHP_EOL, $config_ignore_settings->get('ignored_config_collections') ?: []),
+ '#size' => 60,
+ ];
+
return parent::buildForm($form, $form_state);
}
@@ -69,7 +79,14 @@ Examples:
array_map('trim', explode("\n", $ignored_config_entities))
)
);
+ $ignored_config_entities = str_replace("\r", "\n", $form_state->getValue('ignored_config_collections'));
+ $config_collection_storage_array = array_values(
+ array_filter(
+ array_map('trim', explode("\n", $ignored_config_entities))
+ )
+ );
$config_ignore_settings->set('ignored_config_entities', $config_ignore_settings_array);
+ $config_ignore_settings->set('ignored_config_collections', $config_collection_storage_array);
$config_ignore_settings->save();
parent::submitForm($form, $form_state);
}
diff --git a/tests/modules/config_ignore_hook_test.module b/tests/modules/config_ignore_hook_test.module
index e45bca5..52208d0 100644
--- a/tests/modules/config_ignore_hook_test.module
+++ b/tests/modules/config_ignore_hook_test.module
@@ -14,3 +14,13 @@ function config_ignore_hook_test_config_ignore_settings_alter(array &$settings)
$count = \Drupal::state()->get('hook_config_ignore_settings_alter__call_count', 0);
\Drupal::state()->set('hook_config_ignore_settings_alter__call_count', $count + 1);
}
+
+/**
+ * Implements hook_config_ignore_settings_alter().
+ */
+function config_ignore_hook_test_config_ignore_collections_alter(array &$collections) {
+ $collections[] = 'language.*';
+
+ $count = \Drupal::state()->get('hook_config_ignore_collections_alter__call_count', 0);
+ \Drupal::state()->set('hook_config_ignore_collections_alter__call_count', $count + 1);
+}
diff --git a/tests/src/Functional/ConfigIgnoreHookTest.php b/tests/src/Functional/ConfigIgnoreHookTest.php
index 01808c6..cf75d26 100644
--- a/tests/src/Functional/ConfigIgnoreHookTest.php
+++ b/tests/src/Functional/ConfigIgnoreHookTest.php
@@ -80,4 +80,47 @@ class ConfigIgnoreHookTest extends ConfigIgnoreBrowserTestBase {
return \Drupal::state()->get('hook_config_ignore_settings_alter__call_count');
}
+ /**
+ * Test alter hook values are cached unless invalidated.
+ *
+ * Its important import/export is called directly, if called by a browser then
+ * static cache will not be present.
+ *
+ * @covers \Drupal\config_ignore\EventSubscriber\ConfigIgnoreEventSubscriber::getIgnoredCollections
+ */
+ public function testCollectionsAlterHookStaticCache() {
+ // Each of these are cloned since normally result of transformStorage
+ // statically cached by \Drupal\Core\Config\ManagedStorage::$manager.
+ $sourceOriginal = $this->container->get('config.storage.export');
+ $destinationOriginal = $this->container->get('config.storage.sync');
+
+ // Never called.
+ $this->assertNull($this->getCollectionsAlterCallCount());
+
+ // Initial call caches.
+ $this->copyConfig(clone $sourceOriginal, clone $destinationOriginal);
+ $this->assertEquals(1, $this->getCollectionsAlterCallCount());
+
+ // Subsequent calls read from cache.
+ $this->copyConfig(clone $sourceOriginal, clone $destinationOriginal);
+ $this->assertEquals(1, $this->getCollectionsAlterCallCount());
+
+ // Writing a new value to our config triggers cache tag invalidation.
+ // This value itself is inconsequential.
+ $this->config('config_ignore.settings')->set('ignored_config_entities', ['foo.bar'])->save();
+
+ $this->copyConfig(clone $sourceOriginal, clone $destinationOriginal);
+ $this->assertEquals(2, $this->getCollectionsAlterCallCount());
+ }
+
+ /**
+ * Get call count of alter hook.
+ *
+ * @return int|null
+ * Call count, or NULL if not called.
+ */
+ protected function getCollectionsAlterCallCount() {
+ return \Drupal::state()->get('hook_config_ignore_collections_alter__call_count');
+ }
+
}
diff --git a/tests/src/Kernel/IgnoreKernelTest.php b/tests/src/Kernel/IgnoreKernelTest.php
index 2546588..0d5a921 100644
--- a/tests/src/Kernel/IgnoreKernelTest.php
+++ b/tests/src/Kernel/IgnoreKernelTest.php
@@ -55,6 +55,8 @@ class IgnoreKernelTest extends KernelTestBase {
* The import modes.
* @param array $patterns
* An array of ignore patterns, we may refactor this to be the whole config.
+ * @param array $collections
+ * An array of ignored collections.
* @param array $active
* Modifications to the active config.
* @param array $sync
@@ -64,8 +66,9 @@ class IgnoreKernelTest extends KernelTestBase {
*
* @dataProvider importProvider
*/
- public function testImport(array $modes, array $patterns, array $active, array $sync, array $expected) {
+ public function testImport(array $modes, array $patterns, array $collections, array $active, array $sync, array $expected) {
$this->config('config_ignore.settings')->set('ignored_config_entities', $patterns)->save();
+ $this->config('config_ignore.settings')->set('ignored_config_collections', $collections)->save();
$expectedStorage = $this->setUpStorages($active, $sync, $expected);
@@ -85,6 +88,8 @@ class IgnoreKernelTest extends KernelTestBase {
[],
// The ignore config.
[],
+ // The ignored collections.
+ [],
// Modifications to the active config keyed by language.
[],
// Modifications to the sync config keyed by language.
@@ -96,6 +101,7 @@ class IgnoreKernelTest extends KernelTestBase {
[],
['config_test.system'],
[],
+ [],
[
// Delete the config_test.system from all languages in sync storage.
'' => ['config_test.system' => FALSE],
@@ -107,6 +113,7 @@ class IgnoreKernelTest extends KernelTestBase {
'remove translation when not ignored' => [
[],
['config_test.system'],
+ [],
['de' => ['config_test.no_status.default' => ['label' => 'DE default']]],
[],
[],
@@ -114,6 +121,7 @@ class IgnoreKernelTest extends KernelTestBase {
'do not remove translation when ignored' => [
[],
['config_test.system'],
+ [],
['de' => ['config_test.system' => ['foo' => 'Neues Foo']]],
[],
['de' => ['config_test.system' => ['foo' => 'Neues Foo']]],
@@ -121,6 +129,15 @@ class IgnoreKernelTest extends KernelTestBase {
'do not remove translation when key is ignored' => [
[],
['config_test.system:foo'],
+ [],
+ ['de' => ['config_test.system' => ['foo' => 'Neues Foo']]],
+ [],
+ ['de' => ['config_test.system' => ['foo' => 'Neues Foo']]],
+ ],
+ 'do not remove translation when collection is ignored' => [
+ [],
+ [],
+ ['language.de'],
['de' => ['config_test.system' => ['foo' => 'Neues Foo']]],
[],
['de' => ['config_test.system' => ['foo' => 'Neues Foo']]],
@@ -128,6 +145,7 @@ class IgnoreKernelTest extends KernelTestBase {
'remove translation when other key is ignored' => [
[],
['config_test.system:404'],
+ [],
['de' => ['config_test.system' => ['foo' => 'Neues Foo']]],
[],
[],
@@ -136,12 +154,14 @@ class IgnoreKernelTest extends KernelTestBase {
['strict'],
['config_test.*'],
[],
+ [],
['se' => ['config_test.system' => ['foo' => 'Ny foo']]],
[],
],
'new config is ignored' => [
['strict'],
['config_test.*'],
+ [],
[
'' => [
'config_test.dynamic.exist' => ['id' => 'exist', 'label' => 'E'],
@@ -163,6 +183,7 @@ class IgnoreKernelTest extends KernelTestBase {
// 'new config is not ignored in lenient mode' => [
// ['lenient'],
// ['config_test.*'],
+ // [],
// [
// '' => [
// 'config_test.dynamic.exist' => ['id' => 'exist', 'label' => 'E'],
@@ -185,6 +206,7 @@ class IgnoreKernelTest extends KernelTestBase {
'new config with only key ignored (issue 3137437)' => [
['strict'],
['config_test.*:label'],
+ [],
['' => ['config_test.dynamic.exist' => ['id' => 'exist', 'label' => 'E']]],
[],
[],
@@ -192,6 +214,7 @@ class IgnoreKernelTest extends KernelTestBase {
// 'new config with only key ignored lenient (issue 3137437)' => [
// ['lenient'],
// ['config_test.*:label'],
+ // [],
// ['' => ['config_test.dynamic.exist' => ['id' => 'exist', 'label' => 'E']]],
// [],
// ['' => ['config_test.dynamic.exist' => ['id' => 'exist', 'label' => 'E']]],
@@ -202,10 +225,12 @@ class IgnoreKernelTest extends KernelTestBase {
/**
* Test the export transformations.
*
- * @param string $mode
- * The export mode
+ * @param array $modes
+ * The export modes
* @param array $patterns
* An array of ignore patterns, we may refactor this to be the whole config.
+ * @param array $collections
+ * An array of ignored collections.
* @param array $active
* Modifications to the active config.
* @param array $sync
@@ -215,8 +240,9 @@ class IgnoreKernelTest extends KernelTestBase {
*
* @dataProvider exportProvider
*/
- public function testExport(array $modes, array $patterns, array $active, array $sync, array $expected) {
+ public function testExport(array $modes, array $patterns, array $collections, array $active, array $sync, array $expected) {
$this->config('config_ignore.settings')->set('ignored_config_entities', $patterns)->save();
+ $this->config('config_ignore.settings')->set('ignored_config_collections', $collections)->save();
$expectedStorage = $this->setUpStorages($active, $sync, $expected);
@@ -236,6 +262,8 @@ class IgnoreKernelTest extends KernelTestBase {
['off'],
// The ignore config.
[],
+ // The ignored collections.
+ [],
// Modifications to the active config keyed by language.
[],
// Modifications to the sync config keyed by language.