Unverified Commit 4e8f9778 authored by larowlan's avatar larowlan
Browse files

Issue #3077504 by bircher, marcvangend, alexpott, borisson_: Add...

Issue #3077504 by bircher, marcvangend, alexpott, borisson_: Add config_exclude functionality to core
parent 5ad59681
......@@ -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 @@ public function __construct(StorageInterface $active, StateInterface $state, Con
* {@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));
......
<?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;
}
}
<?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);
}
}
langcode: en
status: true
dependencies:
enforced:
module:
- config_test
id: exclude_test
label: Test
description: 'Test menu depending on config_test'
locked: true
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
# @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
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 @@ public function onExportTransform(StorageTransformEvent $event) {
// 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 @@ public static function getSubscribedEvents() {
// @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 @@ public function testGetStorage() {
// 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']);
}
}
<?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']);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment