Issue #3077504 by bircher, marcvangend, alexpott, borisson_: Add config_exclude functionality to core
parent
5ad596810f
commit
4e8f9778b1
|
|
@ -9,6 +9,7 @@
|
|||
// See the experimental modules policy https://www.drupal.org/core/experimental
|
||||
// @todo: remove class aliases in #2991683
|
||||
@class_alias('Drupal\config_environment\Core\Config\StorageTransformEvent', 'Drupal\Core\Config\StorageTransformEvent');
|
||||
@class_alias('Drupal\config_environment\Core\Config\StorageRebuildNeededEvent', 'Drupal\Core\Config\StorageRebuildNeededEvent');
|
||||
@class_alias('Drupal\config_environment\Core\Config\ManagedStorage', 'Drupal\Core\Config\ManagedStorage');
|
||||
@class_alias('Drupal\config_environment\Core\Config\StorageManagerInterface', 'Drupal\Core\Config\StorageManagerInterface');
|
||||
@class_alias('Drupal\config_environment\Core\Config\ExportStorageManager', 'Drupal\Core\Config\ExportStorageManager');
|
||||
|
|
|
|||
|
|
@ -11,3 +11,9 @@ services:
|
|||
arguments: ['@config.storage', '@state', '@database', '@event_dispatcher']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
# config_environment services.
|
||||
config_environment.excluded_modules.event_subscriber:
|
||||
class: Drupal\config_environment\EventSubscriber\ExcludedModulesEventSubscriber
|
||||
arguments: ['@config.storage', '@settings', '@config.manager', '@state']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
|
|
|
|||
|
|
@ -71,9 +71,38 @@ final class ConfigEvents {
|
|||
*
|
||||
* @see \Drupal\Core\Config\StorageTransformEvent
|
||||
* @see \Drupal\Core\Config\ConfigEvents::STORAGE_TRANSFORM_IMPORT
|
||||
* @see \Drupal\Core\Config\ConfigEvents::STORAGE_EXPORT_REBUILD
|
||||
* @see \Drupal\config_environment\Core\Config\ExportStorageManager::getStorage
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const STORAGE_TRANSFORM_EXPORT = 'config.transform.export';
|
||||
|
||||
/**
|
||||
* Name of the event fired when the export storage may need to be rebuilt.
|
||||
*
|
||||
* This event allows subscribers to indicate that the export storage should be
|
||||
* rebuilt. The event listener method receives a
|
||||
* \Drupal\Core\Config\StorageRebuildNeededEvent instance.
|
||||
* When this event is set to be needing a rebuild by a subscriber then the
|
||||
* \Drupal\Core\Config\ConfigEvents::STORAGE_TRANSFORM_EXPORT event will be
|
||||
* dispatched.
|
||||
*
|
||||
* @code
|
||||
* if ($exportStorageIsOutOfDateConditionIsMet) {
|
||||
* $event->setRebuildNeeded();
|
||||
* }
|
||||
* // else, do nothing.
|
||||
* @endcode
|
||||
*
|
||||
* @Event
|
||||
*
|
||||
* @see \Drupal\Core\Config\StorageRebuildNeededEvent
|
||||
* @see \Drupal\Core\Config\ConfigEvents::STORAGE_TRANSFORM_EXPORT
|
||||
* @see \Drupal\config_environment\Core\Config\ExportStorageManager::getStorage
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const STORAGE_EXPORT_REBUILD = 'config.export.rebuild';
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,12 @@ class ExportStorageManager implements StorageManagerInterface, EventSubscriberIn
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStorage() {
|
||||
if ($this->state->get(self::NEEDS_REBUILD_KEY, TRUE)) {
|
||||
$rebuild = $this->state->get(self::NEEDS_REBUILD_KEY, TRUE);
|
||||
if (!$rebuild) {
|
||||
// @todo: Use ConfigEvents::STORAGE_EXPORT_REBUILD in #2991683
|
||||
$rebuild = $this->eventDispatcher->dispatch('config.export.rebuild', new StorageRebuildNeededEvent())->isRebuildNeeded();
|
||||
}
|
||||
if ($rebuild) {
|
||||
self::replaceStorageContents($this->active, $this->storage);
|
||||
// @todo: Use ConfigEvents::STORAGE_TRANSFORM_EXPORT in #2991683
|
||||
$this->eventDispatcher->dispatch('config.transform.export', new StorageTransformEvent($this->storage));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\config_environment\Core\Config;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* The dispatched by a storage manager to check if a rebuild is needed.
|
||||
*/
|
||||
class StorageRebuildNeededEvent extends Event {
|
||||
|
||||
/**
|
||||
* The flag which keeps track of whether the storage needs to be rebuilt.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $rebuildNeeded = FALSE;
|
||||
|
||||
/**
|
||||
* Flags to the config storage manager that a rebuild is needed.
|
||||
*/
|
||||
public function setRebuildNeeded() {
|
||||
$this->rebuildNeeded = TRUE;
|
||||
$this->stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the storage needs to be rebuilt or not.
|
||||
*
|
||||
* @return bool
|
||||
* Whether the rebuild is needed or not.
|
||||
*/
|
||||
public function isRebuildNeeded() {
|
||||
return $this->rebuildNeeded;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\config_environment\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Config\ConfigManagerInterface;
|
||||
use Drupal\Core\Config\StorageInterface;
|
||||
use Drupal\Core\Config\StorageRebuildNeededEvent;
|
||||
use Drupal\Core\Config\StorageTransformEvent;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* The event subscriber preventing excluded modules to be exported.
|
||||
*/
|
||||
final class ExcludedModulesEventSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* The key in settings and state for listing excluded modules.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const EXCLUDED_MODULES_KEY = "config_exclude_modules";
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Config\StorageInterface
|
||||
*/
|
||||
private $activeStorage;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Site\Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Config\ConfigManagerInterface
|
||||
*/
|
||||
private $manager;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
private $state;
|
||||
|
||||
/**
|
||||
* EnvironmentModulesEventSubscriber constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\StorageInterface $active_storage
|
||||
* The active config storage.
|
||||
* @param \Drupal\Core\Site\Settings $settings
|
||||
* The Drupal settings.
|
||||
* @param \Drupal\Core\Config\ConfigManagerInterface $manager
|
||||
* The config manager.
|
||||
* @param \Drupal\Core\State\StateInterface $state
|
||||
* The Drupal state.
|
||||
*/
|
||||
public function __construct(StorageInterface $active_storage, Settings $settings, ConfigManagerInterface $manager, StateInterface $state) {
|
||||
$this->activeStorage = $active_storage;
|
||||
$this->settings = $settings;
|
||||
$this->manager = $manager;
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
// React early on export and late on import.
|
||||
return [
|
||||
'config.transform.import' => ['onConfigTransformImport', -500],
|
||||
'config.transform.export' => ['onConfigTransformExport', 500],
|
||||
'config.export.rebuild' => ['onExportStorageNeedsRebuild', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the export storage as out of date when the settings changed.
|
||||
*
|
||||
* @param \Drupal\Core\Config\StorageRebuildNeededEvent $event
|
||||
* The event to control the storage rebuild.
|
||||
*/
|
||||
public function onExportStorageNeedsRebuild(StorageRebuildNeededEvent $event) {
|
||||
// If the excluded modules are not the same as last time, re-transform.
|
||||
if ($this->state->get(self::EXCLUDED_MODULES_KEY) != $this->getExcludedModules()) {
|
||||
$event->setRebuildNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the storage which is used to import the configuration.
|
||||
*
|
||||
* Make sure excluded modules are not uninstalled by adding them and their
|
||||
* config to the storage when importing configuration.
|
||||
*
|
||||
* @param \Drupal\Core\Config\StorageTransformEvent $event
|
||||
* The transformation event.
|
||||
*/
|
||||
public function onConfigTransformImport(StorageTransformEvent $event) {
|
||||
$storage = $event->getStorage();
|
||||
if (!$storage->exists('core.extension')) {
|
||||
// If the core.extension config is not present there is nothing to do.
|
||||
// This means that probably the storage is empty or non-functional.
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_merge([StorageInterface::DEFAULT_COLLECTION], $this->activeStorage->getAllCollectionNames()) as $collectionName) {
|
||||
$collection = $storage->createCollection($collectionName);
|
||||
$activeCollection = $this->activeStorage->createCollection($collectionName);
|
||||
foreach ($this->getDependentConfigNames() as $configName) {
|
||||
if (!$collection->exists($configName) && $activeCollection->exists($configName)) {
|
||||
// Make sure the config is not removed if it exists.
|
||||
$collection->write($configName, $activeCollection->read($configName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$extension = $storage->read('core.extension');
|
||||
$existing = $this->activeStorage->read('core.extension');
|
||||
|
||||
$modules = $extension['module'];
|
||||
foreach ($this->getExcludedModules() as $module) {
|
||||
if (array_key_exists($module, $existing['module'])) {
|
||||
// Set the modules weight from the active store.
|
||||
$modules[$module] = $existing['module'][$module];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the extensions.
|
||||
$extension['module'] = module_config_sort($modules);
|
||||
// Set the modified extension.
|
||||
$storage->write('core.extension', $extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the storage which is used to export the configuration.
|
||||
*
|
||||
* Make sure excluded modules are not exported by removing all the config
|
||||
* which depends on them from the storage that is exported.
|
||||
*
|
||||
* @param \Drupal\Core\Config\StorageTransformEvent $event
|
||||
* The transformation event.
|
||||
*/
|
||||
public function onConfigTransformExport(StorageTransformEvent $event) {
|
||||
// Save which modules are excluded in state to know if it has changed.
|
||||
$this->state->set(self::EXCLUDED_MODULES_KEY, $this->getExcludedModules());
|
||||
|
||||
$storage = $event->getStorage();
|
||||
if (!$storage->exists('core.extension')) {
|
||||
// If the core.extension config is not present there is nothing to do.
|
||||
// This means some other process has rendered it non-functional already.
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_merge([StorageInterface::DEFAULT_COLLECTION], $storage->getAllCollectionNames()) as $collectionName) {
|
||||
$collection = $storage->createCollection($collectionName);
|
||||
foreach ($this->getDependentConfigNames() as $configName) {
|
||||
$collection->delete($configName);
|
||||
}
|
||||
}
|
||||
|
||||
$extension = $storage->read('core.extension');
|
||||
// Remove all the excluded modules from the extensions list.
|
||||
$extension['module'] = array_diff_key($extension['module'], array_flip($this->getExcludedModules()));
|
||||
|
||||
$storage->write('core.extension', $extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the modules set as excluded in the Drupal settings.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of module names.
|
||||
*/
|
||||
private function getExcludedModules() {
|
||||
return $this->settings->get(self::EXCLUDED_MODULES_KEY, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the configuration which depends on one of the excluded modules.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of configuration names.
|
||||
*/
|
||||
private function getDependentConfigNames() {
|
||||
$modules = $this->getExcludedModules();
|
||||
|
||||
$dependencyManager = $this->manager->getConfigDependencyManager();
|
||||
$config = [];
|
||||
|
||||
// Find all the configuration depending on the excluded modules.
|
||||
foreach ($modules as $module) {
|
||||
foreach ($dependencyManager->getDependentEntities('module', $module) as $dependent) {
|
||||
$config[] = $dependent->getConfigDependencyName();
|
||||
}
|
||||
$config = array_merge($config, $this->activeStorage->listAll($module . '.'));
|
||||
}
|
||||
|
||||
// Find all configuration that depends on the configuration found above.
|
||||
foreach ($this->manager->findConfigEntityDependents('config', array_unique($config)) as $dependent) {
|
||||
$config[] = $dependent->getConfigDependencyName();
|
||||
}
|
||||
|
||||
return array_unique($config);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- config_test
|
||||
id: exclude_test
|
||||
label: Test
|
||||
description: 'Test menu depending on config_test'
|
||||
locked: true
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
config:
|
||||
- system.menu.exclude_test
|
||||
id: indirect_exclude_test
|
||||
label: Test
|
||||
description: 'Test menu depending indirectly on config_test'
|
||||
locked: true
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# @todo: Move this test module under the config module in #2991683.
|
||||
name: 'Configuration Module Exclude Test'
|
||||
type: module
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
config_transformer_test.event_subscriber:
|
||||
class: Drupal\config_transformer_test\EventSubscriber
|
||||
arguments: ['@config.storage', '@config.storage.sync']
|
||||
arguments: ['@config.storage', '@config.storage.sync', '@state']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
namespace Drupal\config_transformer_test;
|
||||
|
||||
use Drupal\Core\Config\StorageInterface;
|
||||
use Drupal\Core\Config\StorageRebuildNeededEvent;
|
||||
use Drupal\Core\Config\StorageTransformEvent;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
|
|
@ -28,6 +30,13 @@ class EventSubscriber implements EventSubscriberInterface {
|
|||
*/
|
||||
protected $sync;
|
||||
|
||||
/**
|
||||
* The Drupal state.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* EventSubscriber constructor.
|
||||
*
|
||||
|
|
@ -35,10 +44,13 @@ class EventSubscriber implements EventSubscriberInterface {
|
|||
* The active config storage.
|
||||
* @param \Drupal\Core\Config\StorageInterface $sync
|
||||
* The sync config storage.
|
||||
* @param \Drupal\Core\State\StateInterface $state
|
||||
* The Drupal state.
|
||||
*/
|
||||
public function __construct(StorageInterface $active, StorageInterface $sync) {
|
||||
public function __construct(StorageInterface $active, StorageInterface $sync, StateInterface $state) {
|
||||
$this->active = $active;
|
||||
$this->sync = $sync;
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -85,10 +97,23 @@ class EventSubscriber implements EventSubscriberInterface {
|
|||
// Add "Arrr" to the site slogan. Because pirates!
|
||||
// The active slogan will be ignored.
|
||||
$site['slogan'] = $sync['slogan'] . ' Arrr';
|
||||
$site['mail'] = $this->state->get('config_transform_test_mail', '');
|
||||
$storage->write('system.site', $site);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* React to the rebuilding the config export storage.
|
||||
*
|
||||
* @param \Drupal\Core\Config\StorageRebuildNeededEvent $event
|
||||
* The event we may stop.
|
||||
*/
|
||||
public function onExportRebuild(StorageRebuildNeededEvent $event) {
|
||||
if ($this->state->get('config_transform_test_rebuild', FALSE)) {
|
||||
$event->setRebuildNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
@ -96,6 +121,7 @@ class EventSubscriber implements EventSubscriberInterface {
|
|||
// @todo: use class constants when they get added in #2991683
|
||||
$events['config.transform.import'][] = ['onImportTransform'];
|
||||
$events['config.transform.export'][] = ['onExportTransform'];
|
||||
$events['config.export.rebuild'][] = ['onExportRebuild'];
|
||||
return $events;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@ class ExportStorageManagerTest extends KernelTestBase {
|
|||
// The test subscriber adds "Arrr" to the slogan of the sync config.
|
||||
$this->assertEquals('New name', $exported['name']);
|
||||
$this->assertEquals($rawConfig['slogan'] . ' Arrr', $exported['slogan']);
|
||||
|
||||
// Change the state which will not trigger a rebuild.
|
||||
$this->container->get('state')->set('config_transform_test_mail', 'config@drupal.example');
|
||||
|
||||
$storage = $this->container->get('config.storage.export.manager')->getStorage();
|
||||
$exported = $storage->read('system.site');
|
||||
// The mail is still set to the empty value from last time.
|
||||
$this->assertEquals('', $exported['mail']);
|
||||
|
||||
$this->container->get('state')->set('config_transform_test_rebuild', TRUE);
|
||||
$storage = $this->container->get('config.storage.export.manager')->getStorage();
|
||||
$exported = $storage->read('system.site');
|
||||
// The mail is still set to the value from the beginning.
|
||||
$this->assertEquals('config@drupal.example', $exported['mail']);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\config_environment\Kernel\EventSubscriber;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests ExcludedModulesEventSubscriber.
|
||||
*
|
||||
* @group config
|
||||
*/
|
||||
class ExcludedModulesEventSubscriberTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'system',
|
||||
'config_environment',
|
||||
'config_test',
|
||||
'config_exclude_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installConfig(['system', 'config_test', 'config_exclude_test']);
|
||||
$this->setSetting('config_exclude_modules', ['config_test']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test excluding modules from the config export.
|
||||
*/
|
||||
public function testExcludedModules() {
|
||||
// Assert that config_test is in the active config.
|
||||
$active = $this->container->get('config.storage');
|
||||
$this->assertNotEmpty($active->listAll('config_test.'));
|
||||
$this->assertNotEmpty($active->listAll('system.'));
|
||||
$this->assertArrayHasKey('config_test', $active->read('core.extension')['module']);
|
||||
$collection = $this->randomMachineName();
|
||||
foreach ($active->listAll() as $config) {
|
||||
$active->createCollection($collection)->write($config, $active->read($config));
|
||||
}
|
||||
|
||||
// Assert that config_test is not in the export storage.
|
||||
$export = $this->container->get('config.storage.export');
|
||||
$this->assertEmpty($export->listAll('config_test.'));
|
||||
$this->assertNotEmpty($export->listAll('system.'));
|
||||
$this->assertEmpty($export->createCollection($collection)->listAll('config_test.'));
|
||||
$this->assertNotEmpty($export->createCollection($collection)->listAll('system.'));
|
||||
$this->assertArrayNotHasKey('config_test', $export->read('core.extension')['module']);
|
||||
// The config_exclude_test is not excluded but the menu it installs are.
|
||||
$this->assertArrayHasKey('config_exclude_test', $export->read('core.extension')['module']);
|
||||
$this->assertFalse($export->exists('system.menu.exclude_test'));
|
||||
$this->assertFalse($export->exists('system.menu.indirect_exclude_test'));
|
||||
|
||||
// Assert that config_test is again in the import storage.
|
||||
$import = $this->container->get('config.import_transformer')->transform($export);
|
||||
$this->assertNotEmpty($import->listAll('config_test.'));
|
||||
$this->assertNotEmpty($import->listAll('system.'));
|
||||
$this->assertNotEmpty($import->createCollection($collection)->listAll('config_test.'));
|
||||
$this->assertNotEmpty($import->createCollection($collection)->listAll('system.'));
|
||||
$this->assertArrayHasKey('config_test', $import->read('core.extension')['module']);
|
||||
$this->assertArrayHasKey('config_exclude_test', $import->read('core.extension')['module']);
|
||||
$this->assertTrue($import->exists('system.menu.exclude_test'));
|
||||
$this->assertTrue($import->exists('system.menu.indirect_exclude_test'));
|
||||
|
||||
$this->assertEquals($active->read('core.extension'), $import->read('core.extension'));
|
||||
$this->assertEquals($active->listAll(), $import->listAll());
|
||||
foreach ($active->listAll() as $config) {
|
||||
$this->assertEquals($active->read($config), $import->read($config));
|
||||
}
|
||||
|
||||
// Changing the settings triggers the export storage manager to re-dispatch
|
||||
// the events so the config_text will not be excluded.
|
||||
$this->setSetting('config_exclude_modules', []);
|
||||
$export = $this->container->get('config.storage.export.manager')->getStorage();
|
||||
$this->assertArrayHasKey('config_test', $export->read('core.extension')['module']);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue