diff --git a/includes/cache.inc b/includes/cache.inc index 590592d..c528530 100644 --- a/includes/cache.inc +++ b/includes/cache.inc @@ -93,9 +93,30 @@ function _views_fetch_plugin_data($type = NULL, $plugin = NULL, $reset = FALSE) /** * Scan all modules for default views and rebuild the default views cache. * - * @return An associative array of all known default views. + * + * + * @return + * For PHP > 5.0, returns an ArrayObject. For PHP < 5.0 + * returns an array of all default views. */ function _views_discover_default_views($reset = FALSE) { + static $php5; + + // ArrayObject is not available in PHP < 5.0. Therefore this function + // supports separate code paths depending on version. + if (!isset($php5)) { + $php5 = version_compare(PHP_VERSION, '5.0', '>='); + } + if ($php5) { + module_load_include('inc', 'views', 'includes/cache.php5'); + } + else { + module_load_include('inc', 'views', 'includes/cache.php4'); + } + return _views_default_views_cache($reset = FALSE); +} + +function _views_build_default_views($reset = FALSE) { static $cache = NULL; if (!isset($cache) || $reset) { diff --git a/includes/cache.php4.inc b/includes/cache.php4.inc new file mode 100644 index 0000000..734650f --- /dev/null +++ b/includes/cache.php4.inc @@ -0,0 +1,60 @@ +data) && is_array($index->data)) { + $cache = array(); + foreach ($index->data as $view_name) { + $data = views_cache_get('views_default:' . $view_name, TRUE); + if (isset($data->data) && is_object($data->data)) { + $cache[$view_name] = $data->data; + } + } + } + // If missing index, rebuild the cache + else { + views_include_default_views(); + $cache = array(); + + foreach (module_implements('views_default_views') as $module) { + $results = call_user_func($module . "_views_default_views"); + if (!empty($results) && is_array($results)) { + foreach($results as $name => $view) { + // Only views with a sufficiently high api version are eligible. + if (!empty($view->api_version) && $view->api_version >= 2) { + // Do not cache dead handlers. + $view->destroy(); + if (!isset($cache[$name])) { + $cache[$name] = $view; + } + else { + watchdog('view', "View name '@name' is already taken", array('@name' => $name), WATCHDOG_ERROR); + } + } + } + } + } + + // Allow modules to modify default views before they are cached. + drupal_alter('views_default_views', $cache); + + // Cache the index + $index = array_keys($cache); + views_cache_set('views_default_views_index', $index, TRUE); + + // Cache each view + foreach ($cache as $name => $view) { + views_cache_set('views_default:' . $name, $view, TRUE); + } + } + } + + return $cache; +} diff --git a/includes/cache.php5.inc b/includes/cache.php5.inc new file mode 100644 index 0000000..107a709 --- /dev/null +++ b/includes/cache.php5.inc @@ -0,0 +1,309 @@ +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 ViewsCacheArrayObject extends ArrayObject { + + /** + * 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 $keysToSave = array(); + + /** + * Private variable holding a reference to the mysql connection. + * + * The active database connection consistently goes out of scope before + * the destructor gets called. Destroying (or otherwise triggering the + * cache_set() manually in hook_exit() or register_shutdown_function is + * fragile. Therefore as a workaround, add a reference to the $active_db + * object within this class. That keeps the class active until this object + * has been destroed. + * + * @todo: find a better way to deal with this. + */ + private $_connection; + + /** + * Constructor. + * + * @param $cid + * The cid for the array being cached. + * @param $bin + * The bin to cache the array. + */ + public function __construct($cid, $bin) { + $this->_connection = &$GLOBALS['active_db']; + + $this->cid = $cid; + $this->bin = $bin; + + if ($cached = cache_get($this->cid, $this->bin)) { + parent::__construct($cached->data); + } + else { + parent::__construct(array()); + } + } + + public function offsetExists($offset) { + return $this->offsetGet($offset) !== NULL; + } + + public function offsetGet($offset) { + if (!parent::offsetExists($offset)) { + $this->resolveCacheMiss($offset); + } + return parent::offsetGet($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->keysToSave[$offset] = $persist; + } + + public function exchangeArray($input) { + // Reset keysToSave to ensure there is no attempt to cache keys that are + // not part of the cache object. + $this->keysToSave = array(); + return parent::exchangeArray($input); + } + + /** + * 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. + */ + 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_name = $cid . ':' . $bin; + // Since this method merges with the existing cache entry if it exists, + // ensure that only one process can update the cache item at any one time. + // This ensures that different requests can't overwrite each others' + // partial version of the cache and should help to avoid stampedes. + // When a lock cannot be acquired, the cache will not be written by + // that request. To implement locking for cache misses, override + // __construct(). + 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->keysToSave as $offset => $persist) { + if ($persist) { + $data[$offset] = parent::offsetGet($offset); + } + } + if (!empty($data)) { + $this->set($this->cid, $data, $this->bin); + } + } +} + +function _views_default_views_cache($reset = FALSE) { + static $cache; + if ($reset) { + unset($cache); + cache_clear_all('views_default', 'cache_views', TRUE); + } + if (!isset($cache)) { + $cache = new ViewsDefaultViewsCache(); + } + return $cache; +} + +/** + * Extends CacheArrayObject to support lazy-loading of default views. + */ +class ViewsDefaultViewsCache extends ViewsCacheArrayObject { + function __construct() { + $this->_connection = &$GLOBALS['active_db']; + + global $language; + if (variable_get('views_skip_cache', FALSE)) { + return; + } + $cid = 'views_default_views_index:' . $language->lanugage; + parent::__construct($cid, 'cache_views'); + } + + function offsetGet($offset) { + // The ArrayObject storage holds the index of default views. If this is set + // look up the default view from cache. + if (ArrayObject::offsetExists($offset)) { + // If the view is cached, add it to the ArrayObject storage, this will + // retain the view in memory for the rest of the request. + if ($cached = views_cache_get('views_default:' . $offset, TRUE)) { + $this->offsetSet($offset, $cached->data); + } + else { + // The default view exists, but is not cached. + $this->resolveCacheMiss($offset); + } + } + else { + // If the index is populated, but the view is not in there, assume there is no default for this view. + $index = $this->getArrayCopy(); + if (is_array($index) && !empty($index)) { + $this->offsetSet($offset, FALSE); + } + else { + // If the index is not cached, build the index, then re-run this method. + $index = _views_build_default_index(); + $this->exchangeArray($index); + return $this->offsetGet($offset); + } + } + return parent::offsetGet($offset); + } + + function resolveCacheMiss($offset) { + views_include_default_views(); + foreach (module_implements('views_default_views') as $module) { + $results = call_user_func($module . "_views_default_views"); + if (!empty($results) && is_array($results)) { + if (isset($results[$offset])) { + $view = $results[$offset]; + $alter = array($offset => &$view); + drupal_alter('views_default_views', $alter); + if (!empty($view->api_version) && $view->api_version >= 2) { + $this->offsetSet($offset, $view); + $view_clone = clone $view; + // Do not cache dead handlers. + $view_clone->destroy(); + views_cache_set('views_default:' . $offset, $view_clone); + // Nothing more to do here, return. + unset($results); + return; + } + } + } + } + // If no view was eligible, cache FALSE. + $this->offsetSet($offset, FALSE); + views_cache_set('views_default:' . $offset, FALSE); + } + + function __destruct() { + // Nothing to do. + return; + } +} + +/** + * Build an index of default views. + * + * @return + * An array with both the keys and values as view names. + */ +function _views_build_default_index() { + $index = array(); + views_include_default_views(); + foreach (module_implements('views_default_views') as $module) { + $results = call_user_func($module . '_views_default_views'); + if (!empty($results) && is_array($results)) { + foreach ($results as $name => $view) { + if (!empty($view->api_version) && $view->api_version >= 2) { + $view->destroy(); + if (!isset($index[$name])) { + $index[$name] = $name; + } + else { + watchdog('view', "View name '@name' is already taken", array('@name' => $name), WATCHDOG_ERROR); + } + } + } + } + } + return $index; +}