diff --git a/core/lib/Drupal/Core/Config/CachedStorage.php b/core/lib/Drupal/Core/Config/CachedStorage.php index a5d11fd..9fcc12d 100644 --- a/core/lib/Drupal/Core/Config/CachedStorage.php +++ b/core/lib/Drupal/Core/Config/CachedStorage.php @@ -238,4 +238,22 @@ public function deleteAll($prefix = '') { public function resetListCache() { $this->findByPrefixCache = array(); } + + /** + * {@inheritdoc} + */ + public function setCollection($collection) { + // @todo Hmmm how to change the bin name? I guess we need to inject the + // cache_factory instead of the cache object so that we can create bins on + // the fly. + $this->storage->setCollection($collection); + } + + /** + * {@inheritdoc} + */ + public function getCollections() { + return $this->storage->getCollections(); + } + } diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index 11ab691..5edb660 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -38,6 +38,13 @@ class DatabaseStorage implements StorageInterface { protected $options = array(); /** + * The storage collection. + * + * @var string + */ + protected $collection = ''; + + /** * Constructs a new DatabaseStorage. * * @param \Drupal\Core\Database\Connection $connection @@ -47,10 +54,11 @@ class DatabaseStorage implements StorageInterface { * @param array $options * (optional) Any additional database connection options to use in queries. */ - public function __construct(Connection $connection, $table, array $options = array()) { + public function __construct(Connection $connection, $table, array $options = array(), $collection = '') { $this->connection = $connection; $this->table = $table; $this->options = $options; + $this->collection = $collection; } /** @@ -75,7 +83,7 @@ public function exists($name) { public function read($name) { $data = FALSE; try { - $raw = $this->connection->query('SELECT data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', array(':name' => $name), $this->options)->fetchField(); + $raw = $this->connection->query('SELECT data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name = :name', array(':collection' => $this->collection, ':name' => $name), $this->options)->fetchField(); if ($raw !== FALSE) { $data = $this->decode($raw); } @@ -93,7 +101,7 @@ public function read($name) { public function readMultiple(array $names) { $list = array(); try { - $list = $this->connection->query('SELECT name, data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name IN (:names)', array(':names' => $names), $this->options)->fetchAllKeyed(); + $list = $this->connection->query('SELECT name, data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name IN (:names)', array(':collection' => $this->collection, ':names' => $names), $this->options)->fetchAllKeyed(); foreach ($list as &$data) { $data = $this->decode($data); } @@ -136,7 +144,7 @@ public function write($name, array $data) { protected function doWrite($name, $data) { $options = array('return' => Database::RETURN_AFFECTED) + $this->options; return (bool) $this->connection->merge($this->table, $options) - ->key('name', $name) + ->keys(array('collection', 'name'), array($this->collection, $name)) ->fields(array('data' => $data)) ->execute(); } @@ -176,8 +184,15 @@ protected static function schemaDefinition() { $schema = array( 'description' => 'The base table for configuration data.', 'fields' => array( + 'collection' => array( + 'description' => 'Primary Key: Config object collection.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), 'name' => array( - 'description' => 'Primary Key: Unique config object name.', + 'description' => 'Primary Key: Config object name.', 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, @@ -190,7 +205,7 @@ protected static function schemaDefinition() { 'size' => 'big', ), ), - 'primary key' => array('name'), + 'primary key' => array('collection', 'name'), ); return $schema; } @@ -205,6 +220,7 @@ protected static function schemaDefinition() { public function delete($name) { $options = array('return' => Database::RETURN_AFFECTED) + $this->options; return (bool) $this->connection->delete($this->table, $options) + ->condition('collection', $this->collection) ->condition('name', $name) ->execute(); } @@ -220,6 +236,7 @@ public function rename($name, $new_name) { return (bool) $this->connection->update($this->table, $options) ->fields(array('name' => $new_name)) ->condition('name', $name) + ->condition('collection', $this->collection) ->execute(); } @@ -246,7 +263,8 @@ public function decode($raw) { */ public function listAll($prefix = '') { try { - return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name LIKE :name', array( + return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name LIKE :name', array( + ':collection' => $this->collection, ':name' => $this->connection->escapeLike($prefix) . '%', ), $this->options)->fetchCol(); } @@ -263,10 +281,27 @@ public function deleteAll($prefix = '') { $options = array('return' => Database::RETURN_AFFECTED) + $this->options; return (bool) $this->connection->delete($this->table, $options) ->condition('name', $prefix . '%', 'LIKE') + ->condition('collection', $this->collection) ->execute(); } catch (\Exception $e) { return FALSE; } } + + /** + * {@inheritdoc} + */ + public function setCollection($collection) { + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function getCollections() { + return $this->connection->query('SELECT DISTINCT collection FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection <> \'\' ORDER by collection')->fetchCol(); + } + + } diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index 1dac0f4..88ac1e3 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -17,6 +17,13 @@ class FileStorage implements StorageInterface { /** + * The storage collection. + * + * @var string + */ + protected $collection; + + /** * The filesystem path for configuration objects. * * @var string @@ -29,8 +36,9 @@ class FileStorage implements StorageInterface { * @param string $directory * A directory path to use for reading and writing of configuration files. */ - public function __construct($directory) { + public function __construct($directory, $collection = NULL) { $this->directory = $directory; + $this->collection = $this->setCollection($collection); } /** @@ -40,7 +48,7 @@ public function __construct($directory) { * The path to the configuration file. */ public function getFilePath($name) { - return $this->directory . '/' . $name . '.' . static::getFileExtension(); + return $this->getCollectionDirectory() . '/' . $name . '.' . static::getFileExtension(); } /** @@ -57,10 +65,14 @@ public static function getFileExtension() { * Check if the directory exists and create it if not. */ protected function ensureStorage() { - $success = file_prepare_directory($this->directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - $success = $success && file_save_htaccess($this->directory, TRUE, TRUE); + $dir = $this->getCollectionDirectory(); + $success = file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + // Only create .htaccess file in root driectory. + if ($dir == $this->directory) { + $success = $success && file_save_htaccess($this->directory, TRUE, TRUE); + } if (!$success) { - throw new StorageException("Failed to create config directory {$this->directory}"); + throw new StorageException("Failed to create config directory {$dir}"); } return $this; } @@ -142,12 +154,22 @@ public function write($name, array $data) { */ public function delete($name) { if (!$this->exists($name)) { - if (!file_exists($this->directory)) { - throw new StorageException($this->directory . '/ not found.'); + $dir = $this->getCollectionDirectory(); + if (!file_exists($dir)) { + throw new StorageException($dir . '/ not found.'); } return FALSE; } - return drupal_unlink($this->getFilePath($name)); + $success = drupal_unlink($this->getFilePath($name)); + + // If a collection is now empty remove the directory. + if ($success && !empty($this->collection)) { + $names = $this->listAll(); + if (empty($names)) { + drupal_rmdir($this->getCollectionDirectory()); + } + } + return $success; } /** @@ -186,12 +208,13 @@ public function decode($raw) { public function listAll($prefix = '') { // glob() silently ignores the error of a non-existing search directory, // even with the GLOB_ERR flag. - if (!file_exists($this->directory)) { + $dir = $this->getCollectionDirectory(); + if (!file_exists($dir)) { return array(); } $extension = '.' . static::getFileExtension(); // \GlobIterator on Windows requires an absolute path. - $files = new \GlobIterator(realpath($this->directory) . '/' . $prefix . '*' . $extension); + $files = new \GlobIterator(realpath($dir) . '/' . $prefix . '*' . $extension); $names = array(); foreach ($files as $file) { @@ -215,4 +238,54 @@ public function deleteAll($prefix = '') { return $success; } + + /** + * {@inheritdoc} + */ + public function setCollection($collection) { + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function getCollections($directory = NULL) { + $collections = array(); + if (!isset($directory)) { + $directory = $this->directory; + } + foreach (new \DirectoryIterator($directory) as $fileinfo) { + if ($fileinfo->isDir() && !$fileinfo->isDot()) { + $collection = $fileinfo->getFilename(); + $sub_collections = $this->getCollections($directory . '/' . $collection); + if (empty($sub_collections)) { + $collections[] = $collection; + } + else { + foreach ($sub_collections as $sub_collection) { + $collections[] = $collection . '.' . $sub_collection; + } + } + } + } + sort($collections); + return $collections; + } + + /** + * Gets the directory for the collection. + * + * @return string + * The directory for the collection. + */ + protected function getCollectionDirectory() { + if (isset($this->collection)) { + $dir = $this->directory . '/' . str_replace('.', '/', $this->collection); + } + else { + $dir = $this->directory; + } + return $dir; + } + } diff --git a/core/lib/Drupal/Core/Config/NullStorage.php b/core/lib/Drupal/Core/Config/NullStorage.php index c66f718..7914bd9 100644 --- a/core/lib/Drupal/Core/Config/NullStorage.php +++ b/core/lib/Drupal/Core/Config/NullStorage.php @@ -92,4 +92,19 @@ public function listAll($prefix = '') { public function deleteAll($prefix = '') { return FALSE; } + + /** + * {@inheritdoc} + */ + public function setCollection($collection) { + // No op. + } + + /** + * {@inheritdoc} + */ + public function getCollections() { + return array(); + } + } diff --git a/core/lib/Drupal/Core/Config/StorageInterface.php b/core/lib/Drupal/Core/Config/StorageInterface.php index 8b99784..f5c0348 100644 --- a/core/lib/Drupal/Core/Config/StorageInterface.php +++ b/core/lib/Drupal/Core/Config/StorageInterface.php @@ -156,4 +156,25 @@ public function listAll($prefix = ''); */ public function deleteAll($prefix = ''); + /** + * Sets the collection on the storage. + * + * @param string $collection + * The collection key. If not set then this is normal configuration + * storage. However if set the storage needs to use it to differentiate the + * collections. collections can be nested for example 'language.de'. If this + * is a file system then we could create a language folder with a subfolder + * named de. If this is a database table then there could be a collection + * column which will store the value 'language.de'. + */ + public function setCollection($collection); + + /** + * Gets the existing collections. + * + * @return array + * An array of existing collections. + */ + public function getCollections(); + } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php index ac77a58..21ee6c5 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php @@ -42,7 +42,6 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('system', 'config_snapshot'); $this->installSchema('node', 'node'); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameValidationTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameValidationTest.php index a538c12..f2ca9aa 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameValidationTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameValidationTest.php @@ -51,7 +51,6 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('system', 'config_snapshot'); $this->installSchema('node', 'node'); // Set up the ConfigImporter object for testing. diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php index 341d3d4..b3c1633 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -43,8 +43,6 @@ public static function getInfo() { function setUp() { parent::setUp(); - $this->installSchema('system', 'config_snapshot'); - $this->installConfig(array('config_test')); // Installing config_test's default configuration pollutes the global // variable being used for recording hook invocations by this test already, diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php index 7c6c1cb..7359a35 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php @@ -31,7 +31,6 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('system', 'config_snapshot'); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php index 295ff74..a0b3ecc 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php @@ -32,7 +32,6 @@ public static function getInfo() { public function setUp() { parent::setUp(); - $this->installSchema('system', 'config_snapshot'); // Update the config snapshot. This allows the parent::setUp() to write // configuration files. \Drupal::service('config.manager')->createSnapshot(\Drupal::service('config.storage'), \Drupal::service('config.storage.snapshot')); diff --git a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php index 925ecbd..1923cce 100644 --- a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php +++ b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php @@ -24,6 +24,11 @@ abstract class ConfigStorageTestBase extends DrupalUnitTestBase { /** + * @var \Drupal\Core\Config\StorageInterface; + */ + protected $storage; + + /** * Tests storage CRUD operations. * * @todo Coverage: Trigger PDOExceptions / Database exceptions. @@ -180,4 +185,57 @@ function testDataTypes() { abstract protected function update($name, $data); abstract protected function delete($name); + + public function testCollection() { + $name = 'config_test.storage'; + $data = array('foo => bar'); + $result = $this->storage->write($name, $data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($data, $this->storage->read($name)); + + // Create configuration in a new collection. + $this->storage->setCollection('collection.new'); + $this->storage->write($name, $data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($data, $this->storage->read($name)); + $new_data = array('foo' => 'baz'); + $this->storage->write($name, $new_data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($new_data, $this->storage->read($name)); + + // Create configuration in another collection. + $this->storage->setCollection('collection.another'); + $this->storage->write($name, $new_data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($new_data, $this->storage->read($name)); + + // Create configuration in yet another collection. + $this->storage->setCollection('alternate'); + $this->storage->write($name, $new_data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($new_data, $this->storage->read($name)); + + // Switch back to the collection-less mode and check the data still exists + // add has not been touched. + $this->storage->setCollection(''); + $this->assertIdentical($data, $this->storage->read($name)); + + // Check that the getCollections() method works. + $this->assertIdentical(array('alternate', 'collection.another', 'collection.new'), $this->storage->getCollections()); + + // Check that the collections are removed when they are empty. + $this->storage->setCollection('alternate'); + $this->storage->delete($name); + $this->assertIdentical(array('collection.another', 'collection.new'), $this->storage->getCollections()); + + // Check that the having an empty collection-less storage does not break + // anything. + $this->storage->setCollection(''); + // Before deleting check that the previous delete did not affect data in + // another collection. + $this->assertIdentical($data, $this->storage->read($name)); + $this->storage->delete($name); + $this->assertIdentical(array('collection.another', 'collection.new'), $this->storage->getCollections()); + } + } diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php index 5f77d92..5717a23 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php @@ -37,7 +37,7 @@ function setUp() { parent::setUp(); $this->installSchema('entity_test', 'entity_test'); - $this->installSchema('system', array('sequences', 'config_snapshot')); + $this->installSchema('system', array('sequences')); $this->installSchema('user', array('users', 'users_roles')); // Set default storage backend and configure the theme system. diff --git a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportChangeTest.php b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportChangeTest.php index bdd2b92..f848870 100644 --- a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportChangeTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportChangeTest.php @@ -26,9 +26,6 @@ class NodeImportChangeTest extends DrupalUnitTestBase { */ public function setUp() { parent::setUp(); - - $this->installSchema('system', array('config_snapshot')); - // Set default storage backend. $this->installConfig(array('field', 'node_test_config')); } diff --git a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php index 1d59ca1..cbc2920 100644 --- a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php @@ -26,8 +26,6 @@ class NodeImportCreateTest extends DrupalUnitTestBase { */ public function setUp() { parent::setUp(); - - $this->installSchema('system', array('config_snapshot')); $this->installSchema('user', array('users')); // Set default storage backend. diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 820efa7..a14cd7b 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -956,25 +956,5 @@ function system_schema() { ), ); - $schema['config_snapshot'] = array( - 'description' => 'Stores a snapshot of the last imported configuration.', - 'fields' => array( - 'name' => array( - 'description' => 'The identifier for the config object (the name of the file, minus the file extension).', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'data' => array( - 'description' => 'The raw data for this configuration object.', - 'type' => 'blob', - 'not null' => TRUE, - 'size' => 'big', - ), - ), - 'primary key' => array('name'), - ); - return $schema; }