diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index c7c484f8315d06e04ed807a4bd694fdc370e9f68..43cbb1d854131962f54f9f1c45ff8add14abf73d 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -225,6 +225,195 @@ */ define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'); +/** + * Provides a caching wrapper to be used in place of large array structures. + * + * This class should be extended by systems that need to cache large amounts + * of data and have it represented as an array to calling functions. These + * arrays can become very large, so ArrayAccess is used to allow different + * strategies to be used for caching internally (lazy loading, building caches + * over time etc.). This can dramatically reduce the amount of data that needs + * to be loaded from cache backends on each request, and memory usage from + * static caches of that same data. + * + * Note that array_* functions do not work with ArrayAccess. Systems using + * DrupalCacheArray should use this only internally. If providing API functions + * that return the full array, this can be cached separately or returned + * directly. However since DrupalCacheArray holds partial content by design, it + * should be a normal PHP array or otherwise contain the full structure. + * + * Note also that due to limitations in PHP prior to 5.3.4, it is impossible to + * write directly to the contents of nested arrays contained in this object. + * Only writes to the top-level array elements are possible. So if you + * previously had set $object['foo'] = array(1, 2, 'bar' => 'baz'), but later + * want to change the value of 'bar' from 'baz' to 'foobar', you cannot do so + * a targeted write like $object['foo']['bar'] = 'foobar'. Instead, you must + * overwrite the entire top-level 'foo' array with the entire set of new + * values: $object['foo'] = array(1, 2, 'bar' => 'foobar'). Due to this same + * limitation, attempts to create references to any contained data, nested or + * otherwise, will fail silently. So $var = &$object['foo'] will not throw an + * error, and $var will be populated with the contents of $object['foo'], but + * that data will be passed by value, not reference. For more information on + * the PHP limitation, see the note in the official PHP documentation at· + * http://php.net/manual/en/arrayaccess.offsetget.php on + * ArrayAccess::offsetGet(). + * + * By default, the class accounts for caches where calling functions might + * request keys in the array that won't exist even after a cache rebuild. This + * prevents situations where a cache rebuild would be triggered over and over + * due to a 'missing' item. These cases are stored internally as a value of + * NULL. This means that the offsetGet() and offsetExists() methods + * must be overridden if caching an array where the top level values can + * legitimately be NULL, and where $object->offsetExists() needs to correctly + * return (equivalent to array_key_exists() vs. isset()). This should not + * be necessary in the majority of cases. + * + * Classes extending this class must override at least the + * resolveCacheMiss() method to have a working implementation. + * + * offsetSet() is not overridden by this class by default. In practice this + * means that assigning an offset via arrayAccess will only apply while the + * object is in scope and will not be written back to the persistent cache. + * This follows a similar pattern to static vs. persistent caching in + * procedural code. Extending classes may wish to alter this behaviour, for + * example by overriding offsetSet() and adding an automatic call to persist(). + * + * @see SchemaCache + */ +abstract class DrupalCacheArray implements ArrayAccess { + + /** + * A cid to pass to cache_set() and cache_get(). + */ + private $cid; + + /** + * A bin to pass to cache_set() and cache_get(). + */ + private $bin; + + /** + * An array of keys to add to the cache at the end of the request. + */ + protected $keysToPersist = array(); + + /** + * Storage for the data itself. + */ + protected $storage = array(); + + /** + * Constructor. + * + * @param $cid + * The cid for the array being cached. + * @param $bin + * The bin to cache the array. + */ + public function __construct($cid, $bin) { + $this->cid = $cid; + $this->bin = $bin; + + if ($cached = cache_get($this->cid, $this->bin)) { + $this->storage = $cached->data; + } + } + + public function offsetExists($offset) { + return $this->offsetGet($offset) !== NULL; + } + + public function offsetGet($offset) { + if (isset($this->storage[$offset]) || array_key_exists($offset, $this->storage)) { + return $this->storage[$offset]; + } + else { + return $this->resolveCacheMiss($offset); + } + } + + public function offsetSet($offset, $value) { + $this->storage[$offset] = $value; + } + + public function offsetUnset($offset) { + unset($this->storage[$offset]); + } + + /** + * Flags an offset value to be written to the persistent cache. + * + * If a value is assigned to a cache object with offsetSet(), by default it + * will not be written to the persistent cache unless it is flagged with this + * method. This allows items to be cached for the duration of a request, + * without necessarily writing back to the persistent cache at the end. + * + * @param $offset + * The array offset that was request. + * @param $persist + * Optional boolean to specify whether the offset should be persisted or + * not, defaults to TRUE. When called with $persist = FALSE the offset will + * be unflagged so that it will not written at the end of the request. + */ + protected function persist($offset, $persist = TRUE) { + $this->keysToPersist[$offset] = $persist; + } + + /** + * Resolves a cache miss. + * + * When an offset is not found in the object, this is treated as a cache + * miss. This method allows classes implementing the interface to look up + * the actual value and allow it to be cached. + * + * @param $offset + * The offset that was requested. + * + * @return + * The value of the offset, or NULL if no value was found. + */ + abstract protected function resolveCacheMiss($offset); + + /** + * Immediately write a value to the persistent cache. + * + * @param $cid + * The cache ID. + * @param $bin + * The cache bin. + * @param $data + * The data to write to the persistent cache. + * @param $lock + * Whether to acquire a lock before writing to cache. + */ + protected function set($cid, $data, $bin, $lock = TRUE) { + // Lock cache writes to help avoid stampedes. + // To implement locking for cache misses, override __construct(). + $lock_name = $cid . ':' . $bin; + if (!$lock || lock_acquire($lock_name)) { + if ($cached = cache_get($cid, $bin)) { + $data = $cached->data + $data; + } + cache_set($cid, $data, $bin); + if ($lock) { + lock_release($lock_name); + } + } + } + + public function __destruct() { + $data = array(); + foreach ($this->keysToPersist as $offset => $persist) { + if ($persist) { + $data[$offset] = $this->storage[$offset]; + } + } + if (!empty($data)) { + $this->set($this->cid, $data, $this->bin); + } + } +} + /** * Start the timer with the specified name. If you start and stop the same * timer multiple times, the measured intervals will be accumulated. @@ -2532,6 +2721,55 @@ function ip_address() { * If true, the schema will be rebuilt instead of retrieved from the cache. */ function drupal_get_schema($table = NULL, $rebuild = FALSE) { + static $schema; + + if ($rebuild || !isset($table)) { + $schema = drupal_get_complete_schema($rebuild); + } + elseif (!isset($schema)) { + $schema = new SchemaCache(); + } + + if (!isset($table)) { + return $schema; + } + if (isset($schema[$table])) { + return $schema[$table]; + } + else { + return FALSE; + } +} + +/** + * Extends DrupalCacheArray to allow for dynamic building of the schema cache. + */ +class SchemaCache extends DrupalCacheArray { + + public function __construct() { + // Cache by request method. + parent::__construct('schema:runtime:' . $_SERVER['REQUEST_METHOD'] == 'GET', 'cache'); + } + + protected function resolveCacheMiss($offset) { + $complete_schema = drupal_get_complete_schema(); + $value = isset($complete_schema[$offset]) ? $complete_schema[$offset] : NULL; + $this->storage[$offset] = $value; + $this->persist($offset); + return $value; + } +} + +/** + * Get the whole database schema. + * + * The returned schema will include any modifications made by any + * module that implements hook_schema_alter(). + * + * @param $rebuild + * If true, the schema will be rebuilt instead of retrieved from the cache. + */ +function drupal_get_complete_schema($rebuild = FALSE) { static $schema = array(); if (empty($schema) || $rebuild) { @@ -2573,18 +2811,13 @@ function drupal_get_schema($table = NULL, $rebuild = FALSE) { if (!empty($schema) && (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL)) { cache_set('schema', $schema); } + if ($rebuild) { + cache_clear_all('schema:', 'cache', TRUE); + } } } - if (!isset($table)) { - return $schema; - } - elseif (isset($schema[$table])) { - return $schema[$table]; - } - else { - return FALSE; - } + return $schema; } /**