diff --git a/core/core.services.yml b/core/core.services.yml index 9a36139..3e0a206 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -58,6 +58,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..f98542f 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 static $listAllCache = array(); + + /** * Constructs a new CachedStorage controller. * * @param Drupal\Core\Config\StorageInterface $storage @@ -80,6 +87,32 @@ public function read($name) { } /** + * Implements Drupal\Core\Config\StorageInterface::readMultiple(). + */ + public function readMultiple(array $names) { + $remaining_names = $names; + $list = array(); + $cached_list = $this->cache->getMultiple($remaining_names); + + // The cache backend removed names that were successfully loaded from the + // cache. + if (!empty($remaining_names)) { + $list = $this->storage->readMultiple($remaining_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('listAll' => TRUE)); + static::$listAllCache = 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('listAll' => TRUE)); + static::$listAllCache = 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('listAll' => TRUE)); + static::$listAllCache = array(); return TRUE; } return FALSE; @@ -135,11 +174,24 @@ public function decode($raw) { /** * Implements Drupal\Core\Config\StorageInterface::listAll(). - * - * Not supported by CacheBackendInterface. */ public function listAll($prefix = '') { - return $this->storage->listAll($prefix); + // Check the static cache first. + if (!isset(static::$listAllCache[$prefix])) { + + // The : character is not allowed in config file names, so this can not + // conflict. + // @todo: Maintain a single cache entry for this, similar to a cache + // collector? + if ($cache = $this->cache->get('list:' . $prefix)) { + static::$listAllCache[$prefix] = $cache->data; + } + else { + static::$listAllCache[$prefix] = $this->storage->listAll($prefix); + $this->cache->set('list:' . $prefix, static::$listAllCache[$prefix], CacheBackendInterface::CACHE_PERMANENT, array('listAll' => TRUE)); + } + } + return static::$listAllCache[$prefix]; } /** @@ -155,4 +207,11 @@ public function deleteAll($prefix = '') { } return FALSE; } + + /** + * Clears the static list cache. + */ + public function resetListCache() { + static::$listAllCache = 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..796bba1 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -86,6 +86,29 @@ public function read($name) { } /** + * Implements Drupal\Core\Config\StorageInterface::read(). + * + * + * @param array $names + */ + 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 095114a..34b49d6 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -232,24 +232,20 @@ protected function buildQuery($ids, $revision_id = FALSE) { // Load all of the configuration entities. if ($ids === NULL) { $names = drupal_container()->get('config.storage')->listAll($prefix); - $result = array(); - foreach ($names as $name) { - $config = config($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 = config($prefix . $id); - if (!$config->isNew()) { - $result[$id] = new $config_class($config->get(), $this->entityType); - } + $names[] = $prefix . $id; } - return $result; } + + $result = array(); + foreach (\Drupal::service('config.factory')->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/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index 6422e9d..7021c25 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -90,6 +90,21 @@ public function read($name) { } /** + * Implements Drupal\Core\Config\StorageInterface::readMultiple(). + * + * @throws Symfony\Component\Yaml\Exception\ParseException + */ + 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..026875d --- /dev/null +++ b/core/lib/Drupal/Core/Config/StorageCacheInterface.php @@ -0,0 +1,20 @@ + 'Config cached storage test', + 'description' => 'Tests the interaction of cache and file storage in CachedStorage.', + 'group' => 'Configuration' + ); + } + + /** + * 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); + $cachedStorage->readMultiple($configNames); + } + + /** + * Test fall through to file storage on a cache miss. + */ + public function testGetMultipleOnPartiallyPrimedCache() { + $configNames = array( + 'foo.bar', + 'baz.back', + 'dididi.idiback', + ); + $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); + } + + $storage = $this->getMock('Drupal\Core\Config\StorageInterface'); + $storage->expects($this->once()) + ->method('readMultiple') + ->with(array(2 => 'dididi.idiback')) + ->will($this->returnValue(array('dididi.idiback' => array('yo wtf')))); + + $cachedStorage = new CachedStorage($storage, $cache); + $cachedStorage->readMultiple($configNames); + } +}