diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 7359411..fb50050 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -2504,11 +2504,29 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) { $container ->register('config.storage.staging', 'Drupal\Core\Config\FileStorage') ->addArgument(config_get_config_directory(CONFIG_STAGING_DIRECTORY)); + $container + ->register('state.storage', 'Drupal\Core\KeyValueStore\DatabaseStorage') + ->addArgument('state'); } return $container; } /** + * Returns the state storage service. + * + * Use this to store machine-generated data, local to a specific environment + * that does not need deploying and does not need human editing; for example, + * the last time cron was run. Data which needs to be edited by humans and + * needs to be the same across development, production, etc. environments + * (for example, the system maintenance message) should use config() instead. + * + * @return Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ +function state() { + return drupal_container()->get('state.storage'); +} + +/** * Returns the test prefix if this is an internal request from SimpleTest. * * @return diff --git a/core/includes/common.inc b/core/includes/common.inc index c47494e..0214777 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -3178,7 +3178,7 @@ function drupal_pre_render_styles($elements) { function drupal_build_css_cache($css) { $data = ''; $uri = ''; - $map = variable_get('drupal_css_cache_files', array()); + $map = state()->get('drupal_css_cache_files') ?: array(); // Create a new array so that only the file names are used to create the hash. // This prevents new aggregates from being created unnecessarily. $css_data = array(); @@ -3246,7 +3246,7 @@ function drupal_build_css_cache($css) { } // Save the updated map. $map[$key] = $uri; - variable_set('drupal_css_cache_files', $map); + state()->set('drupal_css_cache_files', $map); } return $uri; } @@ -3411,7 +3411,7 @@ function _drupal_load_stylesheet($matches) { * Deletes old cached CSS files. */ function drupal_clear_css_cache() { - variable_del('drupal_css_cache_files'); + state()->delete('drupal_css_cache_files'); file_scan_directory('public://css', '/.*/', array('callback' => 'drupal_delete_file_if_stale')); } @@ -4721,7 +4721,7 @@ function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgro function drupal_build_js_cache($files) { $contents = ''; $uri = ''; - $map = variable_get('drupal_js_cache_files', array()); + $map = state()->get('drupal_js_cache_files') ?: array(); // Create a new array so that only the file names are used to create the hash. // This prevents new aggregates from being created unnecessarily. $js_data = array(); @@ -4766,7 +4766,7 @@ function drupal_build_js_cache($files) { } } $map[$key] = $uri; - variable_set('drupal_js_cache_files', $map); + state()->set('drupal_js_cache_files', $map); } return $uri; } diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 06be4ee..e723e1f 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -311,7 +311,9 @@ function install_begin_request(&$install_state) { $container->register('config.factory', 'Drupal\Core\Config\ConfigFactory') ->addArgument(new Reference('config.storage')) ->addArgument(new Reference('dispatcher')); - + $container + ->register('state.storage', 'Drupal\Core\KeyValueStore\DatabaseStorage') + ->addArgument('state'); drupal_container($container); } diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 639e1cf..4de25c7 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -318,7 +318,7 @@ function menu_get_ancestors($parts) { $ancestors = array(); $length = $number_parts - 1; $end = (1 << $number_parts) - 1; - $masks = variable_get('menu_masks'); + $masks = state()->get('menu_masks'); // If the optimized menu_masks array is not available use brute force to get // the correct $ancestors and $placeholders returned. Do not use this as the // default value of the menu_masks variable to avoid building such a big @@ -451,7 +451,7 @@ function menu_get_item($path = NULL, $router_item = NULL) { if (!isset($router_items[$path])) { // Rebuild if we know it's needed, or if the menu masks are missing which // occurs rarely, likely due to a race condition of multiple rebuilds. - if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) { + if (state()->get('menu_rebuild_needed') || !state()->get('menu_masks', array())) { menu_router_rebuild(); } $original_map = arg(NULL, $path); @@ -2666,7 +2666,7 @@ function menu_router_rebuild() { menu_cache_clear_all(); _menu_clear_page_cache(); // Indicate that the menu has been successfully rebuilt. - variable_del('menu_rebuild_needed'); + state()->delete('menu_rebuild_needed'); } catch (Exception $e) { $transaction->rollback(); @@ -3776,7 +3776,7 @@ function _menu_router_save($menu, $masks) { // Insert any remaining records. $insert->execute(); // Store the masks. - variable_set('menu_masks', $masks); + state()->set('menu_masks', $masks); return $menu; } diff --git a/core/includes/path.inc b/core/includes/path.inc index 07aeee5..71c10a3 100644 --- a/core/includes/path.inc +++ b/core/includes/path.inc @@ -72,7 +72,7 @@ function drupal_lookup_path($action, $path = '', $langcode = NULL) { // Retrieve the path alias whitelist. if (!isset($cache['whitelist'])) { - $cache['whitelist'] = variable_get('path_alias_whitelist', NULL); + $cache['whitelist'] = state()->get('path_alias_whitelist'); if (!isset($cache['whitelist'])) { $cache['whitelist'] = drupal_path_alias_whitelist_rebuild(); } @@ -391,7 +391,7 @@ function drupal_path_alias_whitelist_rebuild($source = NULL) { // When paths are inserted, only rebuild the whitelist if the system path // has a top level component which is not already in the whitelist. if (!empty($source)) { - $whitelist = variable_get('path_alias_whitelist', NULL); + $whitelist = state()->get('path_alias_whitelist'); if (isset($whitelist[strtok($source, '/')])) { return $whitelist; } @@ -404,7 +404,7 @@ function drupal_path_alias_whitelist_rebuild($source = NULL) { foreach ($result as $row) { $whitelist[$row->path] = TRUE; } - variable_set('path_alias_whitelist', $whitelist); + state()->set('path_alias_whitelist', $whitelist); return $whitelist; } diff --git a/core/includes/update.inc b/core/includes/update.inc index 9dd8456..33c8301 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -130,6 +130,36 @@ function update_prepare_d8_bootstrap() { update_extra_requirements($requirements); if ($has_required_schema) { + if (!db_table_exists('key_value')) { + $specs = array( + 'description' => 'Generic key-value storage table. See state() for an example.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + 'description' => 'The key of the key-value pair. As KEY is a SQL reserved keyword, name was chosen instead.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + ), + ), + 'primary key' => array('collection', 'name'), + ); + db_create_table('keyvalue', $specs); + } // Bootstrap variables so we can update theme while preparing the update // process. drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES); diff --git a/core/lib/Drupal/Core/KeyValueStore/AbstractStorage.php b/core/lib/Drupal/Core/KeyValueStore/AbstractStorage.php new file mode 100644 index 0000000..1e85151 --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/AbstractStorage.php @@ -0,0 +1,57 @@ +collection = $collection; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getCollectionName(). + */ + public function getCollectionName() { + return $this->collection; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::get(). + */ + public function get($key) { + $values = $this->getMultiple(array($key)); + return reset($values); + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setMultiple(). + */ + public function setMultiple(array $data) { + foreach ($data as $key => $value) { + $this->set($key, $value); + } + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::delete(). + */ + public function delete($key) { + $this->deleteMultiple(array($key)); + } + +} diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php new file mode 100644 index 0000000..19e95d4 --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php @@ -0,0 +1,96 @@ +table = isset($options['table']) ? $options['table'] : 'key_value'; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getMultiple(). + */ + public function getMultiple(array $keys) { + $values = array(); + try { + $result = db_query('SELECT name, value FROM {' . db_escape_table($this->table) . '} WHERE name IN (:keys) AND collection = :collection', array(':keys' => $keys, ':collection' => $this->collection))->fetchAllAssoc('name'); + foreach ($keys as $key) { + if (isset($result[$key])) { + $values[$key] = unserialize($result[$key]->value); + } + } + } + catch (\Exception $e) { + // @todo: Perhaps if the database is never going to be available, + // key/value requests should return FALSE in order to allow exception + // handling to occur but for now, keep it an array, always. + } + return $values; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getAll(). + */ + public function getAll() { + $result = db_query('SELECT name, value FROM {' . db_escape_table($this->table) . '} WHERE collection = :collection', array(':collection' => $this->collection)); + $values = array(); + + foreach ($result as $item) { + if ($item) { + $values[$item->name] = unserialize($item->value); + } + } + return $values; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::set(). + */ + public function set($key, $value) { + db_merge($this->table) + ->key(array( + 'name' => $key, + 'collection' => $this->collection, + )) + ->fields(array('value' => serialize($value))) + ->execute(); + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple(). + */ + public function deleteMultiple(array $keys) { + // Delete in chunks when a large array is passed. + do { + db_delete($this->table) + ->condition('name', array_splice($keys, 0, 1000)) + ->condition('collection', $this->collection) + ->execute(); + } + while (count($keys)); + } + +} diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php new file mode 100644 index 0000000..a32e08f --- /dev/null +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php @@ -0,0 +1,99 @@ +collection = $collection; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getCollectionName(). + */ + public function getCollectionName() { + return $this->collection; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::get(). + */ + public function get($key) { + return array_key_exists($key, $this->data) ? $this->data[$key] : FALSE; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getMultiple(). + */ + public function getMultiple(array $keys) { + return array_intersect_key($this->data, array_flip($keys)); + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getAll(). + */ + public function getAll() { + return $this->data; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::set(). + */ + public function set($key, $value) { + $this->data[$key] = $value; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setMultiple(). + */ + public function setMultiple(array $data) { + $this->data = $data + $this->data; + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::delete(). + */ + public function delete($key) { + unset($this->data[$key]); + } + + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple(). + */ + public function deleteMultiple(array $keys) { + foreach ($keys as $key) { + unset($this->data[$key]); + } + } + +} diff --git a/core/modules/color/lib/Drupal/color/Tests/ColorTest.php b/core/modules/color/lib/Drupal/color/Tests/ColorTest.php index 8902d4c..0c8c2a2 100644 --- a/core/modules/color/lib/Drupal/color/Tests/ColorTest.php +++ b/core/modules/color/lib/Drupal/color/Tests/ColorTest.php @@ -108,7 +108,7 @@ class ColorTest extends WebTestBase { $config->set('preprocess.css', 1); $config->save(); $this->drupalGet(''); - $stylesheets = variable_get('drupal_css_cache_files', array()); + $stylesheets = state()->get('drupal_css_cache_files') ?: array(); $stylesheet_content = ''; foreach ($stylesheets as $key => $uri) { $stylesheet_content .= join("\n", file(drupal_realpath($uri))); diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 44af89a..7aa5644 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -1553,7 +1553,7 @@ function hook_field_available_languages_alter(&$langcodes, $context) { function hook_field_attach_create_bundle($entity_type, $bundle) { // When a new bundle is created, the menu needs to be rebuilt to add the // Field UI menu item tabs. - variable_set('menu_rebuild_needed', TRUE); + state()->set('menu_rebuild_needed', TRUE); } /** diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module index bde3f35..63cf18b 100644 --- a/core/modules/field_ui/field_ui.module +++ b/core/modules/field_ui/field_ui.module @@ -309,7 +309,7 @@ function field_ui_element_info() { function field_ui_field_attach_create_bundle($entity_type, $bundle) { // When a new bundle is created, the menu needs to be rebuilt to add our // menu item tabs. - variable_set('menu_rebuild_needed', TRUE); + state()->set('menu_rebuild_needed', TRUE); } /** diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 4fd2be8..45e76ea 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -253,7 +253,7 @@ function image_form_system_file_system_settings_alter(&$form, &$form_state) { */ function image_system_file_system_settings_submit($form, &$form_state) { if ($form['file_public_path']['#default_value'] !== $form_state['values']['file_public_path']) { - variable_set('menu_rebuild_needed', TRUE); + state()->set('menu_rebuild_needed', TRUE); } } diff --git a/core/modules/search/search.admin.inc b/core/modules/search/search.admin.inc index 9168510..57e5d6b 100644 --- a/core/modules/search/search.admin.inc +++ b/core/modules/search/search.admin.inc @@ -175,7 +175,7 @@ function search_admin_settings_submit($form, &$form_state) { if ($config->get('active_modules') != $new_modules) { $config->set('active_modules', $new_modules); drupal_set_message(t('The active search modules have been changed.')); - variable_set('menu_rebuild_needed', TRUE); + state()->set('menu_rebuild_needed', TRUE); } $config->save(); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php index ba7a48b..f72d384 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php @@ -346,11 +346,11 @@ class JavaScriptTest extends WebTestBase { )); // Store the expected key for the first item in the cache. - $cache = array_keys(variable_get('drupal_js_cache_files', array())); + $cache = array_keys(state()->get('drupal_js_cache_files') ?: array()); $expected_key = $cache[0]; // Reset variables and add a file in a different scope first. - variable_del('drupal_js_cache_files'); + state()->delete('drupal_js_cache_files'); drupal_static_reset('drupal_add_js'); drupal_add_library('system', 'drupal'); drupal_add_js('some/custom/javascript_file.js', array('scope' => 'footer')); @@ -365,7 +365,7 @@ class JavaScriptTest extends WebTestBase { )); // Compare the expected key for the first file to the current one. - $cache = array_keys(variable_get('drupal_js_cache_files', array())); + $cache = array_keys(state()->get('drupal_js_cache_files') ?: array()); $key = $cache[0]; $this->assertEqual($key, $expected_key, 'JavaScript aggregation is not affected by ordering in different scopes.'); } diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageTest.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageTest.php new file mode 100644 index 0000000..a762030 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageTest.php @@ -0,0 +1,42 @@ + 'Database storage', + 'description' => 'Tests the key-value database storage.', + 'group' => 'Key-value store', + ); + } + + protected function setUp() { + parent::setUp(); + module_load_install('system'); + $schema = system_schema(); + db_create_table('keyvalue', $schema['keyvalue']); + } + + protected function tearDown() { + db_drop_table('keyvalue'); + parent::tearDown(); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/MemoryStorageTest.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/MemoryStorageTest.php new file mode 100644 index 0000000..e3d6b08 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/MemoryStorageTest.php @@ -0,0 +1,30 @@ + 'Memory storage', + 'description' => 'Tests the key-value memory storage.', + 'group' => 'Key-value store', + ); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php new file mode 100644 index 0000000..3e321f3 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php @@ -0,0 +1,104 @@ +collection1 = 'first'; + $this->collection2 = 'second'; + + $this->store1 = new $this->storageClass($this->collection1); + $this->store2 = new $this->storageClass($this->collection2); + } + + /** + * Tests CRUD operations. + */ + public function testCRUD() { + // Verify that each store returns its own collection name. + $this->assertEqual($this->store1->getCollectionName(), $this->collection1); + $this->assertEqual($this->store2->getCollectionName(), $this->collection2); + + // Verify that an item can be stored. + $this->store1->set('foo', 'bar'); + $this->assertEqual('bar', $this->store1->get('foo')); + // Verify that the other collection is not affected. + $this->assertFalse($this->store2->get('foo')); + + // Verify that an item can be updated. + $this->store1->set('foo', 'baz'); + $this->assertEqual('baz', $this->store1->get('foo')); + // Verify that the other collection is still not affected. + $this->assertFalse($this->store2->get('foo')); + + // Verify that a collection/name pair is unique. + $this->store2->set('foo', 'other'); + $this->assertEqual('baz', $this->store1->get('foo')); + $this->assertEqual('other', $this->store2->get('foo')); + + // Verify that an item can be deleted. + $this->store1->delete('foo'); + $this->assertFalse($this->store1->get('foo')); + + // Verify that the other collection is not affected. + $this->assertEqual('other', $this->store2->get('foo')); + $this->store2->delete('foo'); + $this->assertFalse($this->store2->get('foo')); + + // Verify that multiple items can be stored. + $values = array( + 'foo' => 'bar', + 'baz' => 'qux', + ); + $this->store1->setMultiple($values); + + // Verify that multiple items can be retrieved. + $result = $this->store1->getMultiple(array('foo', 'baz')); + $this->assertEqual($values, $result); + + // Verify that the other collection was not affected. + $this->assertFalse($this->store2->get('foo')); + $this->assertFalse($this->store2->get('baz')); + + // Verify that all items in a collection can be retrieved. + // Ensure that an item with the same name exists in the other collection. + $this->store2->set('foo', 'other'); + $result = $this->store1->getAll(); + // Not using assertIdentical(), since the order is not defined for getAll(). + $this->assertEqual(count($result), count($values)); + foreach ($result as $key => $value) { + $this->assertEqual($values[$key], $value); + } + // Verify that all items in the other collection are different. + $result = $this->store2->getAll(); + $this->assertEqual($result, array('foo' => 'other')); + + // Verify that multiple items can be deleted. + $this->store1->deleteMultiple(array_keys($values)); + $this->assertFalse($this->store1->get('foo')); + $this->assertFalse($this->store1->get('bar')); + $this->assertFalse($this->store1->getMultiple(array('foo', 'baz'))); + // Verify that the item in the other collection still exists. + $this->assertEqual('other', $this->store2->get('foo')); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/RebuildTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/RebuildTest.php index b6e0ec2..9cf1080 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/RebuildTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/RebuildTest.php @@ -38,7 +38,7 @@ class RebuildTest extends WebTestBase { // Now we enable the rebuild variable and send a request to rebuild the menu // item. Now 'admin' should exist. - variable_set('menu_rebuild_needed', TRUE); + state()->set('menu_rebuild_needed', TRUE); // The request should trigger the rebuild. $this->drupalGet(''); $admin_exists = db_query('SELECT path from {menu_router} WHERE path = :path', array(':path' => 'admin'))->fetchField(); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 484b384..924519f 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -866,6 +866,34 @@ function system_schema() { ), ); + $schema['key_value'] = array( + 'description' => 'Generic key-value storage table. See state() for an example.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + 'description' => 'The key of the key-value pair. As KEY is a SQL reserved keyword, name was chosen instead.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + ), + ), + 'primary key' => array('collection', 'name'), + ); + $schema['menu_router'] = array( 'description' => 'Maps paths to various callbacks (access, page and title)', 'fields' => array(