diff --git a/core/core.services.yml b/core/core.services.yml
index d915e10..adb1675 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -65,6 +65,8 @@ services:
config.storage:
class: Drupal\Core\Config\CachedStorage
arguments: ['@config.cachedstorage.storage', '@cache.config']
+ tags:
+ - { name: persist }
config.context.factory:
class: Drupal\Core\Config\Context\ConfigContextFactory
arguments: ['@event_dispatcher']
diff --git a/core/lib/Drupal/Core/Config/CachedStorage.php b/core/lib/Drupal/Core/Config/CachedStorage.php
index 7d094b4..ec143e8 100644
--- a/core/lib/Drupal/Core/Config/CachedStorage.php
+++ b/core/lib/Drupal/Core/Config/CachedStorage.php
@@ -16,7 +16,7 @@
* the cache and delegates the read to the storage on a cache miss. It also
* handles cache invalidation.
*/
-class CachedStorage implements StorageInterface {
+class CachedStorage implements StorageInterface, StorageCacheInterface {
/**
* The configuration storage to be cached.
@@ -33,6 +33,13 @@ class CachedStorage implements StorageInterface {
protected $cache;
/**
+ * List of listAll() prefixes with their results.
+ *
+ * @var array
+ */
+ protected $findByPrefixCache = array();
+
+ /**
* Constructs a new CachedStorage controller.
*
* @param Drupal\Core\Config\StorageInterface $storage
@@ -80,6 +87,32 @@ public function read($name) {
}
/**
+ * {@inheritdoc}
+ */
+ public function readMultiple(array $names) {
+ $list = array();
+ // The names array is passed by reference and will only contain the names of
+ // config object not found after the method call.
+ // @see Drupal\Core\Cache\CacheBackendInterface::getMultiple()
+ $cached_list = $this->cache->getMultiple($names);
+
+ if (!empty($names)) {
+ $list = $this->storage->readMultiple($names);
+ // Cache configuration objects that were loaded from the storage.
+ foreach ($list as $name => $data) {
+ $this->cache->set($name, $data, CacheBackendInterface::CACHE_PERMANENT);
+ }
+ }
+
+ // Add the configuration objects from the cache to the list.
+ foreach ($cached_list as $name => $cache) {
+ $list[$name] = $cache->data;
+ }
+
+ return $list;
+ }
+
+ /**
* Implements Drupal\Core\Config\StorageInterface::write().
*/
public function write($name, array $data) {
@@ -87,6 +120,8 @@ public function write($name, array $data) {
// While not all written data is read back, setting the cache instead of
// just deleting it avoids cache rebuild stampedes.
$this->cache->set($name, $data, CacheBackendInterface::CACHE_PERMANENT);
+ $this->cache->deleteTags(array($this::FIND_BY_PREFIX_CACHE_TAG => TRUE));
+ $this->findByPrefixCache = array();
return TRUE;
}
return FALSE;
@@ -100,6 +135,8 @@ public function delete($name) {
// rebuilding the cache before the storage is gone.
if ($this->storage->delete($name)) {
$this->cache->delete($name);
+ $this->cache->deleteTags(array($this::FIND_BY_PREFIX_CACHE_TAG => TRUE));
+ $this->findByPrefixCache = array();
return TRUE;
}
return FALSE;
@@ -114,6 +151,8 @@ public function rename($name, $new_name) {
if ($this->storage->rename($name, $new_name)) {
$this->cache->delete($name);
$this->cache->delete($new_name);
+ $this->cache->deleteTags(array($this::FIND_BY_PREFIX_CACHE_TAG => TRUE));
+ $this->findByPrefixCache = array();
return TRUE;
}
return FALSE;
@@ -134,12 +173,50 @@ public function decode($raw) {
}
/**
- * Implements Drupal\Core\Config\StorageInterface::listAll().
- *
- * Not supported by CacheBackendInterface.
+ * {@inheritdoc}
*/
public function listAll($prefix = '') {
- return $this->storage->listAll($prefix);
+ // Do not cache when a prefix is not provided.
+ if ($prefix) {
+ return $this->findByPrefix($prefix);
+ }
+ return $this->storage->listAll();
+ }
+
+ /**
+ * Finds configuration object names starting with a given prefix.
+ *
+ * Given the following configuration objects:
+ * - node.type.article
+ * - node.type.page
+ *
+ * Passing the prefix 'node.type.' will return an array containing the above
+ * names.
+ *
+ * @param string $prefix
+ * The prefix to search for
+ *
+ * @return array
+ * An array containing matching configuration object names.
+ */
+ protected function findByPrefix($prefix) {
+ if (!isset($this->findByPrefixCache[$prefix])) {
+ // The : character is not allowed in config file names, so this can not
+ // conflict.
+ if ($cache = $this->cache->get('find:' . $prefix)) {
+ $this->findByPrefixCache[$prefix] = $cache->data;
+ }
+ else {
+ $this->findByPrefixCache[$prefix] = $this->storage->listAll($prefix);
+ $this->cache->set(
+ 'find:' . $prefix,
+ $this->findByPrefixCache[$prefix],
+ CacheBackendInterface::CACHE_PERMANENT,
+ array($this::FIND_BY_PREFIX_CACHE_TAG => TRUE)
+ );
+ }
+ }
+ return $this->findByPrefixCache[$prefix];
}
/**
@@ -155,4 +232,11 @@ public function deleteAll($prefix = '') {
}
return FALSE;
}
+
+ /**
+ * Clears the static list cache.
+ */
+ public function resetListCache() {
+ $this->findByPrefixCache = array();
+ }
}
diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php
index 740f316..02afbd1 100644
--- a/core/lib/Drupal/Core/Config/Config.php
+++ b/core/lib/Drupal/Core/Config/Config.php
@@ -108,6 +108,25 @@ public function init() {
}
/**
+ * Initializes a configuration object with pre-loaded data.
+ *
+ * @param array $data
+ * Array of loaded data for this configuration object.
+ *
+ * @return Drupal\Core\Config\Config
+ * The configuration object.
+ */
+ public function initWithData(array $data) {
+ $this->isLoaded = TRUE;
+ $this->overrides = array();
+ $this->isNew = FALSE;
+ $this->notify('init');
+ $this->replaceData($data);
+ $this->notify('load');
+ return $this;
+ }
+
+ /**
* Returns the name of this configuration object.
*
* @return string
diff --git a/core/lib/Drupal/Core/Config/ConfigFactory.php b/core/lib/Drupal/Core/Config/ConfigFactory.php
index dd5df1e..fe9f4cb 100644
--- a/core/lib/Drupal/Core/Config/ConfigFactory.php
+++ b/core/lib/Drupal/Core/Config/ConfigFactory.php
@@ -85,6 +85,45 @@ public function get($name) {
}
/**
+ * Returns a list of configuration objects for a given names and context.
+ *
+ * This will pre-load all requested configuration objects does not create
+ * new configuration objects.
+ *
+ * @param array $names
+ * List of names of configuration objects.
+ *
+ * @return array
+ * List of successfully loaded configuration objects, keyed by name.
+ */
+ public function loadMultiple(array $names) {
+ $context = $this->getContext();
+
+ $list = array();
+ foreach ($names as $key => $name) {
+ $cache_key = $this->getCacheKey($name, $context);
+ // @todo: Deleted configuration stays in $this->cache, only return
+ // config entities that are not new.
+ if (isset($this->cache[$cache_key]) && !$this->cache[$cache_key]->isNew()) {
+ $list[$name] = $this->cache[$cache_key];
+ unset($names[$key]);
+ }
+ }
+
+ // Pre-load remaining configuration files.
+ if (!empty($names)) {
+ $storage_data = $this->storage->readMultiple($names);
+ foreach ($storage_data as $name => $data) {
+ $cache_key = $this->getCacheKey($name, $context);
+ $this->cache[$cache_key] = new Config($name, $this->storage, $context);
+ $this->cache[$cache_key]->initWithData($data);
+ $list[$name] = $this->cache[$cache_key];
+ }
+ }
+ return $list;
+ }
+
+ /**
* Resets and re-initializes configuration objects. Internal use only.
*
* @param string $name
@@ -104,6 +143,11 @@ public function reset($name = NULL) {
else {
$this->cache = array();
}
+
+ // Clear the static list cache if supported by the storage.
+ if ($this->storage instanceof StorageCacheInterface) {
+ $this->storage->resetListCache();
+ }
return $this;
}
diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php
index cc595a6..78e1629 100644
--- a/core/lib/Drupal/Core/Config/DatabaseStorage.php
+++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php
@@ -86,6 +86,26 @@ public function read($name) {
}
/**
+ * {@inheritdoc}
+ */
+ public function readMultiple(array $names) {
+ // There are situations, like in the installer, where we may attempt a
+ // read without actually having the database available. In this case,
+ // catch the exception and just return an empty array so the caller can
+ // handle it if need be.
+ $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();
+ foreach ($list as &$data) {
+ $data = $this->decode($data);
+ }
+ }
+ catch (Exception $e) {
+ }
+ return $list;
+ }
+
+ /**
* Implements Drupal\Core\Config\StorageInterface::write().
*
* @throws PDOException
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
index f4ef9a8..7fe3c49 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php
@@ -254,27 +254,24 @@ protected function buildQuery($ids, $revision_id = FALSE) {
$config_class = $this->entityInfo['class'];
$prefix = $this->getConfigPrefix();
- // Load all of the configuration entities.
+ // Get the names of the configuration entities we are going to load.
if ($ids === NULL) {
$names = $this->configStorage->listAll($prefix);
- $result = array();
- foreach ($names as $name) {
- $config = $this->configFactory->get($name);
- $result[$config->get($this->idKey)] = new $config_class($config->get(), $this->entityType);
- }
- return $result;
}
else {
- $result = array();
+ $names = array();
foreach ($ids as $id) {
// Add the prefix to the ID to serve as the configuration object name.
- $config = $this->configFactory->get($prefix . $id);
- if (!$config->isNew()) {
- $result[$id] = new $config_class($config->get(), $this->entityType);
- }
+ $names[] = $prefix . $id;
}
- return $result;
}
+
+ // Load all of the configuration entities.
+ $result = array();
+ foreach ($this->configFactory->loadMultiple($names) as $config) {
+ $result[$config->get($this->idKey)] = new $config_class($config->get(), $this->entityType);
+ }
+ return $result;
}
/**
diff --git a/core/lib/Drupal/Core/Config/Entity/Query/Query.php b/core/lib/Drupal/Core/Config/Entity/Query/Query.php
index 8577c08..c6320f3 100644
--- a/core/lib/Drupal/Core/Config/Entity/Query/Query.php
+++ b/core/lib/Drupal/Core/Config/Entity/Query/Query.php
@@ -83,8 +83,8 @@ public function condition($property, $value = NULL, $operator = NULL, $langcode
public function execute() {
// Load all config files.
$entity_info = $this->entityManager->getDefinition($this->getEntityType());
- $prefix = $entity_info['config_prefix'];
- $prefix_length = strlen($prefix) + 1;
+ $prefix = $entity_info['config_prefix'] . '.';
+ $prefix_length = strlen($prefix);
$names = $this->configStorage->listAll($prefix);
$configs = array();
foreach ($names as $name) {
diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php
index 6422e9d..6e428b3 100644
--- a/core/lib/Drupal/Core/Config/FileStorage.php
+++ b/core/lib/Drupal/Core/Config/FileStorage.php
@@ -90,6 +90,19 @@ public function read($name) {
}
/**
+ * {@inheritdoc}
+ */
+ public function readMultiple(array $names) {
+ $list = array();
+ foreach ($names as $name) {
+ if ($data = $this->read($name)) {
+ $list[$name] = $data;
+ }
+ }
+ return $list;
+ }
+
+ /**
* Implements Drupal\Core\Config\StorageInterface::write().
*
* @throws Symfony\Component\Yaml\Exception\DumpException
diff --git a/core/lib/Drupal/Core/Config/NullStorage.php b/core/lib/Drupal/Core/Config/NullStorage.php
index 336c111..2bf121d 100644
--- a/core/lib/Drupal/Core/Config/NullStorage.php
+++ b/core/lib/Drupal/Core/Config/NullStorage.php
@@ -38,6 +38,13 @@ public function read($name) {
}
/**
+ * Implements Drupal\Core\Config\StorageInterface::readMultiple().
+ */
+ public function readMultiple(array $names) {
+ return array();
+ }
+
+ /**
* Implements Drupal\Core\Config\StorageInterface::write().
*/
public function write($name, array $data) {
diff --git a/core/lib/Drupal/Core/Config/StorageCacheInterface.php b/core/lib/Drupal/Core/Config/StorageCacheInterface.php
new file mode 100644
index 0000000..8d864bb
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/StorageCacheInterface.php
@@ -0,0 +1,28 @@
+get();
$fields[$field['uuid']] = $field;
}
diff --git a/core/modules/field/lib/Drupal/field/FieldInfo.php b/core/modules/field/lib/Drupal/field/FieldInfo.php
index 349872e..e7e46b9 100644
--- a/core/modules/field/lib/Drupal/field/FieldInfo.php
+++ b/core/modules/field/lib/Drupal/field/FieldInfo.php
@@ -178,14 +178,14 @@ public function getFieldMap() {
$map = array();
// Get active fields.
- foreach (config_get_storage_names_with_prefix('field.field') as $config_id) {
+ foreach (config_get_storage_names_with_prefix('field.field.') as $config_id) {
$field_config = $this->config->get($config_id)->get();
if ($field_config['active'] && $field_config['storage']['active']) {
$fields[$field_config['uuid']] = $field_config;
}
}
// Get field instances.
- foreach (config_get_storage_names_with_prefix('field.instance') as $config_id) {
+ foreach (config_get_storage_names_with_prefix('field.instance.') as $config_id) {
$instance_config = $this->config->get($config_id)->get();
$field_uuid = $instance_config['field_uuid'];
// Filter out instances of inactive fields, and instances on unknown
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 8e295b3..59d05dd 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -934,6 +934,8 @@ protected function refreshVariables() {
global $conf;
cache('bootstrap')->delete('variables');
$conf = variable_initialize();
+ // Clear the tag cache.
+ drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache');
drupal_container()->get('config.factory')->reset();
}
diff --git a/core/modules/user/tests/Drupal/user/Tests/Views/Argument/RolesRidTest.php b/core/modules/user/tests/Drupal/user/Tests/Views/Argument/RolesRidTest.php
index 3cee6d7..c5cd817 100644
--- a/core/modules/user/tests/Drupal/user/Tests/Views/Argument/RolesRidTest.php
+++ b/core/modules/user/tests/Drupal/user/Tests/Views/Argument/RolesRidTest.php
@@ -10,6 +10,7 @@
use Drupal\Component\Utility\String;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Tests\UnitTestCase;
+use Drupal\user\Plugin\Core\Entity\Role;
use Drupal\user\Plugin\views\argument\RolesRid;
/**
@@ -47,28 +48,24 @@ public static function getInfo() {
* @see \Drupal\user\Plugin\views\argument\RolesRid::title_query()
*/
public function testTitleQuery() {
- $config = array(
- 'user.role.test_rid_1' => array(
- 'id' => 'test_rid_1',
- 'label' => 'test rid 1'
- ),
- 'user.role.test_rid_2' => array(
- 'id' => 'test_rid_2',
- 'label' => 'test rid 2',
- ),
- );
- $config_factory = $this->getConfigFactoryStub($config);
- $config_storage = $this->getConfigStorageStub($config);
-
- $entity_query_factory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory')
- ->disableOriginalConstructor()
- ->getMock();
-
- // Creates a stub role storage controller and replace the attachLoad()
- // method with an empty version, because attachLoad() calls
- // module_implements().
- $role_storage_controller = $this->getMock('Drupal\user\RoleStorageController', array('attachLoad'), array('user_role', static::$entityInfo, $config_factory, $config_storage, $entity_query_factory));
-
+ $role1 = new Role(array(
+ 'id' => 'test_rid_1',
+ 'label' => 'test rid 1'
+ ), 'user_role');
+ $role2 = new Role(array(
+ 'id' => 'test_rid_2',
+ 'label' => 'test rid 2',
+ ), 'user_role');
+
+ // Creates a stub entity storage controller;
+ $role_storage_controller = $this->getMockForAbstractClass('Drupal\Core\Entity\EntityStorageControllerInterface');
+ $role_storage_controller->expects($this->any())
+ ->method('loadMultiple')
+ ->will($this->returnValueMap(array(
+ array(array(), array()),
+ array(array('test_rid_1'), array('test_rid_1' => $role1)),
+ array(array('test_rid_1', 'test_rid_2'), array('test_rid_1' => $role1, 'test_rid_2' => $role2)),
+ )));
$entity_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityManager')
->disableOriginalConstructor()
@@ -92,7 +89,7 @@ public function testTitleQuery() {
$container->set('plugin.manager.entity', $entity_manager);
\Drupal::setContainer($container);
- $roles_rid_argument = new RolesRid($config, 'users_roles_rid', array(), $entity_manager);
+ $roles_rid_argument = new RolesRid(array(), 'users_roles_rid', array(), $entity_manager);
$roles_rid_argument->value = array();
$titles = $roles_rid_argument->title_query();
diff --git a/core/tests/Drupal/Tests/Core/Config/CachedStorageTest.php b/core/tests/Drupal/Tests/Core/Config/CachedStorageTest.php
new file mode 100644
index 0000000..e9e32f1
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Config/CachedStorageTest.php
@@ -0,0 +1,118 @@
+ 'Config cached storage test',
+ 'description' => 'Tests the interaction of cache and file storage in CachedStorage.',
+ 'group' => 'Configuration'
+ );
+ }
+
+ /**
+ * Test listAll static cache.
+ */
+ public function testListAllStaticCache() {
+ $prefix = __FUNCTION__;
+ $storage = $this->getMock('Drupal\Core\Config\StorageInterface');
+
+ $response = array("$prefix." . $this->randomName(), "$prefix." . $this->randomName());
+ $storage->expects($this->once())
+ ->method('listAll')
+ ->with($prefix)
+ ->will($this->returnValue($response));
+
+ $cache = new NullBackend(__FUNCTION__);
+ $cachedStorage = new CachedStorage($storage, $cache);
+ $this->assertEquals($response, $cachedStorage->listAll($prefix));
+ $this->assertEquals($response, $cachedStorage->listAll($prefix));
+ }
+
+ /**
+ * Test CachedStorage::listAll() persistent cache.
+ */
+ public function testListAllPrimedPersistentCache() {
+ $prefix = __FUNCTION__;
+ $storage = $this->getMock('Drupal\Core\Config\StorageInterface');
+ $storage->expects($this->never())->method('listAll');
+
+ $response = array("$prefix." . $this->randomName(), "$prefix." . $this->randomName());
+ $cache = new MemoryBackend(__FUNCTION__);
+ $cache->set('find:' . $prefix, $response);
+ $cachedStorage = new CachedStorage($storage, $cache);
+ $this->assertEquals($response, $cachedStorage->listAll($prefix));
+ }
+
+ /**
+ * Test that we don't fall through to file storage with a primed cache.
+ */
+ public function testGetMultipleOnPrimedCache() {
+ $configNames = array(
+ 'foo.bar',
+ 'baz.back',
+ );
+ $configCacheValues = array(
+ 'foo.bar' => (object) array(
+ 'data' => array('foo' => 'bar'),
+ ),
+ 'baz.back' => (object) array(
+ 'data' => array('foo' => 'bar'),
+ ),
+ );
+ $storage = $this->getMock('Drupal\Core\Config\StorageInterface');
+ $storage->expects($this->never())->method('readMultiple');
+ $cache = new MemoryBackend(__FUNCTION__);
+ foreach ($configCacheValues as $key => $value) {
+ $cache->set($key, $value);
+ }
+ $cachedStorage = new CachedStorage($storage, $cache);
+ $this->assertEquals($configCacheValues, $cachedStorage->readMultiple($configNames));
+ }
+
+ /**
+ * Test fall through to file storage on a cache miss.
+ */
+ public function testGetMultipleOnPartiallyPrimedCache() {
+ $configNames = array(
+ 'foo.bar',
+ 'baz.back',
+ $this->randomName() . '. ' . $this->randomName(),
+ );
+ $configCacheValues = array(
+ 'foo.bar' => (object) array(
+ 'data' => array('foo' => 'bar'),
+ ),
+ 'baz.back' => (object) array(
+ 'data' => array('foo' => 'bar'),
+ ),
+ );
+ $cache = new MemoryBackend(__FUNCTION__);
+ foreach ($configCacheValues as $key => $value) {
+ $cache->set($key, $value);
+ }
+
+ $response = array($configNames[2] => array($this->randomName()));
+ $storage = $this->getMock('Drupal\Core\Config\StorageInterface');
+ $storage->expects($this->once())
+ ->method('readMultiple')
+ ->with(array(2 => $configNames[2]))
+ ->will($this->returnValue($response));
+
+ $cachedStorage = new CachedStorage($storage, $cache);
+ $this->assertEquals($configCacheValues + $response, $cachedStorage->readMultiple($configNames));
+ }
+}