summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwebchick2012-10-06 18:22:39 (GMT)
committerwebchick2012-10-06 18:22:39 (GMT)
commit20f5e2b1bdab2497e93bfe1ad4b4ddb610341344 (patch)
treec3cf93e965d2a5d63dfc3654f3e36f156f47b58a
parent08ff47b5fd1e9ff1039dba129ac1c4ea97d47e3c (diff)
Issue #1642062 by tim.plunkett, xjm, chx, merlinofchaos, damiankloip, dawehner, Berdir, aspilicious, Fabianx: Add TempStore for persistent, limited-term storage of non-cache data.
-rw-r--r--core/lib/Drupal/Core/CoreBundle.php5
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php29
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php133
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php52
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php13
-rw-r--r--core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php11
-rw-r--r--core/modules/simpletest/lib/Drupal/simpletest/TestBase.php49
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageExpirableTest.php167
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php73
-rw-r--r--core/modules/system/system.install83
-rw-r--r--core/modules/user/lib/Drupal/user/TempStore.php197
-rw-r--r--core/modules/user/lib/Drupal/user/TempStoreException.php13
-rw-r--r--core/modules/user/lib/Drupal/user/TempStoreFactory.php72
-rw-r--r--core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php155
14 files changed, 1035 insertions, 17 deletions
diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php
index c1abfe5..ed15dd5 100644
--- a/core/lib/Drupal/Core/CoreBundle.php
+++ b/core/lib/Drupal/Core/CoreBundle.php
@@ -55,6 +55,11 @@ class CoreBundle extends Bundle
->setFactoryMethod('getConnection')
->addArgument('slave');
$container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager');
+ // Add the user's storage for temporary, non-cache data.
+ $container->register('lock', 'Drupal\Core\Lock\DatabaseLockBackend');
+ $container->register('user.tempstore', 'Drupal\user\TempStoreFactory')
+ ->addArgument(new Reference('database'))
+ ->addArgument(new Reference('lock'));
$container->register('router.dumper', '\Drupal\Core\Routing\MatcherDumper')
->addArgument(new Reference('database'));
diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php
index 532f690..c837796 100644
--- a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php
+++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php
@@ -7,15 +7,28 @@
namespace Drupal\Core\KeyValueStore;
+use Drupal\Core\Database\Query\Merge;
+
/**
* Defines a default key/value store implementation.
*
* This is Drupal's default key/value store implementation. It uses the database
* to store key/value data.
+ *
+ * @todo This class still calls db_* functions directly because it's needed
+ * very early, pre-Container. Once the early bootstrap dependencies are
+ * sorted out, consider using an injected database connection instead.
*/
class DatabaseStorage extends StorageBase {
/**
+ * The name of the SQL table to use.
+ *
+ * @var string
+ */
+ protected $table;
+
+ /**
* Overrides Drupal\Core\KeyValueStore\StorageBase::__construct().
*
* @param string $collection
@@ -78,6 +91,22 @@ class DatabaseStorage extends StorageBase {
}
/**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setIfNotExists().
+ */
+ public function setIfNotExists($key, $value) {
+ $result = db_merge($this->table)
+ ->insertFields(array(
+ 'collection' => $this->collection,
+ 'name' => $key,
+ 'value' => serialize($value),
+ ))
+ ->condition('collection', $this->collection)
+ ->condition('name', $key)
+ ->execute();
+ return $result == Merge::STATUS_INSERT;
+ }
+
+ /**
* Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple().
*/
public function deleteMultiple(array $keys) {
diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php
new file mode 100644
index 0000000..eb73878
--- /dev/null
+++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\KeyValueStore\DatabaseStorageExpirable.
+ */
+
+namespace Drupal\Core\KeyValueStore;
+
+use Drupal\Core\Database\Query\Merge;
+use Drupal\Core\Database\Database;
+
+/**
+ * Defines a default key/value store implementation for expiring items.
+ *
+ * This key/value store implementation uses the database to store key/value
+ * data with an expire date.
+ */
+class DatabaseStorageExpirable extends DatabaseStorage implements KeyValueStoreExpirableInterface {
+
+ /**
+ * The connection object for this storage.
+ *
+ * @var Drupal\Core\Database\Connection
+ */
+ protected $connection;
+
+ /**
+ * Overrides Drupal\Core\KeyValueStore\StorageBase::__construct().
+ *
+ * @param string $collection
+ * The name of the collection holding key and value pairs.
+ * @param array $options
+ * An associative array of options for the key/value storage collection.
+ * Keys used:
+ * - connection: (optional) The database connection to use for storing the
+ * data. Defaults to the current connection.
+ * - table: (optional) The name of the SQL table to use. Defaults to
+ * key_value_expire.
+ */
+ public function __construct($collection, array $options = array()) {
+ parent::__construct($collection, $options);
+ $this->connection = isset($options['connection']) ? $options['connection'] : Database::getConnection();
+ $this->table = isset($options['table']) ? $options['table'] : 'key_value_expire';
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getMultiple().
+ */
+ public function getMultiple(array $keys) {
+ $values = $this->connection->query(
+ 'SELECT name, value FROM {' . $this->connection->escapeTable($this->table) . '} WHERE expire > :now AND name IN (:keys) AND collection = :collection',
+ array(
+ ':now' => REQUEST_TIME,
+ ':keys' => $keys,
+ ':collection' => $this->collection,
+ ))->fetchAllKeyed();
+ return array_map('unserialize', $values);
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getAll().
+ */
+ public function getAll() {
+ $values = $this->connection->query(
+ 'SELECT name, value FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND expire > :now',
+ array(
+ ':collection' => $this->collection,
+ ':now' => REQUEST_TIME
+ ))->fetchAllKeyed();
+ return array_map('unserialize', $values);
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreExpireInterface::setWithExpire().
+ */
+ function setWithExpire($key, $value, $expire) {
+ $this->connection->merge($this->table)
+ ->key(array(
+ 'name' => $key,
+ 'collection' => $this->collection,
+ ))
+ ->fields(array(
+ 'value' => serialize($value),
+ 'expire' => REQUEST_TIME + $expire,
+ ))
+ ->execute();
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface::setWithExpireIfNotExists().
+ */
+ function setWithExpireIfNotExists($key, $value, $expire) {
+ $result = $this->connection->merge($this->table)
+ ->insertFields(array(
+ 'collection' => $this->collection,
+ 'name' => $key,
+ 'value' => serialize($value),
+ 'expire' => REQUEST_TIME + $expire,
+ ))
+ ->condition('collection', $this->collection)
+ ->condition('name', $key)
+ ->execute();
+ return $result == Merge::STATUS_INSERT;
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreExpirablInterface::setMultipleWithExpire().
+ */
+ function setMultipleWithExpire(array $data, $expire) {
+ foreach ($data as $key => $value) {
+ $this->setWithExpire($key, $value, $expire);
+ }
+ }
+
+ /**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteMultiple().
+ */
+ public function deleteMultiple(array $keys) {
+ $this->garbageCollection();
+ parent::deleteMultiple($keys);
+ }
+
+ /**
+ * Deletes expired items.
+ */
+ public function garbageCollection() {
+ $this->connection->delete($this->table)
+ ->condition('expire', REQUEST_TIME, '<')
+ ->execute();
+ }
+
+}
diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php
new file mode 100644
index 0000000..e39dbe5
--- /dev/null
+++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreExpirableInterface.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface.
+ */
+
+namespace Drupal\Core\KeyValueStore;
+
+/**
+ * Defines the interface for expiring data in a key/value store.
+ */
+interface KeyValueStoreExpirableInterface extends KeyValueStoreInterface {
+
+ /**
+ * Saves a value for a given key with a time to live.
+ *
+ * @param string $key
+ * The key of the data to store.
+ * @param mixed $value
+ * The data to store.
+ * @param int $expire
+ * The time to live for items, in seconds.
+ */
+ function setWithExpire($key, $value, $expire);
+
+ /**
+ * Sets a value for a given key with a time to live if it does not yet exist.
+ *
+ * @param string $key
+ * The key of the data to store.
+ * @param mixed $value
+ * The data to store.
+ * @param int $expire
+ * The time to live for items, in seconds.
+ *
+ * @return bool
+ * TRUE if the data was set, or FALSE if it already existed.
+ */
+ function setWithExpireIfNotExists($key, $value, $expire);
+
+ /**
+ * Saves an array of values with a time to live.
+ *
+ * @param array $data
+ * An array of data to store.
+ * @param int $expire
+ * The time to live for items, in seconds.
+ */
+ function setMultipleWithExpire(array $data, $expire);
+
+}
diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php
index d350472..8f006ce 100644
--- a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php
+++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php
@@ -63,6 +63,19 @@ interface KeyValueStoreInterface {
public function set($key, $value);
/**
+ * Saves a value for a given key if it does not exist yet.
+ *
+ * @param string $key
+ * The key of the data to store.
+ * @param mixed $value
+ * The data to store.
+ *
+ * @return bool
+ * TRUE if the data was set, FALSE if it already existed.
+ */
+ public function setIfNotExists($key, $value);
+
+ /**
* Saves key/value pairs.
*
* @param array $data
diff --git a/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php b/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php
index ef3ef0e..6101507 100644
--- a/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php
+++ b/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php
@@ -48,6 +48,17 @@ class MemoryStorage extends StorageBase {
}
/**
+ * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setIfNotExists().
+ */
+ public function setIfNotExists($key, $value) {
+ if (!isset($this->data[$key])) {
+ $this->data[$key] = $value;
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
* Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::setMultiple().
*/
public function setMultiple(array $data) {
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
index 6370033..7320a74 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -439,6 +439,35 @@ abstract class TestBase {
}
/**
+ * Checks to see if two objects are identical.
+ *
+ * @param object $object1
+ * The first object to check.
+ * @param object $object2
+ * The second object to check.
+ * @param $message
+ * The message to display along with the assertion.
+ * @param $group
+ * The type of assertion - examples are "Browser", "PHP".
+ *
+ * @return
+ * TRUE if the assertion succeeded, FALSE otherwise.
+ */
+ protected function assertIdenticalObject($object1, $object2, $message = '', $group = '') {
+ $message = $message ?: format_string('!object1 is identical to !object2', array(
+ '!object1' => var_export($object1, TRUE),
+ '!object2' => var_export($object2, TRUE),
+ ));
+ $identical = TRUE;
+ foreach ($object1 as $key => $value) {
+ $identical = $identical && isset($object2->$key) && $object2->$key === $value;
+ }
+ return $this->assertTrue($identical, $message);
+ }
+
+
+
+ /**
* Fire an assertion that is always positive.
*
* @param $message
@@ -940,6 +969,26 @@ abstract class TestBase {
}
/**
+ * Generates a random PHP object.
+ *
+ * @param int $size
+ * The number of random keys to add to the object.
+ *
+ * @return \stdClass
+ * The generated object, with the specified number of random keys. Each key
+ * has a random string value.
+ */
+ public static function randomObject($size = 4) {
+ $object = new \stdClass();
+ for ($i = 0; $i < $size; $i++) {
+ $random_key = self::randomName();
+ $random_value = self::randomString();
+ $object->{$random_key} = $random_value;
+ }
+ return $object;
+ }
+
+ /**
* Converts a list of possible parameters into a stack of permutations.
*
* Takes a list of parameters containing possible values, and converts all of
diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageExpirableTest.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageExpirableTest.php
new file mode 100644
index 0000000..b173445
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/DatabaseStorageExpirableTest.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\system\Tests\KeyValueStore\DatabaseStorageExpirableTest.
+ */
+
+namespace Drupal\system\Tests\KeyValueStore;
+
+/**
+ * Tests the key-value database storage.
+ */
+class DatabaseStorageExpirableTest extends StorageTestBase {
+
+ /**
+ * The name of the class to test.
+ *
+ * The tests themselves are in StorageTestBase and use this class.
+ */
+ protected $storageClass = 'Drupal\Core\KeyValueStore\DatabaseStorageExpirable';
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Expirable database storage',
+ 'description' => 'Tests the expirable key-value database storage.',
+ 'group' => 'Key-value store',
+ );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ module_load_install('system');
+ $schema = system_schema();
+ db_create_table('key_value_expire', $schema['key_value_expire']);
+ }
+
+ protected function tearDown() {
+ db_drop_table('key_value_expire');
+ parent::tearDown();
+ }
+
+ /**
+ * Tests CRUD functionality with expiration.
+ */
+ public function testCRUDWithExpiration() {
+ // Verify that an item can be stored with setWithExpire().
+ // Use a random expiration in each test.
+ $this->store1->setWithExpire('foo', $this->objects[0], rand(500, 299792458));
+ $this->assertIdenticalObject($this->objects[0], $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 with setWithExpire().
+ $this->store1->setWithExpire('foo', $this->objects[1], rand(500, 299792458));
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get('foo'));
+ // Verify that the other collection is still not affected.
+ $this->assertFalse($this->store2->get('foo'));
+
+ // Verify that the expirable data key is unique.
+ $this->store2->setWithExpire('foo', $this->objects[2], rand(500, 299792458));
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get('foo'));
+ $this->assertIdenticalObject($this->objects[2], $this->store2->get('foo'));
+
+ // Verify that multiple items can be stored with setMultipleWithExpire().
+ $values = array(
+ 'foo' => $this->objects[3],
+ 'bar' => $this->objects[4],
+ );
+ $this->store1->setMultipleWithExpire($values, rand(500, 299792458));
+ $result = $this->store1->getMultiple(array('foo', 'bar'));
+ foreach ($values as $j => $value) {
+ $this->assertIdenticalObject($value, $result[$j]);
+ }
+
+ // Verify that the other collection was not affected.
+ $this->assertIdenticalObject($this->store2->get('foo'), $this->objects[2]);
+ $this->assertFalse($this->store2->get('bar'));
+
+ // 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', $this->objects[5]);
+ $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' => $this->objects[5]));
+
+ // 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', 'bar')));
+ // Verify that the item in the other collection still exists.
+ $this->assertIdenticalObject($this->objects[5], $this->store2->get('foo'));
+
+ // Test that setWithExpireIfNotExists() succeeds only the first time.
+ $key = $this->randomName();
+ for ($i = 0; $i <= 1; $i++) {
+ // setWithExpireIfNotExists() should be TRUE the first time (when $i is
+ // 0) and FALSE the second time (when $i is 1).
+ $this->assertEqual(!$i, $this->store1->setWithExpireIfNotExists($key, $this->objects[$i], rand(500, 299792458)));
+ $this->assertIdenticalObject($this->objects[0], $this->store1->get($key));
+ // Verify that the other collection is not affected.
+ $this->assertFalse($this->store2->get($key));
+ }
+
+ // Remove the item and try to set it again.
+ $this->store1->delete($key);
+ $this->store1->setWithExpireIfNotExists($key, $this->objects[1], rand(500, 299792458));
+ // This time it should succeed.
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get($key));
+ // Verify that the other collection is still not affected.
+ $this->assertFalse($this->store2->get($key));
+
+ }
+
+ /**
+ * Tests data expiration and garbage collection.
+ */
+ public function testExpiration() {
+ $day = 604800;
+
+ // Set an item to expire in the past and another without an expiration.
+ $this->store1->setWithExpire('yesterday', 'all my troubles seemed so far away', -1 * $day);
+ $this->store1->set('troubles', 'here to stay');
+
+ // Only the non-expired item should be returned.
+ $this->assertFalse($this->store1->get('yesterday'));
+ $this->assertIdentical($this->store1->get('troubles'), 'here to stay');
+ $this->assertIdentical(count($this->store1->getMultiple(array('yesterday', 'troubles'))), 1);
+
+ // Store items set to expire in the past in various ways.
+ $this->store1->setWithExpire($this->randomName(), $this->objects[0], -7 * $day);
+ $this->store1->setWithExpireIfNotExists($this->randomName(), $this->objects[1], -5 * $day);
+ $this->store1->setMultipleWithExpire(
+ array(
+ $this->randomName() => $this->objects[2],
+ $this->randomName() => $this->objects[3],
+ ),
+ -3 * $day
+ );
+ $this->store1->setWithExpireIfNotExists('yesterday', "you'd forgiven me", -1 * $day);
+ $this->store1->setWithExpire('still', "'til we say we're sorry", 2 * $day);
+
+ // Ensure only non-expired items are retrived.
+ $all = $this->store1->getAll();
+ $this->assertIdentical(count($all), 2);
+ foreach (array('troubles', 'still') as $key) {
+ $this->assertTrue(!empty($all[$key]));
+ }
+
+ // Perform garbage collection and confirm that the expired items are
+ // deleted from the database.
+ $this->store1->garbageCollection();
+ $result = db_query(
+ 'SELECT name, value FROM {key_value_expire} WHERE collection = :collection',
+ array(
+ ':collection' => $this->collection1,
+ ))->fetchAll();
+ $this->assertIdentical(sizeof($result), 2);
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php
index 1eec831..41d216c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/StorageTestBase.php
@@ -21,6 +21,13 @@ abstract class StorageTestBase extends UnitTestBase {
*/
protected $storageClass;
+ /**
+ * An array of random stdClass objects.
+ *
+ * @var array
+ */
+ protected $objects = array();
+
protected function setUp() {
parent::setUp();
@@ -29,6 +36,11 @@ abstract class StorageTestBase extends UnitTestBase {
$this->store1 = new $this->storageClass($this->collection1);
$this->store2 = new $this->storageClass($this->collection2);
+
+ // Create several objects for testing.
+ for ($i = 0; $i <= 5; $i++) {
+ $this->objects[$i] = $this->randomObject();
+ }
}
/**
@@ -40,49 +52,51 @@ abstract class StorageTestBase extends UnitTestBase {
$this->assertIdentical($this->store2->getCollectionName(), $this->collection2);
// Verify that an item can be stored.
- $this->store1->set('foo', 'bar');
- $this->assertIdentical('bar', $this->store1->get('foo'));
+ $this->store1->set('foo', $this->objects[0]);
+ $this->assertIdenticalObject($this->objects[0], $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->assertIdentical('baz', $this->store1->get('foo'));
+ $this->store1->set('foo', $this->objects[1]);
+ $this->assertIdenticalObject($this->objects[1], $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->assertIdentical('baz', $this->store1->get('foo'));
- $this->assertIdentical('other', $this->store2->get('foo'));
+ $this->store2->set('foo', $this->objects[2]);
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get('foo'));
+ $this->assertIdenticalObject($this->objects[2], $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->assertIdentical('other', $this->store2->get('foo'));
+ $this->assertIdenticalObject($this->objects[2], $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',
+ 'foo' => $this->objects[3],
+ 'bar' => $this->objects[4],
);
$this->store1->setMultiple($values);
// Verify that multiple items can be retrieved.
- $result = $this->store1->getMultiple(array('foo', 'baz'));
- $this->assertIdentical($values, $result);
+ $result = $this->store1->getMultiple(array('foo', 'bar'));
+ foreach ($values as $j => $value) {
+ $this->assertIdenticalObject($value, $result[$j]);
+ }
// Verify that the other collection was not affected.
$this->assertFalse($this->store2->get('foo'));
- $this->assertFalse($this->store2->get('baz'));
+ $this->assertFalse($this->store2->get('bar'));
// 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');
+ $this->store2->set('foo', $this->objects[5]);
$result = $this->store1->getAll();
// Not using assertIdentical(), since the order is not defined for getAll().
$this->assertEqual(count($result), count($values));
@@ -91,15 +105,15 @@ abstract class StorageTestBase extends UnitTestBase {
}
// Verify that all items in the other collection are different.
$result = $this->store2->getAll();
- $this->assertEqual($result, array('foo' => 'other'));
+ $this->assertEqual($result, array('foo' => $this->objects[5]));
// 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')));
+ $this->assertFalse($this->store1->getMultiple(array('foo', 'bar')));
// Verify that the item in the other collection still exists.
- $this->assertIdentical('other', $this->store2->get('foo'));
+ $this->assertIdenticalObject($this->objects[5], $this->store2->get('foo'));
}
/**
@@ -123,4 +137,29 @@ abstract class StorageTestBase extends UnitTestBase {
$this->assertFalse(isset($values['foo']), "Key 'foo' not found.");
$this->assertIdentical($values['bar'], 'baz');
}
+
+ /**
+ * Tests the setIfNotExists() method.
+ */
+ public function testSetIfNotExists() {
+ $key = $this->randomName();
+ // Test that setIfNotExists() succeeds only the first time.
+ for ($i = 0; $i <= 1; $i++) {
+ // setIfNotExists() should be TRUE the first time (when $i is 0) and
+ // FALSE the second time (when $i is 1).
+ $this->assertEqual(!$i, $this->store1->setIfNotExists($key, $this->objects[$i]));
+ $this->assertIdenticalObject($this->objects[0], $this->store1->get($key));
+ // Verify that the other collection is not affected.
+ $this->assertFalse($this->store2->get($key));
+ }
+
+ // Remove the item and try to set it again.
+ $this->store1->delete($key);
+ $this->store1->setIfNotExists($key, $this->objects[1]);
+ // This time it should succeed.
+ $this->assertIdenticalObject($this->objects[1], $this->store1->get($key));
+ // Verify that the other collection is still not affected.
+ $this->assertFalse($this->store2->get($key));
+ }
+
}
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 8331d55..28d95da 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -832,6 +832,44 @@ function system_schema() {
'primary key' => array('collection', 'name'),
);
+ $schema['key_value_expire'] = array(
+ 'description' => 'Generic key/value storage table with an expiration.',
+ 'fields' => array(
+ 'collection' => array(
+ 'description' => 'A named collection of key and value pairs.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'name' => array(
+ // KEY is an SQL reserved word, so use 'name' as the key's field name.
+ 'description' => 'The key of the key/value pair.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'value' => array(
+ 'description' => 'The value of the key/value pair.',
+ 'type' => 'blob',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'translatable' => TRUE,
+ ),
+ 'expire' => array(
+ 'description' => 'The time since Unix epoch in seconds when this item expires. Defaults to the maximum possible time.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 2147483647,
+ ),
+ ),
+ 'primary key' => array('collection', 'name'),
+ 'indexes' => array(
+ 'all' => array('name', 'collection', 'expire'),
+ ),
+ );
+
$schema['menu_router'] = array(
'description' => 'Maps paths to various callbacks (access, page and title)',
'fields' => array(
@@ -2102,6 +2140,51 @@ function system_update_8022() {
}
/**
+ * Create the 'key_value_expire' table.
+ */
+function system_update_8023() {
+ $table = array(
+ 'description' => 'Generic key/value storage table with an expiration.',
+ 'fields' => array(
+ 'collection' => array(
+ 'description' => 'A named collection of key and value pairs.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'name' => array(
+ // KEY is an SQL reserved word, so use 'name' as the key's field name.
+ 'description' => 'The key of the key/value pair.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'value' => array(
+ 'description' => 'The value of the key/value pair.',
+ 'type' => 'blob',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'translatable' => TRUE,
+ ),
+ 'expire' => array(
+ 'description' => 'The time since Unix epoch in seconds when this item expires. Defaults to the maximum possible time.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 2147483647,
+ ),
+ ),
+ 'primary key' => array('collection', 'name'),
+ 'indexes' => array(
+ 'all' => array('name', 'collection', 'expire'),
+ ),
+ );
+
+ db_create_table('key_value_expire', $table);
+}
+
+/**
* @} End of "defgroup updates-7.x-to-8.x".
* The next series of updates should start at 9000.
*/
diff --git a/core/modules/user/lib/Drupal/user/TempStore.php b/core/modules/user/lib/Drupal/user/TempStore.php
new file mode 100644
index 0000000..c8a6dd1
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/TempStore.php
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\user\TempStore.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
+use Drupal\Core\Lock\LockBackendInterface;
+
+/**
+ * Stores and retrieves temporary data for a given owner.
+ *
+ * A TempStore can be used to make temporary, non-cache data available across
+ * requests. The data for the TempStore is stored in one key/value collection.
+ * TempStore data expires automatically after a given timeframe.
+ *
+ * The TempStore is different from a cache, because the data in it is not yet
+ * saved permanently and so it cannot be rebuilt. Typically, the TempStore
+ * might be used to store work in progress that is later saved permanently
+ * elsewhere, e.g. autosave data, multistep forms, or in-progress changes
+ * to complex configuration that are not ready to be saved.
+ *
+ * Each TempStore belongs to a particular owner (e.g. a user, session, or
+ * process). Multiple owners may use the same key/value collection, and the
+ * owner is stored along with the key/value pair.
+ *
+ * Every key is unique within the collection, so the TempStore can check
+ * whether a particular key is already set by a different owner. This is
+ * useful for informing one owner that the data is already in use by another;
+ * for example, to let one user know that another user is in the process of
+ * editing certain data, or even to restrict other users from editing it at
+ * the same time. It is the responsibility of the implementation to decide
+ * when and whether one owner can use or update another owner's data.
+ *
+ * @todo We could add getIfOwner() or setIfOwner() methods to make this more
+ * explicit.
+ */
+class TempStore {
+
+ /**
+ * The key/value storage object used for this data.
+ *
+ * @var Drupal\Core\KeyValueStore\KeyValueStoreExpireInterface;
+ */
+ protected $storage;
+
+ /**
+ * The lock object used for this data.
+ *
+ * @var Drupal\Core\Lock\LockBackendInterface
+ */
+ protected $lockBackend;
+
+ /**
+ * The owner key to store along with the data (e.g. a user or session ID).
+ *
+ * @var mixed
+ */
+ protected $owner;
+
+ /**
+ * The time to live for items in seconds.
+ *
+ * By default, data is stored for one week (604800 seconds) before expiring.
+ *
+ * @var int
+ *
+ * @todo Currently, this property is not exposed anywhere, and so the only
+ * way to override it is by extending the class.
+ */
+ protected $expire = 604800;
+
+ /**
+ * Constructs a new object for accessing data from a key/value store.
+ *
+ * @param KeyValueStoreExpireInterface $storage
+ * The key/value storage object used for this data. Each storage object
+ * represents a particular collection of data and will contain any number
+ * of key/value pairs.
+ * @param Drupal\Core\Lock\LockBackendInterface $lockBackend
+ * The lock object used for this data.
+ * @param mixed $owner
+ * The owner key to store along with the data (e.g. a user or session ID).
+ */
+ function __construct(KeyValueStoreExpirableInterface $storage, LockBackendInterface $lockBackend, $owner) {
+ $this->storage = $storage;
+ $this->lockBackend = $lockBackend;
+ $this->owner = $owner;
+ }
+
+ /**
+ * Retrieves a value from this TempStore for a given key.
+ *
+ * @param string $key
+ * The key of the data to retrieve.
+ *
+ * @return mixed
+ * The data associated with the key, or NULL if the key does not exist.
+ */
+ function get($key) {
+ if ($object = $this->storage->get($key)) {
+ return $object->data;
+ }
+ }
+
+ /**
+ * Stores a particular key/value pair only if the key doesn't already exist.
+ *
+ * @param string $key
+ * The key of the data to check and store.
+ * @param mixed $value
+ * The data to store.
+ *
+ * @return bool
+ * TRUE if the data was set, or FALSE if it already existed.
+ */
+ function setIfNotExists($key, $value) {
+ $value = (object) array(
+ 'owner' => $this->owner,
+ 'data' => $value,
+ 'updated' => REQUEST_TIME,
+ );
+ return $this->storage->setWithExpireIfNotExists($key, $value, $this->expire);
+ }
+
+ /**
+ * Stores a particular key/value pair in this TempStore.
+ *
+ * @param string $key
+ * The key of the data to store.
+ * @param mixed $value
+ * The data to store.
+ */
+ function set($key, $value) {
+ if (!$this->lockBackend->acquire($key)) {
+ $this->lockBackend->wait($key);
+ if (!$this->lockBackend->acquire($key)) {
+ throw new TempStoreException(format_string("Couldn't acquire lock to update item %key in %collection temporary storage.", array(
+ '%key' => $key,
+ '%collection' => $this->storage->collection,
+ )));
+ }
+ }
+
+ $value = (object) array(
+ 'owner' => $this->owner,
+ 'data' => $value,
+ 'updated' => REQUEST_TIME,
+ );
+ $this->storage->setWithExpire($key, $value, $this->expire);
+ $this->lockBackend->release($key);
+ }
+
+ /**
+ * Returns the metadata associated with a particular key/value pair.
+ *
+ * @param string $key
+ * The key of the data to store.
+ *
+ * @return mixed
+ * An object with the owner and updated time if the key has a value, or
+ * NULL otherwise.
+ */
+ function getMetadata($key) {
+ // Fetch the key/value pair and its metadata.
+ $object = $this->storage->get($key);
+ if ($object) {
+ // Don't keep the data itself in memory.
+ unset($object->data);
+ return $object;
+ }
+ }
+
+ /**
+ * Deletes data from the store for a given key and releases the lock on it.
+ *
+ * @param string $key
+ * The key of the data to delete.
+ */
+ function delete($key) {
+ if (!$this->lockBackend->acquire($key)) {
+ $this->lockBackend->wait($key);
+ if (!$this->lockBackend->acquire($key)) {
+ throw new TempStoreException(format_string("Couldn't acquire lock to delete item %key from %collection temporary storage.", array(
+ '%key' => $key,
+ '%collection' => $this->storage->collection,
+ )));
+ }
+ }
+ $this->storage->delete($key);
+ $this->lockBackend->release($key);
+ }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/TempStoreException.php b/core/modules/user/lib/Drupal/user/TempStoreException.php
new file mode 100644
index 0000000..497a2ef
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/TempStoreException.php
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\user\TempStoreException.
+ */
+
+namespace Drupal\user;
+
+/**
+ * Defines the exception thrown if the TempStore cannot acquire a lock.
+ */
+class TempStoreException extends \Exception {}
diff --git a/core/modules/user/lib/Drupal/user/TempStoreFactory.php b/core/modules/user/lib/Drupal/user/TempStoreFactory.php
new file mode 100644
index 0000000..473d515
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/TempStoreFactory.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\TempStoreFactory.
+ */
+
+namespace Drupal\user;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\KeyValueStore\DatabaseStorageExpirable;
+use Drupal\Core\Lock\LockBackendInterface;
+
+/**
+ * Creates a key/value storage object for the current user or anonymous session.
+ */
+class TempStoreFactory {
+
+ /**
+ * The connection object used for this data.
+ *
+ * @var Drupal\Core\Database\Connection $connection
+ */
+ protected $connection;
+
+ /**
+ * The lock object used for this data.
+ *
+ * @var Drupal\Core\Lock\LockBackendInterface $lockBackend
+ */
+ protected $lockBackend;
+
+ /**
+ * Constructs a Drupal\user\TempStoreFactory object.
+ *
+ * @param Drupal\Core\Database\Connection $connection
+ * The connection object used for this data.
+ * @param Drupal\Core\Lock\LockBackendInterface $lockBackend
+ * The lock object used for this data.
+ */
+ function __construct(Connection $connection, LockBackendInterface $lockBackend) {
+ $this->connection = $connection;
+ $this->lockBackend = $lockBackend;
+ }
+
+ /**
+ * Creates a TempStore for the current user or anonymous session.
+ *
+ * @param string $collection
+ * The collection name to use for this key/value store. This is typically
+ * a shared namespace or module name, e.g. 'views', 'entity', etc.
+ * @param mixed $owner
+ * (optional) The owner of this TempStore. By default, the TempStore is
+ * owned by the currently authenticated user, or by the active anonymous
+ * session if no user is logged in.
+ *
+ * @return Drupal\user\TempStore
+ * An instance of the the key/value store.
+ */
+ function get($collection, $owner = NULL) {
+ // Use the currently authenticated user ID or the active user ID unless
+ // the owner is overridden.
+ if (!isset($owner)) {
+ $owner = $GLOBALS['user']->uid ?: session_id();
+ }
+
+ // Store the data for this collection in the database.
+ $storage = new DatabaseStorageExpirable($collection, array('connection' => $this->connection));
+ return new TempStore($storage, $this->lockBackend, $owner);
+ }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php b/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php
new file mode 100644
index 0000000..362e739
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Tests/TempStoreDatabaseTest.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\Tests\TempStoreDatabaseTest.
+ */
+
+namespace Drupal\user\Tests;
+
+use Drupal\simpletest\UnitTestBase;
+use Drupal\user\TempStoreFactory;
+use Drupal\Core\Lock\DatabaseLockBackend;
+use Drupal\Core\Database\Database;
+
+/**
+ * Tests the TempStore namespace.
+ *
+ * @see Drupal\Core\TempStore\TempStore.
+ */
+class TempStoreDatabaseTest extends UnitTestBase {
+
+ /**
+ * A key/value store factory.
+ *
+ * @var Drupal\user\TempStoreFactory
+ */
+ protected $storeFactory;
+
+ /**
+ * The name of the key/value collection to set and retrieve.
+ *
+ * @var string
+ */
+ protected $collection;
+
+ /**
+ * An array of (fake) user IDs.
+ *
+ * @var array
+ */
+ protected $users = array();
+
+ /**
+ * An array of random stdClass objects.
+ *
+ * @var array
+ */
+ protected $objects = array();
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'TempStore',
+ 'description' => 'Tests the temporary object storage system.',
+ 'group' => 'TempStore',
+ );
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ // Install system tables to test the key/value storage without installing a
+ // full Drupal environment.
+ module_load_install('system');
+ $schema = system_schema();
+ db_create_table('semaphore', $schema['semaphore']);
+ db_create_table('key_value_expire', $schema['key_value_expire']);
+
+ // Create a key/value collection.
+ $this->storeFactory = new TempStoreFactory(Database::getConnection(), new DatabaseLockBackend());
+ $this->collection = $this->randomName();
+
+ // Create several objects for testing.
+ for ($i = 0; $i <= 3; $i++) {
+ $this->objects[$i] = $this->randomObject();
+ }
+ // Create two mock users for testing.
+ for ($i = 0; $i <= 1; $i++) {
+ $this->users[$i] = mt_rand(500, 5000000);
+ $this->stores[$i] = $this->getStorePerUID($this->users[$i]);
+ }
+
+ }
+
+ protected function tearDown() {
+ db_drop_table('key_value_expire');
+ db_drop_table('semaphore');
+ parent::tearDown();
+ }
+
+ /**
+ * Tests the UserTempStore API.
+ */
+ public function testUserTempStore() {
+ $key = $this->randomName();
+ // Test that setIfNotExists() succeeds only the first time.
+ for ($i = 0; $i <= 1; $i++) {
+ // setIfNotExists() should be TRUE the first time (when $i is 0) and
+ // FALSE the second time (when $i is 1).
+ $this->assertEqual(!$i, $this->stores[0]->setIfNotExists($key, $this->objects[$i]));
+ $metadata = $this->stores[0]->getMetadata($key);
+ $this->assertEqual($this->users[0], $metadata->owner);
+ $this->assertIdenticalObject($this->objects[0], $this->stores[0]->get($key));
+ // Another user should get the same result.
+ $metadata = $this->stores[1]->getMetadata($key);
+ $this->assertEqual($this->users[0], $metadata->owner);
+ $this->assertIdenticalObject($this->objects[0], $this->stores[1]->get($key));
+ }
+
+ // Remove the item and try to set it again.
+ $this->stores[0]->delete($key);
+ $this->stores[0]->setIfNotExists($key, $this->objects[1]);
+ // This time it should succeed.
+ $this->assertIdenticalObject($this->objects[1], $this->stores[0]->get($key));
+
+ // This user can update the object.
+ $this->stores[0]->set($key, $this->objects[2]);
+ $this->assertIdenticalObject($this->objects[2], $this->stores[0]->get($key));
+ // The object is the same when another user loads it.
+ $this->assertIdenticalObject($this->objects[2], $this->stores[1]->get($key));
+ // Another user can update the object and become the owner.
+ $this->stores[1]->set($key, $this->objects[3]);
+ $this->assertIdenticalObject($this->objects[3], $this->stores[0]->get($key));
+ $this->assertIdenticalObject($this->objects[3], $this->stores[1]->get($key));
+ $metadata = $this->stores[1]->getMetadata($key);
+ $this->assertEqual($this->users[1], $metadata->owner);
+
+ // The first user should be informed that the second now owns the data.
+ $metadata = $this->stores[0]->getMetadata($key);
+ $this->assertEqual($this->users[1], $metadata->owner);
+
+ // Now manually expire the item (this is not exposed by the API) and then
+ // assert it is no longer accessible.
+ db_update('key_value_expire')
+ ->fields(array('expire' => REQUEST_TIME - 1))
+ ->condition('collection', $this->collection)
+ ->condition('name', $key)
+ ->execute();
+ $this->assertFalse($this->stores[0]->get($key));
+ $this->assertFalse($this->stores[1]->get($key));
+ }
+
+ /**
+ * Returns a TempStore for this collection belonging to the given user.
+ *
+ * @param int $uid
+ * A user ID.
+ *
+ * @return Drupal\user\TempStore
+ * The key/value store object.
+ */
+ protected function getStorePerUID($uid) {
+ return $this->storeFactory->get($this->collection, $uid);
+ }
+
+}