Commit 14f0f455 authored by catch's avatar catch
Browse files

Issue #2443073 by Wim Leers, joshtaylor: Add #cache[max-age] to disable...

Issue #2443073 by Wim Leers, joshtaylor: Add #cache[max-age] to disable caching and bubble the max-age
parent b9045f6f
...@@ -466,14 +466,7 @@ public function inheritCacheability(AccessResultInterface $other) { ...@@ -466,14 +466,7 @@ public function inheritCacheability(AccessResultInterface $other) {
$this->setCacheable($other->isCacheable()); $this->setCacheable($other->isCacheable());
$this->addCacheContexts($other->getCacheContexts()); $this->addCacheContexts($other->getCacheContexts());
$this->addCacheTags($other->getCacheTags()); $this->addCacheTags($other->getCacheTags());
// Use the lowest max-age. $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
if ($this->getCacheMaxAge() === Cache::PERMANENT) {
// The other max-age is either lower or equal.
$this->setCacheMaxAge($other->getCacheMaxAge());
}
else {
$this->setCacheMaxAge(min($this->getCacheMaxAge(), $other->getCacheMaxAge()));
}
} }
// If any of the access results don't provide cacheability metadata, then // If any of the access results don't provide cacheability metadata, then
// we cannot cache the combined access result, for we may not make // we cannot cache the combined access result, for we may not make
......
...@@ -70,6 +70,36 @@ public static function mergeTags() { ...@@ -70,6 +70,36 @@ public static function mergeTags() {
return $cache_tags; return $cache_tags;
} }
/**
* Merges max-age values (expressed in seconds), finds the lowest max-age.
*
* Ensures infinite max-age (Cache::PERMANENT) is taken into account.
*
* @param int …
* Max-age values.
*
* @return int
* The minimum max-age value.
*/
public static function mergeMaxAges() {
$max_ages = func_get_args();
// Filter out all max-age values set to cache permanently.
if (in_array(Cache::PERMANENT, $max_ages)) {
$max_ages = array_filter($max_ages, function ($max_age) {
return $max_age !== Cache::PERMANENT;
});
// If nothing is left, then all max-age values were set to cache
// permanently, and then that is the result.
if (empty($max_ages)) {
return Cache::PERMANENT;
}
}
return min($max_ages);
}
/** /**
* Validates an array of cache tags. * Validates an array of cache tags.
* *
......
...@@ -22,47 +22,35 @@ class BubbleableMetadata { ...@@ -22,47 +22,35 @@ class BubbleableMetadata {
* *
* @var string[] * @var string[]
*/ */
protected $contexts; protected $contexts = [];
/** /**
* Cache tags. * Cache tags.
* *
* @var string[] * @var string[]
*/ */
protected $tags; protected $tags = [];
/** /**
* Attached assets. * Cache max-age.
* *
* @var string[][] * @var int
*/ */
protected $attached; protected $maxAge = Cache::PERMANENT;
/** /**
* #post_render_cache metadata. * Attached assets.
* *
* @var array[] * @var string[][]
*/ */
protected $postRenderCache; protected $attached = [];
/** /**
* Constructs a BubbleableMetadata value object. * #post_render_cache metadata.
* *
* @param string[] $contexts * @var array[]
* An array of cache contexts.
* @param string[] $tags
* An array of cache tags.
* @param array $attached
* An array of attached assets.
* @param array $post_render_cache
* An array of #post_render_cache metadata.
*/ */
public function __construct(array $contexts = [], array $tags = [], array $attached = [], array $post_render_cache = []) { protected $postRenderCache = [];
$this->contexts = $contexts;
$this->tags = $tags;
$this->attached = $attached;
$this->postRenderCache = $post_render_cache;
}
/** /**
* Merges the values of another bubbleable metadata object with this one. * Merges the values of another bubbleable metadata object with this one.
...@@ -81,6 +69,7 @@ public function merge(BubbleableMetadata $other) { ...@@ -81,6 +69,7 @@ public function merge(BubbleableMetadata $other) {
$result = new BubbleableMetadata(); $result = new BubbleableMetadata();
$result->contexts = Cache::mergeContexts($this->contexts, $other->contexts); $result->contexts = Cache::mergeContexts($this->contexts, $other->contexts);
$result->tags = Cache::mergeTags($this->tags, $other->tags); $result->tags = Cache::mergeTags($this->tags, $other->tags);
$result->maxAge = Cache::mergeMaxAges($this->maxAge, $other->maxAge);
$result->attached = Renderer::mergeAttachments($this->attached, $other->attached); $result->attached = Renderer::mergeAttachments($this->attached, $other->attached);
$result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache); $result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
return $result; return $result;
...@@ -95,6 +84,7 @@ public function merge(BubbleableMetadata $other) { ...@@ -95,6 +84,7 @@ public function merge(BubbleableMetadata $other) {
public function applyTo(array &$build) { public function applyTo(array &$build) {
$build['#cache']['contexts'] = $this->contexts; $build['#cache']['contexts'] = $this->contexts;
$build['#cache']['tags'] = $this->tags; $build['#cache']['tags'] = $this->tags;
$build['#cache']['max-age'] = $this->maxAge;
$build['#attached'] = $this->attached; $build['#attached'] = $this->attached;
$build['#post_render_cache'] = $this->postRenderCache; $build['#post_render_cache'] = $this->postRenderCache;
} }
...@@ -111,9 +101,185 @@ public static function createFromRenderArray(array $build) { ...@@ -111,9 +101,185 @@ public static function createFromRenderArray(array $build) {
$meta = new static(); $meta = new static();
$meta->contexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : []; $meta->contexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : [];
$meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : []; $meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : [];
$meta->maxAge = (isset($build['#cache']['max-age'])) ? $build['#cache']['max-age'] : Cache::PERMANENT;
$meta->attached = (isset($build['#attached'])) ? $build['#attached'] : []; $meta->attached = (isset($build['#attached'])) ? $build['#attached'] : [];
$meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : []; $meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : [];
return $meta; return $meta;
} }
/**
* Gets cache tags.
*
* @return string[]
*/
public function getCacheTags() {
return $this->tags;
}
/**
* Adds cache tags.
*
* @param string[] $cache_tags
* The cache tags to be added.
*
* @return $this
*/
public function addCacheTags(array $cache_tags) {
$this->tags = Cache::mergeTags($this->tags, $cache_tags);
return $this;
}
/**
* Sets cache tags.
*
* @param string[] $cache_tags
* The cache tags to be associated.
*
* @return $this
*/
public function setCacheTags(array $cache_tags) {
$this->tags = $cache_tags;
return $this;
}
/**
* Gets cache contexts.
*
* @return string[]
*/
public function getCacheContexts() {
return $this->contexts;
}
/**
* Adds cache contexts.
*
* @param string[] $cache_contexts
* The cache contexts to be added.
*
* @return $this
*/
public function addCacheContexts(array $cache_contexts) {
$this->contexts = Cache::mergeContexts($this->contexts, $cache_contexts);
return $this;
}
/**
* Sets cache contexts.
*
* @param string[] $cache_contexts
* The cache contexts to be associated.
*
* @return $this
*/
public function setCacheContexts(array $cache_contexts) {
$this->contexts = $cache_contexts;
return $this;
}
/**
* Gets the maximum age (in seconds).
*
* @return int
*/
public function getCacheMaxAge() {
return $this->maxAge;
}
/**
* Sets the maximum age (in seconds).
*
* Defaults to Cache::PERMANENT
*
* @param int $max_age
* The max age to associate.
*
* @return $this
*
* @throws \InvalidArgumentException
*/
public function setCacheMaxAge($max_age) {
if (!is_int($max_age)) {
throw new \InvalidArgumentException('$max_age must be an integer');
}
$this->maxAge = $max_age;
return $this;
}
/**
* Gets assets.
*
* @return array
*/
public function getAssets() {
return $this->attached;
}
/**
* Adds assets.
*
* @param array $assets
* The associated assets to be attached.
*
* @return $this
*/
public function addAssets(array $assets) {
$this->attached = NestedArray::mergeDeep($this->attached, $assets);
return $this;
}
/**
* Sets assets.
*
* @param array $assets
* The associated assets to be attached.
*
* @return $this
*/
public function setAssets(array $assets) {
$this->attached = $assets;
return $this;
}
/**
* Gets #post_render_cache callbacks.
*
* @return array
*/
public function getPostRenderCacheCallbacks() {
return $this->postRenderCache;
}
/**
* Adds #post_render_cache callbacks.
*
* @param string $callback
* The #post_render_cache callback that will replace the placeholder with
* its eventual markup.
* @param array $context
* An array providing context for the #post_render_cache callback.
*
* @see \Drupal\Core\Render\RendererInterface::generateCachePlaceholder()
*
* @return $this
*/
public function addPostRenderCacheCallback($callback, array $context) {
$this->postRenderCache[$callback][] = $context;
return $this;
}
/**
* Sets #post_render_cache callbacks.
*
* @param array $post_render_cache_callbacks
* The associated #post_render_cache callbacks to be executed.
*
* @return $this
*/
public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) {
$this->postRenderCache = $post_render_cache_callbacks;
return $this;
}
} }
...@@ -135,21 +135,29 @@ public function renderResponse(array $main_content, Request $request, RouteMatch ...@@ -135,21 +135,29 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
// entire render cache, regardless of the cache bin. // entire render cache, regardless of the cache bin.
$cache_contexts = []; $cache_contexts = [];
$cache_tags = ['rendered']; $cache_tags = ['rendered'];
$cache_max_age = Cache::PERMANENT;
foreach (['page_top', 'page', 'page_bottom'] as $region) { foreach (['page_top', 'page', 'page_bottom'] as $region) {
if (isset($html[$region])) { if (isset($html[$region])) {
$cache_contexts = Cache::mergeContexts($cache_contexts, $html[$region]['#cache']['contexts']); $cache_contexts = Cache::mergeContexts($cache_contexts, $html[$region]['#cache']['contexts']);
$cache_tags = Cache::mergeTags($cache_tags, $html[$region]['#cache']['tags']); $cache_tags = Cache::mergeTags($cache_tags, $html[$region]['#cache']['tags']);
$cache_max_age = Cache::mergeMaxAges($cache_max_age, $html[$region]['#cache']['max-age']);
} }
} }
// Set the generator in the HTTP header. // Set the generator in the HTTP header.
list($version) = explode('.', \Drupal::VERSION, 2); list($version) = explode('.', \Drupal::VERSION, 2);
return new Response($content, 200,[ $response = new Response($content, 200,[
'X-Drupal-Cache-Tags' => implode(' ', $cache_tags), 'X-Drupal-Cache-Tags' => implode(' ', $cache_tags),
'X-Drupal-Cache-Contexts' => implode(' ', $cache_contexts), 'X-Drupal-Cache-Contexts' => implode(' ', $cache_contexts),
'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)' 'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)'
]); ]);
// If an explicit non-infinite max-age is specified by a part of the page,
// respect that by applying it to the response's headers.
if ($cache_max_age !== Cache::PERMANENT) {
$response->setMaxAge($cache_max_age);
}
return $response;
} }
/** /**
......
...@@ -218,6 +218,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { ...@@ -218,6 +218,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
// Defaults for bubbleable rendering metadata. // Defaults for bubbleable rendering metadata.
$elements['#cache']['contexts'] = isset($elements['#cache']['contexts']) ? $elements['#cache']['contexts'] : array(); $elements['#cache']['contexts'] = isset($elements['#cache']['contexts']) ? $elements['#cache']['contexts'] : array();
$elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
$elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
$elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
$elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array();
...@@ -723,6 +724,11 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) { ...@@ -723,6 +724,11 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) {
* The cache ID string, or FALSE if the element may not be cached. * The cache ID string, or FALSE if the element may not be cached.
*/ */
protected function createCacheID(array $elements) { protected function createCacheID(array $elements) {
// If the maximum age is zero, then caching is effectively prohibited.
if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) {
return FALSE;
}
if (isset($elements['#cache']['cid'])) { if (isset($elements['#cache']['cid'])) {
return $elements['#cache']['cid']; return $elements['#cache']['cid'];
} }
......
...@@ -76,6 +76,8 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la ...@@ -76,6 +76,8 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
$plugin->getCacheTags() // Block plugin cache tags. $plugin->getCacheTags() // Block plugin cache tags.
); );
$build[$entity_id]['#cache']['max-age'] = $plugin->getCacheMaxAge();
if ($plugin->isCacheable()) { if ($plugin->isCacheable()) {
$build[$entity_id]['#pre_render'][] = array($this, 'buildBlock'); $build[$entity_id]['#pre_render'][] = array($this, 'buildBlock');
// Generic cache keys, with the block plugin's custom keys appended. // Generic cache keys, with the block plugin's custom keys appended.
......
...@@ -151,7 +151,7 @@ protected function verifyRenderCacheHandling() { ...@@ -151,7 +151,7 @@ protected function verifyRenderCacheHandling() {
// Test that entities with caching disabled do not generate a cache entry. // Test that entities with caching disabled do not generate a cache entry.
$build = $this->getBlockRenderArray(); $build = $this->getBlockRenderArray();
$this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags'), 'The render array element of uncacheable blocks is not cached, but does have cache tags set.'); $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags', 'max-age'), 'The render array element of uncacheable blocks is not cached, but does have cache tags & max-age set.');
// Enable block caching. // Enable block caching.
$this->setBlockCacheConfig(array( $this->setBlockCacheConfig(array(
......
...@@ -113,7 +113,7 @@ public static function preRenderText($element) { ...@@ -113,7 +113,7 @@ public static function preRenderText($element) {
foreach ($filters as $filter) { foreach ($filters as $filter) {
if ($filter_must_be_applied($filter)) { if ($filter_must_be_applied($filter)) {
$result = $filter->process($text, $langcode); $result = $filter->process($text, $langcode);
$metadata = $metadata->merge($result->getBubbleableMetadata()); $metadata = $metadata->merge($result);
$text = $result->getProcessedText(); $text = $result->getProcessedText();
} }
} }
......
...@@ -21,7 +21,10 @@ ...@@ -21,7 +21,10 @@
* 2. declare cache tags that the filtered text depends upon, so when either of * 2. declare cache tags that the filtered text depends upon, so when either of
* those cache tags is invalidated, the filtered text should also be * those cache tags is invalidated, the filtered text should also be
* invalidated; * invalidated;
* 3. apply uncacheable filtering, for example because it differs per user. * 3. declare cache context to vary by, e.g. 'language' to do language-specific
* filtering.
* 4. declare a maximum age for the filtered text
* 5. apply uncacheable filtering, for example because it differs per user.
* *
* In case a filter needs one or more of these advanced use cases, it can use * In case a filter needs one or more of these advanced use cases, it can use
* the additional methods available. * the additional methods available.
...@@ -49,14 +52,20 @@ ...@@ -49,14 +52,20 @@
* ), * ),
* )); * ));
* *
* // Associate cache contexts to vary by.
* $result->setCacheContexts(['language']);
*
* // Associate cache tags to be invalidated by. * // Associate cache tags to be invalidated by.
* $result->setCacheTags($node->getCacheTags()); * $result->setCacheTags($node->getCacheTags());
* *
* // Associate a maximum age.
* $result->setCacheMaxAge(300); // 5 minutes.
*
* return $result; * return $result;
* } * }
* @endcode * @endcode
*/ */
class FilterProcessResult { class FilterProcessResult extends BubbleableMetadata {
/** /**
* The processed text. * The processed text.
...@@ -67,40 +76,6 @@ class FilterProcessResult { ...@@ -67,40 +76,6 @@ class FilterProcessResult {
*/ */
protected $processedText; protected $processedText;
/**
* An array of associated assets to be attached.
*
* @see drupal_process_attached()
*
* @var array
*/
protected $assets;
/**
* The attached cache tags.
*
* @see drupal_render_collect_cache_tags()
*
* @var array
*/
protected $cacheTags;
/**
* The associated cache contexts.
*
* @var string[]
*/
protected $cacheContexts;
/**
* The associated #post_render_cache callbacks.
*
* @see _drupal_render_process_post_render_cache()
*
* @var array
*/
protected $postRenderCacheCallbacks;
/** /**
* Constructs a FilterProcessResult object. * Constructs a FilterProcessResult object.
* *
...@@ -109,11 +84,6 @@ class FilterProcessResult { ...@@ -109,11 +84,6 @@ class FilterProcessResult {
*/ */
public function __construct($processed_text) { public function __construct($processed_text) {
$this->processedText = $processed_text; $this->processedText = $processed_text;
$this->assets = array();
$this->cacheTags = array();
$this->cacheContexts = array();
$this->postRenderCacheCallbacks = array();
} }
/** /**
...@@ -146,164 +116,4 @@ public function setProcessedText($processed_text) { ...@@ -146,164 +116,4 @@ public function setProcessedText($processed_text) {
$this->processedText = $processed_text; $this->processedText = $processed_text;
return $this; return $this;
} }
/**
* Gets cache tags associated with the processed text.
*
* @return array
*/
public function getCacheTags() {
return $this->cacheTags;
}