Issue #3036193 by bircher, mpotter, larowlan, alexpott: Add ExportStorage to allow config export in third party tools
parent
84880d95e8
commit
52bd9a1206
|
@ -315,6 +315,9 @@ services:
|
|||
public: false
|
||||
tags:
|
||||
- { name: backend_overridable }
|
||||
config.storage.export:
|
||||
class: Drupal\Core\Config\ReadOnlyStorage
|
||||
arguments: ['@config.storage']
|
||||
# @deprecated in Drupal 8.0.x and will be removed before 9.0.0. Use
|
||||
# config.storage.sync instead.
|
||||
# @see https://www.drupal.org/node/2574957
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Core\Config;
|
||||
|
||||
/**
|
||||
* A ReadOnlyStorage decorates a storage and does not allow writing to it.
|
||||
*/
|
||||
class ReadOnlyStorage implements StorageInterface {
|
||||
|
||||
/**
|
||||
* The config storage that we are decorating.
|
||||
*
|
||||
* @var \Drupal\Core\Config\StorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Create a ReadOnlyStorage decorating another storage.
|
||||
*
|
||||
* @param \Drupal\Core\Config\StorageInterface $storage
|
||||
* The decorated storage.
|
||||
*/
|
||||
public function __construct(StorageInterface $storage) {
|
||||
$this->storage = $storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists($name) {
|
||||
return $this->storage->exists($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read($name) {
|
||||
return $this->storage->read($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readMultiple(array $names) {
|
||||
return $this->storage->readMultiple($names);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function write($name, array $data) {
|
||||
throw new \BadMethodCallException(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete($name) {
|
||||
throw new \BadMethodCallException(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function rename($name, $new_name) {
|
||||
throw new \BadMethodCallException(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encode($data) {
|
||||
return $this->storage->encode($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function decode($raw) {
|
||||
return $this->storage->decode($raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function listAll($prefix = '') {
|
||||
return $this->storage->listAll($prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAll($prefix = '') {
|
||||
throw new \BadMethodCallException(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createCollection($collection) {
|
||||
return new static($this->storage->createCollection($collection));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAllCollectionNames() {
|
||||
return $this->storage->getAllCollectionNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCollectionName() {
|
||||
return $this->storage->getCollectionName();
|
||||
}
|
||||
|
||||
}
|
|
@ -41,6 +41,13 @@ class ConfigController implements ContainerInjectionInterface {
|
|||
*/
|
||||
protected $configManager;
|
||||
|
||||
/**
|
||||
* The export storage.
|
||||
*
|
||||
* @var \Drupal\Core\Config\StorageInterface
|
||||
*/
|
||||
protected $exportStorage;
|
||||
|
||||
/**
|
||||
* The file download controller.
|
||||
*
|
||||
|
@ -72,7 +79,8 @@ class ConfigController implements ContainerInjectionInterface {
|
|||
$container->get('config.manager'),
|
||||
new FileDownloadController(),
|
||||
$container->get('diff.formatter'),
|
||||
$container->get('file_system')
|
||||
$container->get('file_system'),
|
||||
$container->get('config.storage.export')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -91,14 +99,21 @@ class ConfigController implements ContainerInjectionInterface {
|
|||
* The diff formatter.
|
||||
* @param \Drupal\Core\File\FileSystemInterface $file_system
|
||||
* The file system.
|
||||
* @param \Drupal\Core\Config\StorageInterface $export_storage
|
||||
* The export storage.
|
||||
*/
|
||||
public function __construct(StorageInterface $target_storage, StorageInterface $source_storage, ConfigManagerInterface $config_manager, FileDownloadController $file_download_controller, DiffFormatter $diff_formatter, FileSystemInterface $file_system) {
|
||||
public function __construct(StorageInterface $target_storage, StorageInterface $source_storage, ConfigManagerInterface $config_manager, FileDownloadController $file_download_controller, DiffFormatter $diff_formatter, FileSystemInterface $file_system, StorageInterface $export_storage = NULL) {
|
||||
$this->targetStorage = $target_storage;
|
||||
$this->sourceStorage = $source_storage;
|
||||
$this->configManager = $config_manager;
|
||||
$this->fileDownloadController = $file_download_controller;
|
||||
$this->diffFormatter = $diff_formatter;
|
||||
$this->fileSystem = $file_system;
|
||||
if (is_null($export_storage)) {
|
||||
@trigger_error('The config.storage.export service must be passed to ConfigController::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/3037022.', E_USER_DEPRECATED);
|
||||
$export_storage = \Drupal::service('config.storage.export');
|
||||
}
|
||||
$this->exportStorage = $export_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,13 +128,13 @@ class ConfigController implements ContainerInjectionInterface {
|
|||
}
|
||||
|
||||
$archiver = new ArchiveTar(file_directory_temp() . '/config.tar.gz', 'gz');
|
||||
// Get raw configuration data without overrides.
|
||||
foreach ($this->configManager->getConfigFactory()->listAll() as $name) {
|
||||
$archiver->addString("$name.yml", Yaml::encode($this->configManager->getConfigFactory()->get($name)->getRawData()));
|
||||
// Add all contents of the export storage to the archive.
|
||||
foreach ($this->exportStorage->listAll() as $name) {
|
||||
$archiver->addString("$name.yml", Yaml::encode($this->exportStorage->read($name)));
|
||||
}
|
||||
// Get all override data from the remaining collections.
|
||||
foreach ($this->targetStorage->getAllCollectionNames() as $collection) {
|
||||
$collection_storage = $this->targetStorage->createCollection($collection);
|
||||
// Get all data from the remaining collections.
|
||||
foreach ($this->exportStorage->getAllCollectionNames() as $collection) {
|
||||
$collection_storage = $this->exportStorage->createCollection($collection);
|
||||
foreach ($collection_storage->listAll() as $name) {
|
||||
$archiver->addString(str_replace('.', '/', $collection) . "/$name.yml", Yaml::encode($collection_storage->read($name)));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\KernelTests\Core\Config;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests configuration export storage.
|
||||
*
|
||||
* @group config
|
||||
*/
|
||||
class ConfigExportStorageTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['system', 'config_test'];
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installConfig(['system', 'config_test']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests configuration override.
|
||||
*/
|
||||
public function testExportStorage() {
|
||||
/** @var \Drupal\Core\Config\StorageInterface $active */
|
||||
$active = $this->container->get('config.storage');
|
||||
/** @var \Drupal\Core\Config\StorageInterface $export */
|
||||
$export = $this->container->get('config.storage.export');
|
||||
|
||||
// Test that the active and the export storage contain the same config.
|
||||
$this->assertNotEmpty($active->listAll());
|
||||
$this->assertEquals($active->listAll(), $export->listAll());
|
||||
foreach ($active->listAll() as $name) {
|
||||
$this->assertEquals($active->read($name), $export->read($name));
|
||||
}
|
||||
|
||||
// Test that the export storage is read-only.
|
||||
try {
|
||||
$export->deleteAll();
|
||||
$this->fail("export storage must not allow editing");
|
||||
}
|
||||
catch (\BadMethodCallException $exception) {
|
||||
$this->pass("Exception is thrown.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\Core\Config;
|
||||
|
||||
use Drupal\Core\Config\MemoryStorage;
|
||||
use Drupal\Core\Config\ReadOnlyStorage;
|
||||
use Drupal\Core\Config\StorageCopyTrait;
|
||||
use Drupal\Core\Config\StorageInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\Core\Config\ReadOnlyStorage
|
||||
* @group Config
|
||||
*/
|
||||
class ReadOnlyStorageTest extends UnitTestCase {
|
||||
|
||||
use StorageCopyTrait;
|
||||
|
||||
/**
|
||||
* The memory storage containing the data.
|
||||
*
|
||||
* @var \Drupal\Core\Config\MemoryStorage
|
||||
*/
|
||||
protected $memory;
|
||||
|
||||
/**
|
||||
* The read-only storage under test.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ReadOnlyStorage
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
// Set up a memory storage we can manipulate to set fixtures.
|
||||
$this->memory = new MemoryStorage();
|
||||
// Wrap the memory storage in the read-only storage to test it.
|
||||
$this->storage = new ReadOnlyStorage($this->memory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::exists
|
||||
* @covers ::read
|
||||
* @covers ::readMultiple
|
||||
* @covers ::listAll
|
||||
*
|
||||
* @dataProvider readMethodsProvider
|
||||
*/
|
||||
public function testReadOperations($method, $arguments, $fixture) {
|
||||
$this->setRandomFixtureConfig($fixture);
|
||||
|
||||
$expected = call_user_func_array([$this->memory, $method], $arguments);
|
||||
$actual = call_user_func_array([$this->storage, $method], $arguments);
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the methods that work transparently.
|
||||
*
|
||||
* @return array
|
||||
* The data.
|
||||
*/
|
||||
public function readMethodsProvider() {
|
||||
$fixture = [
|
||||
StorageInterface::DEFAULT_COLLECTION => ['config.a', 'config.b', 'other.a'],
|
||||
];
|
||||
|
||||
$data = [];
|
||||
$data[] = ['exists', ['config.a'], $fixture];
|
||||
$data[] = ['exists', ['not.existing'], $fixture];
|
||||
$data[] = ['read', ['config.a'], $fixture];
|
||||
$data[] = ['read', ['not.existing'], $fixture];
|
||||
$data[] = ['readMultiple', [['config.a', 'config.b', 'not']], $fixture];
|
||||
$data[] = ['listAll', [''], $fixture];
|
||||
$data[] = ['listAll', ['config'], $fixture];
|
||||
$data[] = ['listAll', ['none'], $fixture];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::write
|
||||
* @covers ::delete
|
||||
* @covers ::rename
|
||||
* @covers ::deleteAll
|
||||
*
|
||||
* @dataProvider writeMethodsProvider
|
||||
*/
|
||||
public function testWriteOperations($method, $arguments, $fixture) {
|
||||
$this->setRandomFixtureConfig($fixture);
|
||||
|
||||
// Create an independent memory storage as a backup.
|
||||
$backup = new MemoryStorage();
|
||||
static::replaceStorageContents($this->memory, $backup);
|
||||
|
||||
try {
|
||||
call_user_func_array([$this->storage, $method], $arguments);
|
||||
$this->fail("exception not thrown");
|
||||
}
|
||||
catch (\BadMethodCallException $exception) {
|
||||
$this->assertEquals(ReadOnlyStorage::class . '::' . $method . ' is not allowed on a ReadOnlyStorage', $exception->getMessage());
|
||||
}
|
||||
|
||||
// Assert that the memory storage has not been altered.
|
||||
$this->assertTrue($backup == $this->memory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the methods that throw an exception.
|
||||
*
|
||||
* @return array
|
||||
* The data
|
||||
*/
|
||||
public function writeMethodsProvider() {
|
||||
$fixture = [
|
||||
StorageInterface::DEFAULT_COLLECTION => ['config.a', 'config.b'],
|
||||
];
|
||||
|
||||
$data = [];
|
||||
$data[] = ['write', ['config.a', (array) $this->getRandomGenerator()->object()], $fixture];
|
||||
$data[] = ['write', [$this->randomMachineName(), (array) $this->getRandomGenerator()->object()], $fixture];
|
||||
$data[] = ['delete', ['config.a'], $fixture];
|
||||
$data[] = ['delete', [$this->randomMachineName()], $fixture];
|
||||
$data[] = ['rename', ['config.a', 'config.b'], $fixture];
|
||||
$data[] = ['rename', ['config.a', $this->randomMachineName()], $fixture];
|
||||
$data[] = ['rename', [$this->randomMachineName(), $this->randomMachineName()], $fixture];
|
||||
$data[] = ['deleteAll', [''], $fixture];
|
||||
$data[] = ['deleteAll', ['config'], $fixture];
|
||||
$data[] = ['deleteAll', ['other'], $fixture];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getAllCollectionNames
|
||||
* @covers ::getCollectionName
|
||||
* @covers ::createCollection
|
||||
*/
|
||||
public function testCollections() {
|
||||
$fixture = [
|
||||
StorageInterface::DEFAULT_COLLECTION => [$this->randomMachineName()],
|
||||
'A' => [$this->randomMachineName()],
|
||||
'B' => [$this->randomMachineName()],
|
||||
'C' => [$this->randomMachineName()],
|
||||
];
|
||||
$this->setRandomFixtureConfig($fixture);
|
||||
|
||||
$this->assertEquals(['A', 'B', 'C'], $this->storage->getAllCollectionNames());
|
||||
foreach (array_keys($fixture) as $collection) {
|
||||
$storage = $this->storage->createCollection($collection);
|
||||
// Assert that the collection storage is still a read-only storage.
|
||||
$this->assertInstanceOf(ReadOnlyStorage::class, $storage);
|
||||
$this->assertEquals($collection, $storage->getCollectionName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::encode
|
||||
* @covers ::decode
|
||||
*/
|
||||
public function testEncodeDecode() {
|
||||
$array = (array) $this->getRandomGenerator()->object();
|
||||
$string = $this->getRandomGenerator()->string();
|
||||
|
||||
// Assert reversibility of encoding and decoding.
|
||||
$this->assertEquals($array, $this->storage->decode($this->storage->encode($array)));
|
||||
$this->assertEquals($string, $this->storage->encode($this->storage->decode($string)));
|
||||
// Assert same results as the decorated storage.
|
||||
$this->assertEquals($this->memory->encode($array), $this->storage->encode($array));
|
||||
$this->assertEquals($this->memory->decode($string), $this->storage->decode($string));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random config in the memory storage.
|
||||
*
|
||||
* @param array $config
|
||||
* The config keys, keyed by the collection.
|
||||
*/
|
||||
protected function setRandomFixtureConfig($config) {
|
||||
// Erase previous fixture.
|
||||
foreach (array_merge([StorageInterface::DEFAULT_COLLECTION], $this->memory->getAllCollectionNames()) as $collection) {
|
||||
$this->memory->createCollection($collection)->deleteAll();
|
||||
}
|
||||
|
||||
foreach ($config as $collection => $keys) {
|
||||
$storage = $this->memory->createCollection($collection);
|
||||
foreach ($keys as $key) {
|
||||
// Create some random config.
|
||||
$storage->write($key, (array) $this->getRandomGenerator()->object());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue