Commit 20f5e2b1 authored by webchick's avatar webchick

Issue #1642062 by tim.plunkett, xjm, chx, merlinofchaos, damiankloip,...

Issue #1642062 by tim.plunkett, xjm, chx, merlinofchaos, damiankloip, dawehner, Berdir, aspilicious, Fabianx: Add TempStore for persistent, limited-term storage of non-cache data.
parent 08ff47b5
......@@ -55,6 +55,11 @@ public function build(ContainerBuilder $container) {
->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'));
......
......@@ -7,14 +7,27 @@
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().
*
......@@ -77,6 +90,22 @@ public function set($key, $value) {
->execute();
}
/**
* 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().
*/
......
<?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();
}
}
<?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);
}
......@@ -62,6 +62,19 @@ public function getAll();
*/
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.
*
......
......@@ -47,6 +47,17 @@ public function set($key, $value) {
$this->data[$key] = $value;
}
/**
* 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().
*/
......
......@@ -438,6 +438,35 @@ protected function assertNotIdentical($first, $second, $message = '', $group = '
return $this->assert($first !== $second, $message ? $message : t('Value @first is not identical to value @second.', array('@first' => var_export($first, TRUE), '@second' => var_export($second, TRUE))), $group);
}
/**
* 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.
*
......@@ -939,6 +968,26 @@ public static function randomName($length = 8) {
return $str;
}
/**
* 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.
*
......
<?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);
}
}
......@@ -21,6 +21,13 @@
*/
protected $storageClass;
/**
* An array of random stdClass objects.
*
* @var array
*/
protected $objects = array();
protected function setUp() {
parent::setUp();
......@@ -29,6 +36,11 @@ protected function setUp() {
$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 @@ public function testCRUD() {
$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 @@ public function testCRUD() {
}
// 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 @@ public function testNonExistingKeys() {
$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));
}
}
......@@ -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.',