Commit 3d94f559 authored by catch's avatar catch

Issue #1637478 by alexpott, pounard, catch: Add a PHP array cache backend.

parent 6e8a671f
<?php
/**
* @file
* Definition of Drupal\Core\Cache\ArrayBackend.
*/
namespace Drupal\Core\Cache;
/**
* Defines a memory cache implementation.
*
* Stores cache items in memory using a PHP array.
*
* Should be used for unit tests and specialist use-cases only, does not
* store cached items between requests.
*
*/
class MemoryBackend implements CacheBackendInterface {
/**
* Array to store cache objects.
*/
protected $cache;
/**
* All tags invalidated during the request.
*/
protected $invalidatedTags = array();
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::__construct().
*/
public function __construct($bin) {
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::get().
*/
public function get($cid) {
if (isset($this->cache[$cid])) {
return $this->prepareItem($this->cache[$cid]);
}
else {
return FALSE;
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
public function getMultiple(&$cids) {
$ret = array();
$items = array_intersect_key($this->cache, array_flip($cids));
foreach ($items as $item) {
$item = $this->prepareItem($item);
if ($item) {
$ret[$item->cid] = $item;
}
}
$cids = array_diff($cids, array_keys($ret));
return $ret;
}
/**
* Prepares a cached item.
*
* Checks that items are either permanent or did not expire, and returns data
* as appropriate.
*
* @param stdClass $cache
* An item loaded from cache_get() or cache_get_multiple().
*
* @return mixed
* The item with data as appropriate or FALSE if there is no
* valid item to load.
*/
protected function prepareItem($cache) {
if (!isset($cache->data)) {
return FALSE;
}
// The cache data is invalid if any of its tags have been cleared since.
if (count($cache->tags) && $this->hasInvalidatedTags($cache)) {
return FALSE;
}
return $cache;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::set().
*/
public function set($cid, $data, $expire = CACHE_PERMANENT, array $tags = array()) {
$this->cache[$cid] = (object) array(
'cid' => $cid,
'data' => $data,
'expire' => $expire,
'tags' => $tags,
'checksum' => $this->checksum($this->flattenTags($tags)),
);
}
/*
* Calculates a checksum so data can be invalidated using tags.
*/
function checksum($tags) {
$checksum = "";
foreach($tags as $tag) {
// Has the tag already been invalidated.
if (isset($this->invalidatedTags[$tag])) {
$checksum = $checksum . $tag . ':' . $this->invalidatedTags[$tag];
}
}
return $checksum;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::delete().
*/
public function delete($cid) {
unset($this->cache[$cid]);
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteMultiple().
*/
public function deleteMultiple(array $cids) {
$this->cache = array_diff_key($this->cache, array_flip($cids));
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deletePrefix().
*/
public function deletePrefix($prefix) {
foreach ($this->cache as $cid => $item) {
if (strpos($cid, $prefix) === 0) {
unset($this->cache[$cid]);
}
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::flush().
*/
public function flush() {
$this->cache = array();
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::expire().
*
* Cache expiration is not implemented for PHP ArrayBackend as this backend
* only persists during a single request and expiration are done using
* REQUEST_TIME.
*/
public function expire() {
}
/**
* Checks to see if any of the tags associated with a cache object have been
* invalidated.
*
* @param object @cache
* An cache object to calculate and compare it's original checksum for.
*
* @return boolean
* TRUE if the a tag has been invalidated, FALSE otherwise.
*/
protected function hasInvalidatedTags($cache) {
if ($cache->checksum != $this->checksum($this->flattenTags($cache->tags))) {
return TRUE;
}
return FALSE;
}
/**
* Flattens a tags array into a numeric array suitable for string storage.
*
* @param array $tags
* Associative array of tags to flatten.
*
* @return
* An array of flattened tag identifiers.
*/
protected function flattenTags(array $tags) {
if (isset($tags[0])) {
return $tags;
}
$flat_tags = array();
foreach ($tags as $namespace => $values) {
if (is_array($values)) {
foreach ($values as $value) {
$flat_tags["$namespace:$value"] = "$namespace:$value";
}
}
else {
$flat_tags["$namespace:$value"] = "$namespace:$values";
}
}
return $flat_tags;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
*/
public function invalidateTags(array $tags) {
$flat_tags = $this->flattenTags($tags);
foreach($flat_tags as $tag) {
if (isset($this->invalidatedTags[$tag])) {
$this->invalidatedTags[$tag] = $this->invalidatedTags[$tag] + 1;
}
else {
$this->invalidatedTags[$tag] = 1;
}
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::isEmpty().
*/
public function isEmpty() {
return empty($this->cache);
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::garbageCollection()
*/
public function garbageCollection() {
}
}
......@@ -13,6 +13,7 @@
* Provides helper methods for cache tests.
*/
abstract class CacheTestBase extends WebTestBase {
protected $default_bin = 'page';
protected $default_cid = 'test_temporary';
protected $default_value = 'CacheTest';
......
......@@ -11,6 +11,7 @@
* Tests cache clearing methods.
*/
class ClearTest extends CacheTestBase {
public static function getInfo() {
return array(
'name' => 'Cache clear test',
......@@ -26,83 +27,6 @@ function setUp() {
parent::setUp();
}
/**
* Test clearing using a cid.
*/
function testClearCid() {
$cache = cache($this->default_bin);
$cache->set('test_cid_clear', $this->default_value);
$this->assertCacheExists(t('Cache was set for clearing cid.'), $this->default_value, 'test_cid_clear');
$cache->delete('test_cid_clear');
$this->assertCacheRemoved(t('Cache was removed after clearing cid.'), 'test_cid_clear');
}
/**
* Test clearing using wildcard.
*/
function testClearWildcard() {
$cache = cache($this->default_bin);
$cache->set('test_cid_clear1', $this->default_value);
$cache->set('test_cid_clear2', $this->default_value);
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two caches were created for checking cid "*" with wildcard true.'));
$cache->flush();
$this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
|| $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two caches removed after clearing cid "*" with wildcard true.'));
$cache->set('test_cid_clear1', $this->default_value);
$cache->set('test_cid_clear2', $this->default_value);
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two caches were created for checking cid substring with wildcard true.'));
$cache->deletePrefix('test_');
$this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
|| $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two caches removed after clearing cid substring with wildcard true.'));
}
/**
* Test clearing using an array.
*/
function testClearArray() {
// Create three cache entries.
$cache = cache($this->default_bin);
$cache->set('test_cid_clear1', $this->default_value);
$cache->set('test_cid_clear2', $this->default_value);
$cache->set('test_cid_clear3', $this->default_value);
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value)
&& $this->checkCacheExists('test_cid_clear3', $this->default_value),
t('Three cache entries were created.'));
// Clear two entries using an array.
$cache->deleteMultiple(array('test_cid_clear1', 'test_cid_clear2'));
$this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
|| $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two cache entries removed after clearing with an array.'));
$this->assertTrue($this->checkCacheExists('test_cid_clear3', $this->default_value),
t('Entry was not cleared from the cache'));
// Set the cache clear threshold to 2 to confirm that the full bin is cleared
// when the threshold is exceeded.
variable_set('cache_clear_threshold', 2);
$cache->set('test_cid_clear1', $this->default_value);
$cache->set('test_cid_clear2', $this->default_value);
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two cache entries were created.'));
$cache->deleteMultiple(array('test_cid_clear1', 'test_cid_clear2', 'test_cid_clear3'));
$this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
|| $this->checkCacheExists('test_cid_clear2', $this->default_value)
|| $this->checkCacheExists('test_cid_clear3', $this->default_value),
t('All cache entries removed when the array exceeded the cache clear threshold.'));
}
/**
* Test drupal_flush_all_caches().
*/
......@@ -124,49 +48,4 @@ function testFlushAllCaches() {
$this->assertFalse($this->checkCacheExists($cid, $this->default_value, $bin), t('All cache entries removed from @bin.', array('@bin' => $bin)));
}
}
/**
* Test clearing using cache tags.
*/
function testClearTags() {
$cache = cache($this->default_bin);
$cache->set('test_cid_clear1', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
$cache->set('test_cid_clear2', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two cache items were created.'));
cache_invalidate(array('test_tag' => array(1)));
$this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
|| $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two caches removed after clearing a cache tag.'));
$cache->set('test_cid_clear1', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
$cache->set('test_cid_clear2', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(2)));
$cache->set('test_cid_clear3', $this->default_value, CACHE_PERMANENT, array('test_tag_foo' => array(3)));
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value)
&& $this->checkCacheExists('test_cid_clear3', $this->default_value),
t('Two cached items were created.'));
cache_invalidate(array('test_tag_foo' => array(3)));
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Cached items not matching the tag were not cleared.'));
$this->assertFalse($this->checkCacheExists('test_cid_clear3', $this->default_value),
t('Cached item matching the tag was removed.'));
// For our next trick, we will attempt to clear data in multiple bins.
$tags = array('test_tag' => array(1, 2, 3));
$bins = array('cache', 'cache_page', 'cache_bootstrap');
foreach ($bins as $bin) {
cache($bin)->set('test', $this->default_value, CACHE_PERMANENT, $tags);
$this->assertTrue($this->checkCacheExists('test', $this->default_value, $bin), 'Cache item was set in bin.');
}
cache_invalidate(array('test_tag' => array(2)));
foreach ($bins as $bin) {
$this->assertFalse($this->checkCacheExists('test', $this->default_value, $bin), 'Tag expire affected item in bin.');
}
$this->assertFalse($this->checkCacheExists('test_cid_clear2', $this->default_value), 'Cached items matching tag were cleared.');
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value), 'Cached items not matching tag were not cleared.');
}
}
<?php
/**
* @file
* Definition of Drupal\system\Tests\Cache\DatabaseBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\DatabaseBackend;
/**
* Tests DatabaseBackend using GenericCacheBackendUnitTestBase.
*/
class DatabaseBackendUnitTest extends GenericCacheBackendUnitTestBase {
public static function getInfo() {
return array(
'name' => 'Database backend',
'description' => 'Unit test of the database backend using the generic cache unit test base.',
'group' => 'Cache',
);
}
protected function createCacheBackend($bin) {
return new DatabaseBackend($bin);
}
public function setUpCacheBackend() {
drupal_install_schema('system');
}
public function tearDownCacheBackend() {
drupal_uninstall_schema('system');
}
}
<?php
/**
* @file
* Definition of Drupal\system\Tests\Cache\GenericCacheBackendUnitTestBase.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\simpletest\UnitTestBase;
use stdClass;
/**
* Full generic unit test suite for any cache backend. In order to use it for a
* cache backend implementation extend this class and override the
* createBackendInstace() method to return an object.
*
* @see DatabaseBackendUnitTestCase
* For a full working implementation.
*/
abstract class GenericCacheBackendUnitTestBase extends UnitTestBase {
/**
* Array of objects implementing Drupal\Core\Cache\CacheBackendInterface.
*
* @var array
*/
protected $cachebackends;
/**
* Cache bin to use for testing.
*
* @var string
*/
protected $testBin;
/**
* Random value to use in tests.
*
* @var string
*/
protected $defaultValue;
/**
* Get testing bin.
*
* Override this method if you want to work on a different bin than the
* default one.
*
* @return string
* Bin name.
*/
protected function getTestBin() {
if (!isset($this->testBin)) {
$this->testBin = 'page';
}
return $this->testBin;
}
/**
* Create a cache backend to test.
*
* Override this method to test a CacheBackend.
*
* @param string $bin
* Bin name to use for this backend instance.
*
* @return Drupal\Core\Cache\CacheBackendInterface
* Cache backend to test.
*/
protected abstract function createCacheBackend($bin);
/**
* Allow specific implementation to change the environement before test run.
*/
public function setUpCacheBackend() {
}
/**
* Allow specific implementation to alter the environement after test run but
* before the real tear down, which will changes things such as the database
* prefix.
*/
public function tearDownCacheBackend() {
}
/**
* Get backend to test, this will get a shared instance set in the object.
*
* @return Drupal\Core\Cache\CacheBackendInterface
* Cache backend to test.
*/
final function getCacheBackend($bin = null) {
if (!isset($bin)) {
$bin = $this->getTestBin();
}
if (!isset($this->cachebackends[$bin])) {
$this->cachebackends[$bin] = $this->createCacheBackend($bin);
// Ensure the backend is empty.
$this->cachebackends[$bin]->flush();
}
return $this->cachebackends[$bin];
}
public function setUp() {
$this->cachebackends = array();
$this->defaultValue = $this->randomName(10);
parent::setUp();
$this->setUpCacheBackend();
}
public function tearDown() {
// Destruct the registered backend, each test will get a fresh instance,
// properly flushing it here ensure that on persistant data backends they
// will come up empty the next test.
foreach ($this->cachebackends as $bin => $cachebackend) {
$this->cachebackends[$bin]->flush();
}
unset($this->cachebackends);
$this->tearDownCacheBackend();
parent::tearDown();
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::get() and
* Drupal\Core\Cache\CacheBackendInterface::set().
*/
public function testSetGet() {
$backend = $this->getCacheBackend();
$data = 7;
$this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1.");
$backend->set('test1', $data);
$cached = $backend->get('test1');
$this->assert(is_object($cached), "Backend returned an object for cache id test1.");
$this->assertIdentical($data, $cached->data);
$data = array('value' => 3);
$this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2.");
$backend->set('test2', $data);
$cached = $backend->get('test2');
$this->assert(is_object($cached), "Backend returned an object for cache id test2.");
$this->assertIdentical($data, $cached->data);
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::delete().
*/
public function testDelete() {
$backend = $this->getCacheBackend();
$this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1.");
$backend->set('test1', 7);
$this->assert(is_object($backend->get('test1')), "Backend returned an object for cache id test1.");
$this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2.");
$backend->set('test2', 3);
$this->assert(is_object($backend->get('test2')), "Backend returned an object for cache id %cid.");
$backend->delete('test1');