Unverified Commit 3ae3e525 authored by alexpott's avatar alexpott

Issue #3047812 by bircher, Krzysztof Domański, alexpott, ricardoamaro,...

Issue #3047812 by bircher, Krzysztof Domański, alexpott, ricardoamaro, larowlan, borisson_, mpotter: Add a Config Transformation event dispatching during config import and export
parent 86736407
......@@ -5,6 +5,15 @@
* Allows site administrators to modify environment configuration.
*/
// Set class aliases for the classes that will go into core when we are in beta.
// 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\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');
@class_alias('Drupal\config_environment\Core\Config\ImportStorageTransformer', 'Drupal\Core\Config\ImportStorageTransformer');
use Drupal\Core\Routing\RouteMatchInterface;
/**
......
# @todo: Stop taking over config module routes in #2991683
config.sync:
path: '/admin/config/development/configuration'
defaults:
_form: '\Drupal\config_environment\Form\ConfigSync'
_title: 'Synchronize'
requirements:
_permission: 'synchronize configuration'
config.diff:
path: '/admin/config/development/configuration/sync/diff/{source_name}/{target_name}'
defaults:
_controller: '\Drupal\config_environment\Controller\ConfigController::diff'
target_name: NULL
requirements:
_permission: 'synchronize configuration'
config.diff_collection:
path: '/admin/config/development/configuration/sync/diff_collection/{collection}/{source_name}/{target_name}'
defaults:
_controller: '\Drupal\config_environment\Controller\ConfigController::diff'
target_name: NULL
requirements:
_permission: 'synchronize configuration'
config.export_download:
path: '/admin/config/development/configuration/full/export-download'
defaults:
_controller: '\Drupal\config_environment\Controller\ConfigController::downloadExport'
requirements:
_permission: 'export configuration'
services:
# @todo: Move this back to core services in #2991683
config.import_transformer:
class: Drupal\config_environment\Core\Config\ImportStorageTransformer
arguments: ['@event_dispatcher', '@database']
config.storage.export:
class: Drupal\config_environment\Core\Config\ManagedStorage
arguments: ['@config.storage.export.manager']
config.storage.export.manager:
class: Drupal\config_environment\Core\Config\ExportStorageManager
arguments: ['@config.storage', '@state', '@database', '@event_dispatcher']
tags:
- { name: event_subscriber }
<?php
namespace Drupal\config_environment\Controller;
use Drupal\config\Controller\ConfigController as OriginalConfigController;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for config module routes.
*/
class ConfigController extends OriginalConfigController {
/**
* The import transformer service.
*
* @var \Drupal\Core\Config\ImportStorageTransformer
*/
protected $importTransformer;
/**
* The sync storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $syncStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$controller = parent::create($container);
$controller->importTransformer = $container->get('config.import_transformer');
$controller->syncStorage = $container->get('config.storage.sync');
return $controller;
}
/**
* {@inheritdoc}
*/
public function diff($source_name, $target_name = NULL, $collection = NULL) {
$this->sourceStorage = $this->importTransformer->transform($this->syncStorage);
return parent::diff($source_name, $target_name, $collection);
}
}
<?php
// @codingStandardsIgnoreStart
// @todo: Move this back to \Drupal\Core\Config\ConfigEvents in #2991683.
// @codingStandardsIgnoreEnd
namespace Drupal\config_environment\Core\Config;
/**
* Defines events for the configuration transform system.
*
* The constants in this class will be moved back into ConfigEvents.
* But due to the fact that the config_environment is not in beta we save their
* definitions here and use the literal strings in the mean time.
*
* @internal
*
* @deprecated The class will be merged with Drupal\Core\Config\ConfigEvents.
*/
final class ConfigEvents {
/**
* Name of the event fired just before importing configuration.
*
* This event allows subscribers to modify the configuration which is about to
* be imported. The event listener method receives a
* \Drupal\Core\Config\StorageTransformEvent instance. This event contains a
* config storage which subscribers can interact with and which will finally
* be used to import the configuration from.
* Together with \Drupal\Core\Config\ConfigEvents::STORAGE_TRANSFORM_EXPORT
* subscribers can alter the active configuration in a config sync workflow
* instead of just overriding at runtime via the config-override system.
* This allows a complete customisation of the workflow including additional
* modules and editable configuration in different environments.
*
* @code
* $storage = $event->getStorage();
* @endcode
*
* This event is also fired when just viewing the difference of configuration
* to be imported independently of whether the import takes place or not.
* Use the \Drupal\Core\Config\ConfigEvents::IMPORT event to subscribe to the
* import having taken place.
*
* @Event
*
* @see \Drupal\Core\Config\StorageTransformEvent
* @see \Drupal\Core\Config\ConfigEvents::STORAGE_TRANSFORM_EXPORT
*
* @var string
*/
const STORAGE_TRANSFORM_IMPORT = 'config.transform.import';
/**
* Name of the event fired when the export storage is used.
*
* This event allows subscribers to modify the configuration which is about to
* be exported. The event listener method receives a
* \Drupal\Core\Config\StorageTransformEvent instance. This event contains a
* config storage which subscribers can interact with and which will finally
* be used to export the configuration from.
*
* @code
* $storage = $event->getStorage();
* @endcode
*
* Typically subscribers will want to perform the reverse operation on the
* storage than for \Drupal\Core\Config\ConfigEvents::STORAGE_TRANSFORM_IMPORT
* to make sure successive exports and imports yield no difference.
*
* @Event
*
* @see \Drupal\Core\Config\StorageTransformEvent
* @see \Drupal\Core\Config\ConfigEvents::STORAGE_TRANSFORM_IMPORT
*
* @var string
*/
const STORAGE_TRANSFORM_EXPORT = 'config.transform.export';
}
<?php
// @codingStandardsIgnoreStart
// @todo: Move this back to \Drupal\Core\Config in #2991683.
// Use this class with its class alias Drupal\Core\Config\ExportStorageManager
// @codingStandardsIgnoreEnd
namespace Drupal\config_environment\Core\Config;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Config\ReadOnlyStorage;
use Drupal\Core\Config\StorageCopyTrait;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\State\StateInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* The export storage manager dispatches an event for the export storage.
*
* @internal
*/
class ExportStorageManager implements StorageManagerInterface, EventSubscriberInterface {
use StorageCopyTrait;
/**
* The state key indicating that the export storage needs to be rebuilt.
*/
const NEEDS_REBUILD_KEY = 'config_export_needs_rebuild';
/**
* The active configuration storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $active;
/**
* The drupal state.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The database storage.
*
* @var \Drupal\Core\Config\DatabaseStorage
*/
protected $storage;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* ExportStorageManager constructor.
*
* @param \Drupal\Core\Config\StorageInterface $active
* The active config storage to prime the export storage.
* @param \Drupal\Core\State\StateInterface $state
* The drupal state.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(StorageInterface $active, StateInterface $state, Connection $connection, EventDispatcherInterface $event_dispatcher) {
$this->active = $active;
$this->state = $state;
$this->eventDispatcher = $event_dispatcher;
// The point of this service is to provide the storage and dispatch the
// event when needed, so the storage itself can not be a service.
$this->storage = new DatabaseStorage($connection, 'config_export');
}
/**
* {@inheritdoc}
*/
public function getStorage() {
if ($this->state->get(self::NEEDS_REBUILD_KEY, TRUE)) {
self::replaceStorageContents($this->active, $this->storage);
// @todo: Use ConfigEvents::STORAGE_TRANSFORM_EXPORT in #2991683
$this->eventDispatcher->dispatch('config.transform.export', new StorageTransformEvent($this->storage));
$this->state->set(self::NEEDS_REBUILD_KEY, FALSE);
}
return new ReadOnlyStorage($this->storage);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = ['onConfigChange', 0];
$events[ConfigEvents::DELETE][] = ['onConfigChange', 0];
$events[ConfigEvents::RENAME][] = ['onConfigChange', 0];
return $events;
}
/**
* Set the flag in state that the export storage is out of date.
*/
public function onConfigChange() {
if (!$this->state->get(self::NEEDS_REBUILD_KEY, FALSE)) {
$this->state->set(self::NEEDS_REBUILD_KEY, TRUE);
}
}
}
<?php
// @codingStandardsIgnoreStart
// @todo: Move this back to \Drupal\Core\Config in #2991683.
// Use this class with its class alias Drupal\Core\Config\ImportStorageTransformer
// @codingStandardsIgnoreEnd
namespace Drupal\config_environment\Core\Config;
use Drupal\Core\Database\Connection;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Config\StorageCopyTrait;
use Drupal\Core\Config\StorageInterface;
/**
* Class ImportStorageTransformer.
*
* @internal
*/
class ImportStorageTransformer {
use StorageCopyTrait;
/**
* The event dispatcher to get changes to the configuration.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The drupal database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* ImportStorageTransformer constructor.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*/
public function __construct(EventDispatcherInterface $event_dispatcher, Connection $connection) {
$this->eventDispatcher = $event_dispatcher;
$this->connection = $connection;
}
/**
* Transform the storage to be imported from.
*
* An import transformation is done before the config importer uses the
* storage to synchronize the configuration. The transformation is also
* done for displaying differences to review imports.
* Importing in this context means the active drupal configuration is changed
* with the ConfigImporter which may or may not be as part of the config
* synchronization.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage to transform for importing from it.
*
* @return \Drupal\Core\Config\StorageInterface
* The transformed storage ready to be imported from.
*/
public function transform(StorageInterface $storage) {
// We use a database storage to reduce the memory requirement.
$mutable = new DatabaseStorage($this->connection, 'config_import');
// Copy the sync configuration to the created mutable storage.
self::replaceStorageContents($storage, $mutable);
// Dispatch the event so that event listeners can alter the configuration.
// @todo: Use ConfigEvents::STORAGE_TRANSFORM_IMPORT in #2991683
$this->eventDispatcher->dispatch('config.transform.import', new StorageTransformEvent($mutable));
// Return the storage with the altered configuration.
return $mutable;
}
}
<?php
// @codingStandardsIgnoreStart
// @todo: Move this back to \Drupal\Core\Config in #2991683.
// Use this class with its class alias Drupal\Core\Config\ManagedStorage
// @codingStandardsIgnoreEnd
namespace Drupal\config_environment\Core\Config;
use Drupal\Core\Config\StorageInterface;
/**
* The managed storage defers all the storage method calls to the manager.
*
* The reason for deferring all the method calls is that the storage interface
* is the API but we potentially need to do an expensive transformation before
* the storage can be used so we can't do it in the constructor but we also
* don't know which method is called first.
*
* @internal
*/
class ManagedStorage implements StorageInterface {
/**
* The decorated storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The storage manager to get the storage to decorate.
*
* @var \Drupal\Core\Config\StorageManagerInterface
*/
protected $manager;
/**
* ManagedStorage constructor.
*
* @param \Drupal\Core\Config\StorageManagerInterface $manager
* The storage manager.
*/
public function __construct(StorageManagerInterface $manager) {
$this->manager = $manager;
}
/**
* {@inheritdoc}
*/
public function exists($name) {
return $this->getStorage()->exists($name);
}
/**
* {@inheritdoc}
*/
public function read($name) {
return $this->getStorage()->read($name);
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
return $this->getStorage()->readMultiple($names);
}
/**
* {@inheritdoc}
*/
public function write($name, array $data) {
return $this->getStorage()->write($name, $data);
}
/**
* {@inheritdoc}
*/
public function delete($name) {
return $this->getStorage()->delete($name);
}
/**
* {@inheritdoc}
*/
public function rename($name, $new_name) {
return $this->getStorage()->rename($name, $new_name);
}
/**
* {@inheritdoc}
*/
public function encode($data) {
return $this->getStorage()->encode($data);
}
/**
* {@inheritdoc}
*/
public function decode($raw) {
return $this->getStorage()->decode($raw);
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
return $this->getStorage()->listAll($prefix);
}
/**
* {@inheritdoc}
*/
public function deleteAll($prefix = '') {
return $this->getStorage()->deleteAll($prefix);
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
// We return the collection directly.
// This means that the collection will not be an instance of ManagedStorage
// But this doesn't matter because the storage is retrieved from the
// manager only the first time it is accessed.
return $this->getStorage()->createCollection($collection);
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
return $this->getStorage()->getAllCollectionNames();
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->getStorage()->getCollectionName();
}
/**
* Get the decorated storage from the manager if necessary.
*
* @return \Drupal\Core\Config\StorageInterface
* The config storage.
*/
protected function getStorage() {
// Get the storage from the manager the first time it is needed.
if (!isset($this->storage)) {
$this->storage = $this->manager->getStorage();
}
return $this->storage;
}
}
<?php
// @codingStandardsIgnoreStart
// @todo: Move this back to \Drupal\Core\Config in #2991683.
// Use this class with its class alias Drupal\Core\Config\StorageManagerInterface
// @codingStandardsIgnoreEnd
namespace Drupal\config_environment\Core\Config;
/**
* Interface for a storage manager.
*
* @internal
*/
interface StorageManagerInterface {
/**
* Get the config storage.
*
* @return \Drupal\Core\Config\StorageInterface
* The config storage.
*/
public function getStorage();
}
<?php
// @codingStandardsIgnoreStart
// @todo: Move this back to \Drupal\Core\Config in #2991683.
// Use this class with its class alias Drupal\Core\Config\StorageTransformEvent
// @codingStandardsIgnoreEnd
namespace Drupal\config_environment\Core\Config;
use Symfony\Component\EventDispatcher\Event;
// @todo: below removed when namespace is \Drupal\Core\Config in 2991683.
use Drupal\Core\Config\StorageInterface;
/**
* Class StorageTransformEvent.
*
* This event allows subscribers to alter the configuration of the storage that
* is being transformed.
*/
class StorageTransformEvent extends Event {
/**
* The configuration storage which is transformed.
*
* This storage can be interacted with by event subscribers and will be
* used instead of the original storage after all event subscribers have been
* called.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* StorageTransformEvent constructor.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage with the configuration to transform.
*/
public function __construct(StorageInterface $storage) {
$this->storage = $storage;
}
/**
* Returns the mutable storage ready to be read from and written to.
*
* @return \Drupal\Core\Config\StorageInterface
* The config storage.
*/
public function getStorage() {
return $this->storage;
}
}
<?php
namespace Drupal\config_environment\Form;
use Drupal\config\Form\ConfigSync as OriginalConfigSync;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Overrides the ConfigSync form.
*/
class ConfigSync extends OriginalConfigSync {
/**
* The import transformer service.
*
* @var \Drupal\Core\Config\ImportStorageTransformer
*/
protected $importTransformer;
/**
* The sync storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $originalSyncStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$form = parent::create($container);
$form->importTransformer = $container->get('config.import_transformer');
$form->originalSyncStorage = $form->syncStorage;
return $form;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$this->syncStorage = $this->importTransformer->transform($this->originalSyncStorage);
return parent::buildForm($form, $form_state);
}
}
# @todo: Move this test module under the config module in #2991683.
name: 'Configuration Storage Transformer Test'
type: module
package: Testing
version: VERSION
core: 8.x
dependencies:
- drupal:config
# @todo: remove dependency on config_environment in #2991683.
- drupal:config_environment
services:
config_transformer_test.event_subscriber:
class: Drupal\config_transformer_test\EventSubscriber
arguments: ['@config.storage', '@config.storage.sync']
tags:
- { name: event_subscriber }
<?php
namespace Drupal\config_transformer_test;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\StorageTransformEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class EventSubscriber.
*
* The transformations here are for testing purposes only and do not constitute