class APDQCache in Asynchronous Prefetch Database Query Cache 7
A pretty darn quick cache implementation of Drupal's default cache backend.
This cache backend extends Drupal's default database cache implementation by altering how drupal interacts with the database.
Hierarchy
- class \DrupalDatabaseCache implements DrupalCacheInterface
- class \APDQCache
Expanded class hierarchy of APDQCache
See also
20 string references to 'APDQCache'
- APDQCache::getCaller in ./
apdqc.cache.inc - Get the last 3 items from debug_backtrace.
- ApdqcCacheClearCase::setUp in ./
apdqc.test - Install the apdqc module and setup the cache.
- ApdqcCacheTestCase::setUp in ./
apdqc.test - Setup for tests.
- ApdqcPrefetchDrupalDefaultEntityController::__call in ./
apdqc.module - Magic method; forward all calls to the original base class.
- apdqc_async_data in ./
apdqc.mysql.inc - Used to get & parse async cached data.
File
- ./
apdqc.cache.inc, line 261 - Extends Drupal's default database cache so async queries happen.
View source
class APDQCache extends DrupalDatabaseCache {
protected $compressed;
protected $compression_level;
/**
* Constructs a DrupalDatabaseCache object.
*
* @param string $bin
* The cache bin for which the object is created.
*/
public function __construct($bin) {
// Load the correct database backend.
$db_type = apdqc_fast_get_db_type();
// Async queries are only available in PHP 5.3+.
if ($db_type === 'mysql' && defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 50300) {
require_once 'apdqc.mysql.inc';
}
$this->compressed = $this
->apdqcCacheCompressBin($bin);
if ($this->compressed) {
$this->compression_level = $this
->apdqcCacheCompressionLevelBin($bin);
}
parent::__construct($bin);
}
/**
* Checks if this cache bin is compressed or not.
*
* @param string $bin
* The cache bin name.
*
* @return bool
* TRUE if this cache bin is compressed.
*/
protected function apdqcCacheCompressBin($bin) {
$compress = variable_get('apdqc_cache_compress_' . $bin);
if (!isset($compress)) {
$compress = variable_get('apdqc_cache_default_compress', APDQC_CACHE_DEFAULT_COMPRESS);
}
return $compress;
}
/**
* Checks if this cache bin is compressed or not.
*
* @param string $bin
* The cache bin name.
*
* @return bool
* TRUE if this cache bin is compressed.
*/
protected function apdqcCacheCompressionLevelBin($bin) {
$compress = variable_get('apdqc_cache_compression_level_' . $bin);
if (!isset($compress)) {
$compress = variable_get('apdqc_cache_default_compression_level', APDQC_CACHE_DEFAULT_COMPRESSION_LEVEL);
}
return $compress;
}
/**
* Implements DrupalCacheInterface::getMultiple().
*
* We handle garbage collection differently; this method is reimplemented only
* to remove a call to the garbageCollection() method.
*/
public function getMultiple(&$cids) {
try {
$gc_frequency = variable_get('cache_garbage_collection_frequency', CACHE_GARBAGE_COLLECTION_FREQUENCY);
if (empty($gc_frequency)) {
// Garbage collection necessary when enforcing a minimum cache lifetime.
$this
->garbageCollection($this->bin);
}
$query_cids = $cids;
$prefetch = array();
$result = array();
if (variable_get('cache_bootstrap_prefetch', CACHE_BOOTSTRAP_PREFETCH) || defined('APDQC_PREFETCH') && variable_get('apdqc_prefetch', APDQC_PREFETCH)) {
$prefetch = apdqc_async_data(FALSE, $this->bin, $cids);
$query_cids = array_diff($cids, array_keys($prefetch));
$prefetch = array_values($prefetch);
}
$result = array();
if (!empty($query_cids)) {
// Build query.
$query = Database::getConnection()
->prefixTables("SELECT cid, data, created, expire, serialized FROM {" . db_escape_table($this->bin) . "}");
// If this is the cache_bootstrap bin and the cid is variables, get
// the bootstrap_modules & lookup_cache ids as well.
if ($this->bin === 'cache_bootstrap' && $query_cids[0] === 'variables' && count($query_cids) == 1) {
$query_cids[] = 'bootstrap_modules';
$query_cids[] = 'lookup_cache';
$query_cids[] = 'system_list';
$query_cids[] = 'module_implements';
$bootstrap = TRUE;
}
foreach ($query_cids as $cid) {
$escaped_cids[] = apdqc_escape_string($cid);
}
$escaped_cids = "'" . implode("', '", $escaped_cids) . "'";
$query .= " WHERE cid IN ({$escaped_cids})";
// Run query.
$result = apdqc_query(array(
$this->bin,
), $query_cids, $query, array(
'fetch_all' => TRUE,
));
}
if (is_string($result) && $result === 'NO DB') {
// When serving cached pages, the overhead of using db_select() was
// found to add around 30% overhead to the request. Since $this->bin is
// a variable, this means the call to db_query() here uses a
// concatenated string. This is highly discouraged under any other
// circumstances, and is used here only due to the performance overhead
// we would incur otherwise. When serving an uncached page, the overhead
// of using db_select() is a much smaller proportion of the request.
// @codingStandardsIgnoreLine
$result = db_query('SELECT cid, data, created, expire, serialized FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(
':cids' => $cids,
));
}
else {
$result = array_merge($result, $prefetch);
}
$cache = array();
if (!empty($result)) {
foreach ($result as $item) {
// Skip empty items.
if (empty($item)) {
continue;
}
if (!empty($bootstrap) && is_array($item) && ($item['cid'] === 'bootstrap_modules' || $item['cid'] === 'lookup_cache' || $item['cid'] === 'system_list' || $item['cid'] === 'module_implements')) {
$local_storage =& drupal_static('apdqc_async_data');
$item['bin'] = $this->bin;
$local_storage[$this->bin][$item['cid']] = $item;
continue;
}
// Convert to an object to mirror db_query if needed.
if (is_array($item)) {
$item = (object) $item;
}
$item = $this
->prepareItem($item);
if ($item) {
$cache[$item->cid] = $item;
}
}
}
$cids = array_diff($cids, array_keys($cache));
return $cache;
} catch (Exception $e) {
// If the database is never going to be available, cache requests should
// return FALSE in order to allow exception handling to occur.
return array();
}
}
/**
* Prepares a cached item.
*
* Checks that items are either permanent or did not expire, and unserializes
* data as appropriate.
*
* @param object $cache
* An item loaded from cache_get() or cache_get_multiple().
*
* @return mixed
* The item with data unserialized as appropriate or FALSE if there is no
* valid item to load.
*/
protected function prepareItem($cache) {
if (!isset($cache->data)) {
return FALSE;
}
// If the cached data is temporary and subject to a per-user minimum
// lifetime, compare the cache entry timestamp with the user session
// cache_expiration timestamp. If the cache entry is too old, ignore it.
if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && isset($_SESSION['cache_expiration'][$this->bin]) && $_SESSION['cache_expiration'][$this->bin] > $cache->created) {
// Ignore cache data that is too old and thus not valid for this user.
return FALSE;
}
// Inflate and unserialize data.
apdqc_inflate_unserialize($cache);
return $cache;
}
/**
* Generic garbage collection method.
*
* Rather than respecting minimum cache lifetime at the bin-level, we respect
* it on an entry-by-entry basis.
*
* Additionally, we've moved session cache handling to a separate method.
*/
protected function garbageCollection() {
$cache_lifetime = variable_get('cache_lifetime', 0);
$gc_frequency = variable_get('cache_garbage_collection_frequency', CACHE_GARBAGE_COLLECTION_FREQUENCY);
if (empty($gc_frequency)) {
$cache_lifetime = variable_get('cache_lifetime', 0);
// Clean-up the per-user cache expiration session data, so that the
// session handler can properly clean-up the session data for anonymous
// users.
if (isset($_SESSION['cache_expiration'])) {
$expire = REQUEST_TIME - $cache_lifetime;
foreach ($_SESSION['cache_expiration'] as $bin => $timestamp) {
if ($timestamp < $expire) {
unset($_SESSION['cache_expiration'][$bin]);
}
}
if (!$_SESSION['cache_expiration']) {
unset($_SESSION['cache_expiration']);
}
}
// Garbage collection of temporary items is only necessary when enforcing
// a minimum cache lifetime.
if (!$cache_lifetime) {
return;
}
// When cache lifetime is in force, avoid running garbage collection too
// often since this will remove temporary cache items indiscriminately.
$cache_flush = variable_get('cache_flush_' . $this->bin, 0);
if ($cache_flush && $cache_flush + $cache_lifetime <= REQUEST_TIME) {
// Reset the variable immediately to prevent a meltdown in heavy load
// situations.
variable_set('cache_flush_' . $this->bin, 0);
// Time to flush old cache data
// Build query.
$query = Database::getConnection()
->prefixTables("DELETE FROM {" . db_escape_table($this->bin) . "} ");
$query .= "WHERE (expire <> " . CACHE_PERMANENT . " AND expire <= " . $cache_flush . ")";
// Run query.
$result = apdqc_query(array(
$this->bin,
), array(
'*',
), $query, array(
'async' => TRUE,
));
if (is_string($result) && $result === 'NO DB') {
// Use the DrupalDatabaseCache class.
return parent::garbageCollection();
}
}
}
else {
// Build query.
$query = Database::getConnection()
->prefixTables("DELETE FROM {" . db_escape_table($this->bin) . "} ");
if (!empty($cache_lifetime)) {
// Clear expired items from the cache.
$query .= "WHERE (expire > " . CACHE_PERMANENT . " AND expire < " . REQUEST_TIME . ")";
// Clear CACHE_TEMPORARY items that are older than cache_lifetime.
$query .= "OR (expire = " . CACHE_TEMPORARY . " AND created < " . apdqc_escape_string(REQUEST_TIME - $cache_lifetime) . ")";
}
else {
// No minimum lifetime. Flush all expired and temporary cache entries.
$query .= "WHERE (expire <> " . CACHE_PERMANENT . " AND expire < " . REQUEST_TIME . ")";
}
// Run query.
$result = apdqc_query(array(
$this->bin,
), array(
'*',
), $query, array(
'async' => TRUE,
));
if (is_string($result) && $result === 'NO DB') {
if (!empty($cache_lifetime)) {
// Clear expired items from the cache.
db_delete($this->bin)
->condition('expire', CACHE_PERMANENT, '>')
->condition('expire', REQUEST_TIME, '<')
->execute();
// Clear CACHE_TEMPORARY items that are older than cache_lifetime.
db_delete($this->bin)
->condition('expire', CACHE_TEMPORARY)
->condition('created', REQUEST_TIME - $cache_lifetime, '<')
->execute();
}
else {
// No minimum lifetime. Flush all expired and temporary cache entries.
db_delete($this->bin)
->condition('expire', CACHE_PERMANENT, '<>')
->condition('expire', REQUEST_TIME, '<')
->execute();
}
}
}
}
/**
* Cleans up the per-session cache expiration data.
*/
protected function cleanSession() {
$cache_lifetime = variable_get('cache_lifetime', 0);
if (isset($_SESSION['cache_expiration'])) {
$expire = REQUEST_TIME - $cache_lifetime;
foreach ($_SESSION['cache_expiration'] as $bin => $timestamp) {
if ($timestamp < $expire) {
unset($_SESSION['cache_expiration'][$bin]);
}
}
if (!$_SESSION['cache_expiration']) {
unset($_SESSION['cache_expiration']);
}
}
}
/**
* Implements DrupalCacheInterface::set().
*/
public function set($cid, $data, $expire = CACHE_PERMANENT) {
// When setting cache items, clean up old session data in case it is stale.
$gc_frequency = variable_get('cache_garbage_collection_frequency', CACHE_GARBAGE_COLLECTION_FREQUENCY);
if (!empty($gc_frequency)) {
$this
->cleanSession();
}
$fields = array(
'serialized' => 0,
'created' => REQUEST_TIME,
'expire' => $expire,
);
if (!is_string($data)) {
$fields['data'] = serialize($data);
$fields['serialized'] = 1;
}
else {
$fields['data'] = $data;
$fields['serialized'] = 0;
}
if ($this->compressed > 1 || $this->compressed && $fields['serialized']) {
$deflate = gzdeflate($fields['data'], $this->compression_level);
if (strlen($deflate) < strlen($fields['data'])) {
$fields['data'] = $deflate;
$fields['serialized'] += 2;
}
}
try {
// Build query.
$escaped_cid = apdqc_escape_string($cid);
$escaped_data = apdqc_escape_string($fields['data']);
$query = Database::getConnection()
->prefixTables("INSERT INTO {" . db_escape_table($this->bin) . "}");
$query .= " (cid, serialized, created, expire, data) VALUES ('{$escaped_cid}', '{$fields['serialized']}', '{$fields['created']}', '{$fields['expire']}', '{$escaped_data}')";
$query .= " ON DUPLICATE KEY UPDATE serialized = '{$fields['serialized']}', created = '{$fields['created']}', expire = '{$fields['expire']}', data = '{$escaped_data}'";
// Run query.
$result = apdqc_query(array(
$this->bin,
), array(
$cid,
), $query, array(
'async' => TRUE,
));
if (is_string($result) && $result === 'NO DB') {
return parent::set($cid, $data, $expire);
}
} catch (Exception $e) {
// The database may not be available, so we'll ignore cache_set requests.
}
}
/**
* Implements DrupalCacheInterface::clear().
*
* Here, we ensure that cron only runs cache garbage collection at a
* configurable frequency, defaulting to 24 hours.
*/
public function clear($cid = NULL, $wildcard = FALSE) {
if (variable_get('apdqc_call_hook_on_clear', APDQC_CALL_HOOK_ON_CLEAR)) {
$apdqc_cache_clear_alter = module_implements('apdqc_cache_clear_alter');
if (!empty($apdqc_cache_clear_alter)) {
$caller = $this
->getCaller();
// The bin variable is not alterable; use a copy.
$bin = $this->bin;
// Call hook_apdqc_cache_clear_alter().
drupal_alter('apdqc_cache_clear', $cid, $wildcard, $bin, $caller);
}
}
// Use default core logic for this cache clear if inside of a transaction.
if (Database::getConnection()
->inTransaction()) {
// Do not use any prefectched data.
$apdqc_async_data =& drupal_static('apdqc_async_data');
$do_not_use_async_data =& drupal_static('apdqc_async_data_do_not_use_async');
$do_not_run_prefetch_array =& drupal_static('apdqc_run_prefetch_array');
$apdqc_async_data[$this->bin] = array();
$do_not_use_async_data[$this->bin] = TRUE;
$do_not_run_prefetch_array[$this->bin] = TRUE;
$output = parent::clear($cid, $wildcard);
$this
->callCacheClearHooks($cid, $wildcard);
return $output;
}
$gc_frequency = variable_get('cache_garbage_collection_frequency', CACHE_GARBAGE_COLLECTION_FREQUENCY);
$cids = array(
$cid,
);
if ($wildcard || is_null($cid)) {
$cids = array(
'*',
);
}
if (!empty($gc_frequency) && empty($cid)) {
// Use backtrace to check that the clear came from system_cron.
$backtrace = debug_backtrace();
if ($backtrace[2]['function'] == 'system_cron') {
$cache_lifetime = variable_get('cache_lifetime', 0);
$name = 'cache_garbage_collect_' . $this->bin;
$window = max($cache_lifetime, $gc_frequency);
if (flood_is_allowed($name, 1, $window, 'cron')) {
$this
->garbageCollection();
flood_register_event($name, $window, 'cron');
}
}
else {
$this
->garbageCollection();
}
$this
->callCacheClearHooks($cid, $wildcard);
return;
}
if (is_null($cid)) {
// Build query.
$query = Database::getConnection()
->prefixTables("DELETE FROM {" . db_escape_table($this->bin) . "} ");
if (variable_get('cache_lifetime', 0)) {
// We store the time in the current user's session. We then simulate
// that the cache was flushed for this user by not returning cached
// data that was cached before the timestamp.
$_SESSION['cache_expiration'][$this->bin] = REQUEST_TIME;
$cache_flush = variable_get('cache_flush_' . $this->bin, 0);
if ($cache_flush == 0) {
// This is the first request to clear the cache, start a timer.
variable_set('cache_flush_' . $this->bin, REQUEST_TIME);
}
elseif (REQUEST_TIME > $cache_flush + variable_get('cache_lifetime', 0)) {
// Clear the cache for everyone, cache_lifetime seconds have
// passed since the first request to clear the cache.
$query .= "WHERE (expire <> " . CACHE_PERMANENT . " AND expire < " . REQUEST_TIME . ")";
$result = apdqc_query(array(
$this->bin,
), $cids, $query, array(
'async' => TRUE,
));
if (is_string($result) && $result === 'NO DB') {
// Use core connection if the additional connection to mysql fails.
$output = parent::clear($cid, $wildcard);
$this
->callCacheClearHooks($cid, $wildcard);
return $output;
}
variable_set('cache_flush_' . $this->bin, 0);
}
}
else {
// No minimum cache lifetime, flush all temporary cache entries now.
$query .= "WHERE (expire <> " . CACHE_PERMANENT . " AND expire < " . REQUEST_TIME . ")";
$result = apdqc_query(array(
$this->bin,
), $cids, $query, array(
'async' => TRUE,
));
if (is_string($result) && $result === 'NO DB') {
// Use core connection if an additional connection to mysql fails.
$output = parent::clear($cid, $wildcard);
$this
->callCacheClearHooks($cid, $wildcard);
return $output;
}
}
}
else {
if ($wildcard) {
if ($cid == '*') {
// Check if $this->bin is a cache table before truncating. Other
// cache_clear_all() operations throw a PDO error in this situation,
// so we don't need to verify them first. This ensures that non-cache
// tables cannot be truncated accidentally.
if ($this
->isValidBin()) {
apdqc_truncate_table($this->bin);
}
else {
throw new Exception(t('Invalid or missing cache bin specified: %bin', array(
'%bin' => $this->bin,
)));
}
}
else {
// Build query.
$query = Database::getConnection()
->prefixTables("DELETE FROM {" . db_escape_table($this->bin) . "} ");
db_delete($this->bin)
->condition('cid', db_like($cid) . '%', 'LIKE')
->execute();
$escaped_cid = apdqc_escape_string($cid);
$query .= " WHERE cid LIKE '{$escaped_cid}%'";
// Run an async query.
$result = apdqc_query(array(
$this->bin,
), $cids, $query, array(
'async' => TRUE,
));
if (is_string($result) && $result === 'NO DB') {
// Use core connection if an additional connection to mysql fails.
$output = parent::clear($cid, $wildcard);
$this
->callCacheClearHooks($cid, $wildcard);
return $output;
}
}
}
elseif (is_array($cid)) {
// Delete in chunks when a large array is passed.
$chunks = array_chunk($cid, 2000);
$last = array_pop($chunks);
$query = Database::getConnection()
->prefixTables("DELETE FROM {" . db_escape_table($this->bin) . "} ");
foreach ($chunks as $cids) {
$escaped_cids = array();
foreach ($cids as $id) {
$escaped_cids[] = apdqc_escape_string($id);
}
$escaped_cids = "'" . implode("', '", $escaped_cids) . "'";
// Run query.
$result = apdqc_query(array(
$this->bin,
), $cids, $query . " WHERE cid IN ({$escaped_cids})");
if (is_string($result) && $result === 'NO DB') {
// Use core connection if an additional connection to mysql fails.
$output = parent::clear($cid, $wildcard);
$this
->callCacheClearHooks($cid, $wildcard);
return $output;
}
}
$escaped_cids = array();
foreach ($last as $id) {
$escaped_cids[] = apdqc_escape_string($id);
}
$escaped_cids = "'" . implode("', '", $escaped_cids) . "'";
// Run last one as an async query.
$result = apdqc_query(array(
$this->bin,
), $last, $query . " WHERE cid IN ({$escaped_cids})", array(
'async' => TRUE,
));
if (is_string($result) && $result === 'NO DB') {
// Use core connection if an additional connection to mysql fails.
$output = parent::clear($cid, $wildcard);
$this
->callCacheClearHooks($cid, $wildcard);
return $output;
}
}
else {
$query = Database::getConnection()
->prefixTables("DELETE FROM {" . db_escape_table($this->bin) . "} ");
$result = apdqc_query(array(
$this->bin,
), $cids, $query . " WHERE cid = '" . apdqc_escape_string($cid) . "'", array(
'async' => TRUE,
));
if (is_string($result) && $result === 'NO DB') {
// Use core connection if an additional connection to mysql fails.
$output = parent::clear($cid, $wildcard);
$this
->callCacheClearHooks($cid, $wildcard);
return $output;
}
}
}
$this
->callCacheClearHooks($cid, $wildcard);
}
/**
* Implements DrupalCacheInterface::isEmpty().
*/
public function isEmpty() {
$this
->garbageCollection();
$real_table_name = Database::getConnection()
->prefixTables("{" . db_escape_table($this->bin) . "}");
$result = apdqc_query(array(
$this->bin,
), array(), "SELECT TRUE FROM {$real_table_name} LIMIT 1");
if (is_string($result) && $result === 'NO DB') {
return parent::isEmpty();
}
if (!empty($result) && $result instanceof mysqli_result) {
$empty_table = $result
->fetch_row();
}
if (empty($result) || empty($empty_table)) {
return TRUE;
}
return FALSE;
}
/**
* Checks if $this->bin represents a valid cache table.
*
* This check is required to ensure that non-cache tables are not truncated
* accidentally when calling cache_clear_all().
*
* @see https://www.drupal.org/node/2421253
*
* @return bool
* True if this is a valid bin; false if not.
*/
public function isValidBin() {
return parent::isValidBin();
}
/**
* Allow for 3rd party modules to react to cache clear calls.
*
* @param string $cid
* Cache ID.
* @param bool $wildcard
* If TRUE then anything after the CID is a match.
*/
public function callCacheClearHooks($cid, $wildcard) {
if (!variable_get('apdqc_call_hook_on_clear', APDQC_CALL_HOOK_ON_CLEAR)) {
return;
}
$apdqc_cache_clear = module_implements('apdqc_cache_clear');
if (empty($apdqc_cache_clear)) {
// Bail out if no hooks will be called.
return;
}
$caller = $this
->getCaller();
// Call hook_apdqc_cache_clear().
module_invoke_all('apdqc_cache_clear', $cid, $wildcard, $this->bin, $caller);
}
/**
* Get the last 3 items from debug_backtrace.
*
* @return array
* An array of items from debug_backtrace.
*/
public function getCaller() {
$bt = debug_backtrace();
$caller = array();
foreach ($bt as $key => $call) {
if (!empty($call['class']) && $call['class'] == 'APDQCache') {
continue;
}
if (empty($call['function']) || $call['function'] == 'cache_clear_all') {
continue;
}
// Get this and the next 2 levels from the backtrace.
$caller[$key] = $call;
if (isset($bt[$key + 1])) {
$caller[$key + 1] = $bt[$key + 1];
if (isset($bt[$key + 2])) {
$caller[$key + 2] = $bt[$key + 2];
}
}
break;
}
return $caller;
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
APDQCache:: |
protected | property | ||
APDQCache:: |
protected | property | ||
APDQCache:: |
protected | function | Checks if this cache bin is compressed or not. | |
APDQCache:: |
protected | function | Checks if this cache bin is compressed or not. | |
APDQCache:: |
public | function | Allow for 3rd party modules to react to cache clear calls. | |
APDQCache:: |
protected | function | Cleans up the per-session cache expiration data. | |
APDQCache:: |
public | function |
Implements DrupalCacheInterface::clear(). Overrides DrupalDatabaseCache:: |
|
APDQCache:: |
protected | function |
Generic garbage collection method. Overrides DrupalDatabaseCache:: |
|
APDQCache:: |
public | function | Get the last 3 items from debug_backtrace. | |
APDQCache:: |
public | function |
Implements DrupalCacheInterface::getMultiple(). Overrides DrupalDatabaseCache:: |
|
APDQCache:: |
public | function |
Implements DrupalCacheInterface::isEmpty(). Overrides DrupalDatabaseCache:: |
|
APDQCache:: |
public | function |
Checks if $this->bin represents a valid cache table. Overrides DrupalDatabaseCache:: |
|
APDQCache:: |
protected | function |
Prepares a cached item. Overrides DrupalDatabaseCache:: |
|
APDQCache:: |
public | function |
Implements DrupalCacheInterface::set(). Overrides DrupalDatabaseCache:: |
|
APDQCache:: |
public | function |
Constructs a DrupalDatabaseCache object. Overrides DrupalDatabaseCache:: |
|
DrupalDatabaseCache:: |
protected | property | ||
DrupalDatabaseCache:: |
function |
Implements DrupalCacheInterface::get(). Overrides DrupalCacheInterface:: |
1 |