? caching.patch ? views_2_caching.patch Index: views.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/views.install,v retrieving revision 1.47 diff -u -p -r1.47 views.install --- views.install 2 Jun 2009 18:08:40 -0000 1.47 +++ views.install 2 Jun 2009 20:11:48 -0000 @@ -126,6 +126,11 @@ function views_schema_1() { ); $schema['cache_views'] = drupal_get_schema_unprocessed('system', 'cache'); + + $schema['cache_views_data'] = drupal_get_schema_unprocessed('system', 'cache'); + $schema['cache_views_data']['description'] = 'Cache table for views to store pre-rendered queries, results, and display output.'; + $schema['cache_views_data']['fields']['serialized']['default'] = 1; + $schema['views_object_cache'] = array( 'description' => 'A special cache used to store objects that are being edited; it serves to save state in an ordinarily stateless environment.', @@ -256,3 +261,18 @@ function views_update_6005() { db_change_field($ret, 'views_view', 'base_table', 'base_table', $new_field); return $ret; } + +/** + * Add the cache_views_data table to support standard caching. + */ +function views_update_6006() { + $ret = array(); + + $table = drupal_get_schema_unprocessed('system', 'cache'); + $table['description'] = 'Cache table for views to store pre-rendered queries, results, and display output.'; + $table['fields']['serialized']['default'] = 1; + + db_create_table($ret, 'cache_views_data', $table); + + return $ret; +} Index: views.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/views.module,v retrieving revision 1.334 diff -u -p -r1.334 views.module --- views.module 1 Jun 2009 23:20:17 -0000 1.334 +++ views.module 2 Jun 2009 20:11:48 -0000 @@ -397,7 +397,7 @@ function views_block($op = 'list', $delt * Implementation of hook_flush_caches(). */ function views_flush_caches() { - return array('cache_views'); + return array('cache_views', 'cache_views_data'); } /** Index: includes/plugins.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/plugins.inc,v retrieving revision 1.153 diff -u -p -r1.153 plugins.inc --- includes/plugins.inc 1 Jun 2009 23:34:55 -0000 1.153 +++ includes/plugins.inc 2 Jun 2009 20:11:48 -0000 @@ -235,6 +235,26 @@ function views_views_plugins() { 'help topic' => 'access-perm', ), ), + 'cache' => array( + 'parent' => array( + 'no ui' => TRUE, + 'handler' => 'views_plugin_cache', + 'parent' => '', + ), + 'none' => array( + 'title' => t('None'), + 'help' => t('No caching of Views data.'), + 'handler' => 'views_plugin_cache_none', + 'help topic' => 'cache-none', + ), + 'time' => array( + 'title' => t('Time-based'), + 'help' => t('Simple time-based caching of data.'), + 'handler' => 'views_plugin_cache_time', + 'uses options' => TRUE, + 'help topic' => 'cache-time', + ), + ), ); } @@ -244,7 +264,7 @@ function views_views_plugins() { * @return Nested array of plugins, grouped by type. */ function views_discover_plugins() { - $cache = array('display' => array(), 'style' => array(), 'row' => array(), 'argument default' => array(), 'argument validator' => array(), 'access' => array()); + $cache = array('display' => array(), 'style' => array(), 'row' => array(), 'argument default' => array(), 'argument validator' => array(), 'access' => array(), 'cache' => array()); // Get plugins from all mdoules. foreach (module_implements('views_plugins') as $module) { $function = $module . '_views_plugins'; Index: includes/view.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/view.inc,v retrieving revision 1.153 diff -u -p -r1.153 view.inc --- includes/view.inc 2 Jun 2009 19:38:33 -0000 1.153 +++ includes/view.inc 2 Jun 2009 20:11:48 -0000 @@ -689,72 +689,88 @@ class view extends views_db_object { vpr($query); - $items = array(); - if ($query) { - $replacements = module_invoke_all('views_query_substitutions', $this); - $query = str_replace(array_keys($replacements), $replacements, $query); - $count_query = 'SELECT COUNT(*) FROM (' . str_replace(array_keys($replacements), $replacements, $count_query) . ') count_alias'; - - if (is_array($args)) { - foreach ($args as $id => $arg) { - $args[$id] = str_replace(array_keys($replacements), $replacements, $arg); - } - } - // Allow for a view to query an external database. - if (isset($this->base_database)) { - db_set_active($this->base_database); - $external = TRUE; - } - - $start = views_microtime(); - if (!empty($this->pager['items_per_page'])) { - // We no longer use pager_query() here because pager_query() does not - // support an offset. This is fine as we don't actually need pager - // query; we've already been doing most of what it does, and we - // just need to do a little more playing with globals. - if (!empty($this->pager['use_pager']) || !empty($this->get_total_rows)) { - $this->total_rows = db_result(db_query($count_query, $args)) - $this->pager['offset']; - } - - if (!empty($this->pager['use_pager'])) { - // dump information about what we already know into the globals - global $pager_page_array, $pager_total, $pager_total_items; - // total rows in query - $pager_total_items[$this->pager['element']] = $this->total_rows; - // total pages - $pager_total[$this->pager['element']] = ceil($pager_total_items[$this->pager['element']] / $this->pager['items_per_page']); - - // What page was requested: - $pager_page_array = isset($_GET['page']) ? explode(',', $_GET['page']) : array(); - - // If the requested page was within range. $this->pager['current_page'] - // defaults to 0 so we don't need to set it in an out-of-range condition. - if (!empty($pager_page_array[$this->pager['element']])) { - $page = intval($pager_page_array[$this->pager['element']]); - if ($page > 0 && $page < $pager_total[$this->pager['element']]) { - $this->pager['current_page'] = $page; + // Check for already-cached results. + if ($this->preview) { + $cache = FALSE; + } + else { + $cache = $this->display_handler->get_cache_plugin(); + } + if ($cache && $cache->cache_get('results')) { + vpr('Used cached results'); + } + else { + $items = array(); + if ($query) { + $replacements = module_invoke_all('views_query_substitutions', $this); + $query = str_replace(array_keys($replacements), $replacements, $query); + $count_query = 'SELECT COUNT(*) FROM (' . str_replace(array_keys($replacements), $replacements, $count_query) . ') count_alias'; + + if (is_array($args)) { + foreach ($args as $id => $arg) { + $args[$id] = str_replace(array_keys($replacements), $replacements, $arg); + } + } + + // Allow for a view to query an external database. + if (isset($this->base_database)) { + db_set_active($this->base_database); + $external = TRUE; + } + + $start = views_microtime(); + if (!empty($this->pager['items_per_page'])) { + // We no longer use pager_query() here because pager_query() does not + // support an offset. This is fine as we don't actually need pager + // query; we've already been doing most of what it does, and we + // just need to do a little more playing with globals. + if (!empty($this->pager['use_pager']) || !empty($this->get_total_rows)) { + $this->total_rows = db_result(db_query($count_query, $args)) - $this->pager['offset']; + } + + if (!empty($this->pager['use_pager'])) { + // dump information about what we already know into the globals + global $pager_page_array, $pager_total, $pager_total_items; + // total rows in query + $pager_total_items[$this->pager['element']] = $this->total_rows; + // total pages + $pager_total[$this->pager['element']] = ceil($pager_total_items[$this->pager['element']] / $this->pager['items_per_page']); + + // What page was requested: + $pager_page_array = isset($_GET['page']) ? explode(',', $_GET['page']) : array(); + + // If the requested page was within range. $this->pager['current_page'] + // defaults to 0 so we don't need to set it in an out-of-range condition. + if (!empty($pager_page_array[$this->pager['element']])) { + $page = intval($pager_page_array[$this->pager['element']]); + if ($page > 0 && $page < $pager_total[$this->pager['element']]) { + $this->pager['current_page'] = $page; + } } + $pager_page_array[$this->pager['element']] = $this->pager['current_page']; } - $pager_page_array[$this->pager['element']] = $this->pager['current_page']; + + $offset = $this->pager['current_page'] * $this->pager['items_per_page'] + $this->pager['offset']; + $result = db_query_range($query, $args, $offset, $this->pager['items_per_page']); + } - - $offset = $this->pager['current_page'] * $this->pager['items_per_page'] + $this->pager['offset']; - $result = db_query_range($query, $args, $offset, $this->pager['items_per_page']); - - } - else { - $result = db_query($query, $args); - } - - $this->result = array(); - while ($item = db_fetch_object($result)) { - $this->result[] = $item; + else { + $result = db_query($query, $args); + } + + $this->result = array(); + while ($item = db_fetch_object($result)) { + $this->result[] = $item; + } + if (!empty($external)) { + db_set_active(); + } + $this->execute_time = views_microtime() - $start; } - if (!empty($external)) { - db_set_active(); + if ($cache) { + $cache->cache_set('results'); } - $this->execute_time = views_microtime() - $start; } $this->executed = TRUE; @@ -764,9 +780,6 @@ class view extends views_db_object { * Render this view for display. */ function render($display_id = NULL) { - // Check for cached output. - // @todo: Implement this - $this->execute($display_id); // Check to see if the build failed. @@ -778,34 +791,50 @@ class view extends views_db_object { if (!empty($this->live_preview) && variable_get('views_show_additional_queries', FALSE)) { $this->start_query_capture(); } - - // Initialize the style plugin. - $this->init_style(); - - $this->style_plugin->pre_render($this->result); - - // Let modules modify the view just prior to executing it. - foreach (module_implements('views_pre_render') as $module) { - $function = $module . '_views_pre_render'; - $function($this); + + // Check for already-cached output. + if ($this->preview) { + $cache = FALSE; } - - // Give field handlers the opportunity to perform additional queries - // using the entire resultset prior to rendering. - if ($this->style_plugin->uses_fields()) { - foreach ($this->field as $id => $handler) { - if (!empty($this->field[$id])) { - $this->field[$id]->pre_render($this->result); + else { + $cache = $this->display_handler->get_cache_plugin(); + } + if ($cache && $cache->cache_get('output')) { + vpr('Used cached output'); + } + else { + // Initialize the style plugin. + $this->init_style(); + + $this->style_plugin->pre_render($this->result); + + // Let modules modify the view just prior to executing it. + foreach (module_implements('views_pre_render') as $module) { + $function = $module . '_views_pre_render'; + $function($this); + } + + // Give field handlers the opportunity to perform additional queries + // using the entire resultset prior to rendering. + if ($this->style_plugin->uses_fields()) { + foreach ($this->field as $id => $handler) { + if (!empty($this->field[$id])) { + $this->field[$id]->pre_render($this->result); + } } } + $this->display_handler->output = $this->display_handler->render(); + if ($cache) { + $cache->cache_set('output'); + } } - $output = $this->display_handler->render(); if (!empty($this->live_preview) && variable_get('views_show_additional_queries', FALSE)) { $this->end_query_capture(); } $this->render_time = views_microtime() - $start; - return $output; + + return $this->display_handler->output; } /** Index: plugins/views_plugin_cache.inc =================================================================== RCS file: plugins/views_plugin_cache.inc diff -N plugins/views_plugin_cache.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ plugins/views_plugin_cache.inc 2 Jun 2009 20:11:49 -0000 @@ -0,0 +1,148 @@ +view = &$view; + $this->display = &$display; + $this->options = array(); + + if (is_object($display->handler)) { + // Note: The below is read only. + $this->options = $display->handler->get_option('cache'); + } + } + + /** + * Retrieve the default options when this is a new access + * control plugin + */ + function option_defaults(&$options) { } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * access control. + */ + function summary_title() { + return t('Unknown'); + } + + /** + * Save data to the cache. + */ + function cache_set($type, $data = NULL) { } + + /** + * Retrieve data from the cache. + */ + function cache_get($type) { + return FALSE; + } + + /** + * Clear out cached data for the view. + */ + function cache_flush() { } + + + + /** + * Set out-of-band-data for caching. Copied from Panels. + */ + function gather_headers() { + // Simple replacement for head + $this->head = str_replace($this->head, '', drupal_set_html_head()); + + // Slightly less simple for CSS: + $css = drupal_add_css(); + $start = $this->css; + $this->css = array(); + + foreach ($css as $media => $medias) { + foreach ($medias as $type => $types) { + foreach ($types as $path => $preprocess) { + if (!isset($start[$media][$type][$path])) { + $this->css[] = array($path, $type, $media, $preprocess); + } + } + } + } + + $js = array(); + // A little less simple for js + foreach (array('header', 'footer') as $scope) { + $js[$scope] = drupal_add_js(NULL, NULL, $scope); + } + + $start = $this->js; + $this->js = array(); + + foreach ($js as $scope => $scopes) { + foreach ($scopes as $type => $types) { + foreach ($types as $id => $info) { + if (!isset($start[$scope][$type][$id])) { + switch ($type) { + case 'setting': + $this->js[] = array($info, $type, $scope); + break; + + case 'inline': + $this->js[] = array($info['code'], $type, $scope, $info['defer']); + break; + + default: + $this->js[] = array($id, $type, $scope, $info['defer'], $info['cache']); + } + } + } + } + } + } + + /** + * Restore out of band data saved to cache. Copied from Panels. + */ + function restore_headers() { + if (!empty($this->head)) { + drupal_set_html_head($this->head); + } + if (!empty($this->css)) { + foreach ($this->css as $args) { + call_user_func_array('drupal_add_css', $args); + } + } + if (!empty($this->js)) { + foreach ($this->js as $args) { + call_user_func_array('drupal_add_js', $args); + } + } + } +} Index: plugins/views_plugin_cache_none.inc =================================================================== RCS file: plugins/views_plugin_cache_none.inc diff -N plugins/views_plugin_cache_none.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ plugins/views_plugin_cache_none.inc 2 Jun 2009 20:11:49 -0000 @@ -0,0 +1,11 @@ +view = &$view; + $this->display = &$display; + $this->options = array(); + + if (is_object($display->handler)) { + // Note: The below is read only. + $this->options = $display->handler->get_option('cache'); + } + } + + /** + * Retrieve the default options when this is a new access + * control plugin + */ + function option_defaults(&$options) { + $options['results_lifespan'] = 3600; + $options['output_lifespan'] = 3600; + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + $options = array(60, 300, 1800, 3600, 21600, 518400); + $options = drupal_map_assoc($options, 'format_interval'); + $options = array(-1 => t('Never cache')) + $options; + + $form['results_lifespan'] = array( + '#type' => 'select', + '#title' => t('Query results'), + '#description' => t('The length of time raw query results should be cached.'), + '#options' => $options, + '#default_value' => $this->options['results_lifespan'], + ); + $form['output_lifespan'] = array( + '#type' => 'select', + '#title' => t('Rendered output'), + '#description' => t('The length of time rendered HTML output should be cached.'), + '#options' => $options, + '#default_value' => $this->options['output_lifespan'], + ); + } + + /** + * Provide the default form form for validating options + */ + function options_validate(&$form, &$form_state) { } + + /** + * Provide the default form form for submitting options + */ + function options_submit(&$form, &$form_state) { } + + /** + * Return a string to display as the clickable title for the + * access control. + */ + function summary_title() { + return format_interval($this->options['results_lifespan'], 1) .'/'. format_interval($this->options['output_lifespan'], 1); + } + + /** + * Save data to the cache. + */ + function cache_set($type) { + if ($lifespan = $this->options[$type .'_lifespan']) { + $cutoff = time() - $lifespan; + } + else { + return FALSE; + } + + switch ($type) { + case 'query': + // Not supported currently, but this is certainly where we'd put it. + break; + case 'results': + $data = array( + 'result' => $this->view->result, + 'total_rows' => $this->view->total_rows, + 'pager' => $this->view->pager, + ); + cache_set($this->_results_key(), $data, 'cache_views_data'); + break; + case 'output': + $this->gather_headers(); + $data = array( + 'head' => $this->head, + 'css' => $this->css, + 'js' => $this->js, + 'output' => $this->view->display_handler->output, + ); + cache_set($this->_output_key(), $data, 'cache_views_data'); + break; + } + } + + /** + * Retrieve data from the cache. + */ + function cache_get($type) { + if ($lifespan = $this->options[$type .'_lifespan']) { + $cutoff = time() - $lifespan; + } + else { + return FALSE; + } + + switch ($type) { + case 'query': + // Not supported currently, but this is certainly where we'd put it. + return FALSE; + case 'results': + // Values to set: $view->result, $view->total_rows, $view->execute_time, + // $view->pager['current_page']. + if ($cache = cache_get($this->_results_key(), 'cache_views_data')) { + if ($cache->created > $cutoff) { + $this->view->result = $cache->data['result']; + $this->view->total_rows = $cache->data['total_rows']; + $this->view->pager = $cache->data['pager']; + $this->view->execute_time = 0; + return TRUE; + } + } + return FALSE; + case 'output': + if ($cache = cache_get($this->_output_key(), 'cache_views_data')) { + if ($cache->created > $cutoff) { + $this->view->display_handler->output = $cache->data['output']; + $this->head = $cache->data['head']; + $this->css = $cache->data['css']; + $this->js = $cache->data['js']; + $this->restore_headers(); + return TRUE; + } + } + return FALSE; + } + } + + /** + * Clear out cached data for a view. + * + * We're jsut going to nuke anything related to the view, regardless of display, + * to be sure that we catch everything. Maybe that's a bad idea. + */ + function cache_flush() { + cache_clear_all($this->view->name .':', 'cache_views_data', TRUE); + } + + function _results_key() { + $key_data = array( + 'build_info' => $this->view->build_info, + 'exposed_info' => $this->view->exposed_info, + 'page' => $_GET['page'], + ); + return $this->view->name .':'. $this->display->id .':results:'. md5(serialize($key_data)); + } + + function _output_key() { + $key_data = array( + 'result' => $this->view->result, + ); + return $this->view->name .':'. $this->display->id .':output:'. md5(serialize($key_data)); + } +} Index: plugins/views_plugin_display.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/plugins/views_plugin_display.inc,v retrieving revision 1.21 diff -u -p -r1.21 views_plugin_display.inc --- plugins/views_plugin_display.inc 20 May 2009 02:53:30 -0000 1.21 +++ plugins/views_plugin_display.inc 2 Jun 2009 20:11:49 -0000 @@ -148,6 +148,7 @@ class views_plugin_display extends views function defaultable_sections($section = NULL) { $sections = array( 'access' => array('access'), + 'cache' => array('cache'), 'title' => array('title'), 'header' => array('header', 'header_format', 'header_empty'), 'footer' => array('footer', 'footer_format', 'footer_empty'), @@ -212,6 +213,7 @@ class views_plugin_display extends views 'defaults' => array( 'default' => array( 'access' => TRUE, + 'cache' => TRUE, 'title' => TRUE, 'header' => TRUE, 'header_format' => TRUE, @@ -270,6 +272,11 @@ class views_plugin_display extends views 'type' => array('default' => 'none'), ), ), + 'cache' => array( + 'contains' => array( + 'type' => array('default' => 'none'), + ), + ), 'title' => array( 'default' => '', 'translatable' => TRUE, @@ -482,6 +489,22 @@ class views_plugin_display extends views } /** + * Get the cache plugin + */ + function get_cache_plugin($name = NULL) { + if (!$name) { + $cache = $this->get_option('cache'); + $name = $cache['type']; + } + + $plugin = views_get_plugin('cache', $name); + if ($plugin) { + $plugin->init($this->view, $this->display); + return $plugin; + } + } + + /** * Get the handler object for a single handler. */ function &get_handler($type, $id) { @@ -687,6 +710,25 @@ class views_plugin_display extends views $options['access']['links']['access_options'] = t('Change settings for this access type.'); } + $cache_plugin = $this->get_cache_plugin(); + if (!$cache_plugin) { + // default to the no cache control plugin. + $cache_plugin = views_get_plugin('cache', 'none'); + } + + $cache_str = $cache_plugin->summary_title(); + + $options['cache'] = array( + 'category' => 'basic', + 'title' => t('Caching'), + 'value' => $cache_str, + 'desc' => t('Specify caching type for this display.'), + ); + + if (!empty($cache_plugin->definition['uses options'])) { + $options['cache']['links']['cache_options'] = t('Change settings for this caching type.'); + } + if ($this->uses_link_display()) { // Only show the 'link display' if there is more than one option. $count = 0; @@ -893,6 +935,47 @@ class views_plugin_display extends views $plugin->options_form($form['access_options'], $form_state); } break; + case 'cache': + $form['#title'] .= t('Caching'); + $form['cache'] = array( + '#prefix' => '