Commit 14f0f455 authored by catch's avatar catch

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) {
$this->setCacheable($other->isCacheable());
$this->addCacheContexts($other->getCacheContexts());
$this->addCacheTags($other->getCacheTags());
// Use the lowest max-age.
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()));
}
$this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
}
// If any of the access results don't provide cacheability metadata, then
// we cannot cache the combined access result, for we may not make
......
......@@ -70,6 +70,36 @@ public static function mergeTags() {
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.
*
......
......@@ -22,47 +22,35 @@ class BubbleableMetadata {
*
* @var string[]
*/
protected $contexts;
protected $contexts = [];
/**
* Cache tags.
*
* @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
* 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.
* @var array[]
*/
public function __construct(array $contexts = [], array $tags = [], array $attached = [], array $post_render_cache = []) {
$this->contexts = $contexts;
$this->tags = $tags;
$this->attached = $attached;
$this->postRenderCache = $post_render_cache;
}
protected $postRenderCache = [];
/**
* Merges the values of another bubbleable metadata object with this one.
......@@ -81,6 +69,7 @@ public function merge(BubbleableMetadata $other) {
$result = new BubbleableMetadata();
$result->contexts = Cache::mergeContexts($this->contexts, $other->contexts);
$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->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
return $result;
......@@ -95,6 +84,7 @@ public function merge(BubbleableMetadata $other) {
public function applyTo(array &$build) {
$build['#cache']['contexts'] = $this->contexts;
$build['#cache']['tags'] = $this->tags;
$build['#cache']['max-age'] = $this->maxAge;
$build['#attached'] = $this->attached;
$build['#post_render_cache'] = $this->postRenderCache;
}
......@@ -111,9 +101,185 @@ public static function createFromRenderArray(array $build) {
$meta = new static();
$meta->contexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : [];
$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->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : [];
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
// entire render cache, regardless of the cache bin.
$cache_contexts = [];
$cache_tags = ['rendered'];
$cache_max_age = Cache::PERMANENT;
foreach (['page_top', 'page', 'page_bottom'] as $region) {
if (isset($html[$region])) {
$cache_contexts = Cache::mergeContexts($cache_contexts, $html[$region]['#cache']['contexts']);
$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.
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-Contexts' => implode(' ', $cache_contexts),
'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) {
// Defaults for bubbleable rendering metadata.
$elements['#cache']['contexts'] = isset($elements['#cache']['contexts']) ? $elements['#cache']['contexts'] : 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['#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) {
* The cache ID string, or FALSE if the element may not be cached.
*/
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'])) {
return $elements['#cache']['cid'];
}
......
......@@ -76,6 +76,8 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
$plugin->getCacheTags() // Block plugin cache tags.
);
$build[$entity_id]['#cache']['max-age'] = $plugin->getCacheMaxAge();
if ($plugin->isCacheable()) {
$build[$entity_id]['#pre_render'][] = array($this, 'buildBlock');
// Generic cache keys, with the block plugin's custom keys appended.
......
......@@ -151,7 +151,7 @@ protected function verifyRenderCacheHandling() {
// Test that entities with caching disabled do not generate a cache entry.
$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.
$this->setBlockCacheConfig(array(
......
......@@ -113,7 +113,7 @@ public static function preRenderText($element) {
foreach ($filters as $filter) {
if ($filter_must_be_applied($filter)) {
$result = $filter->process($text, $langcode);
$metadata = $metadata->merge($result->getBubbleableMetadata());
$metadata = $metadata->merge($result);
$text = $result->getProcessedText();
}
}
......
......@@ -21,7 +21,10 @@
* 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
* 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
* the additional methods available.
......@@ -49,14 +52,20 @@
* ),
* ));
*
* // Associate cache contexts to vary by.
* $result->setCacheContexts(['language']);
*
* // Associate cache tags to be invalidated by.
* $result->setCacheTags($node->getCacheTags());
*
* // Associate a maximum age.
* $result->setCacheMaxAge(300); // 5 minutes.
*
* return $result;
* }
* @endcode
*/
class FilterProcessResult {
class FilterProcessResult extends BubbleableMetadata {
/**
* The processed text.
......@@ -67,40 +76,6 @@ class FilterProcessResult {
*/
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.
*
......@@ -109,11 +84,6 @@ class FilterProcessResult {
*/
public function __construct($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) {
$this->processedText = $processed_text;
return $this;
}
/**
* Gets cache tags associated with the processed text.
*
* @return array
*/
public function getCacheTags() {
return $this->cacheTags;
}
/**
* Adds cache tags associated with the processed text.
*
* @param array $cache_tags
* The cache tags to be added.
*
* @return $this
*/
public function addCacheTags(array $cache_tags) {
$this->cacheTags = Cache::mergeTags($this->cacheTags, $cache_tags);
return $this;
}
/**
* Sets cache tags associated with the processed text.
*
* @param array $cache_tags
* The cache tags to be associated.
*
* @return $this
*/
public function setCacheTags(array $cache_tags) {
$this->cacheTags = $cache_tags;
return $this;
}
/**
* Gets cache contexts associated with the processed text.
*
* @return string[]
*/
public function getCacheContexts() {
return $this->cacheContexts;
}
/**
* Adds cache contexts associated with the processed text.
*
* @param string[] $cache_contexts
* The cache contexts to be added.
*
* @return $this
*/
public function addCacheContexts(array $cache_contexts) {
$this->cacheContexts = Cache::mergeContexts($this->cacheContexts, $cache_contexts);
return $this;
}
/**
* Sets cache contexts associated with the processed text.
*
* @param string[] $cache_contexts
* The cache contexts to be associated.
*
* @return $this
*/
public function setCacheContexts(array $cache_contexts) {
$this->cacheContexts = $cache_contexts;
return $this;
}
/**
* Gets assets associated with the processed text.
*
* @return array
*/
public function getAssets() {
return $this->assets;
}
/**
* Adds assets associated with the processed text.
*
* @param array $assets
* The associated assets to be attached.
*
* @return $this
*/
public function addAssets(array $assets) {
$this->assets = NestedArray::mergeDeep($this->assets, $assets);
return $this;
}
/**
* Sets assets associated with the processed text.
*
* @param array $assets
* The associated assets to be attached.
*
* @return $this
*/
public function setAssets(array $assets) {
$this->assets = $assets;
return $this;
}
/**
* Gets #post_render_cache callbacks associated with the processed text.
*
* @return array
*/
public function getPostRenderCacheCallbacks() {
return $this->postRenderCacheCallbacks;
}
/**
* Adds #post_render_cache callbacks associated with the processed text.
*
* @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_render_cache_generate_placeholder()
*
* @return $this
*/
public function addPostRenderCacheCallback($callback, array $context) {
$this->postRenderCacheCallbacks[$callback][] = $context;
return $this;
}
/**
* Sets #post_render_cache callbacks associated with the processed text.
*
* @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->postRenderCacheCallbacks = $post_render_cache_callbacks;
return $this;
}
/**
* Returns the attached asset libraries, etc. as a bubbleable metadata object.
*
* @return \Drupal\Core\Render\BubbleableMetadata
*/
public function getBubbleableMetadata() {
return new BubbleableMetadata(
$this->getCacheContexts(),
$this->getCacheTags(),
$this->getAssets(),
$this->getPostRenderCacheCallbacks()
);
}
}
......@@ -83,6 +83,38 @@ public function testMergeTags(array $a, array $b, array $expected) {
$this->assertEquals($expected, Cache::mergeTags($a, $b));
}
/**
* Provides a list of pairs of cache tags arrays to be merged.
*
* @return array
*/
public function mergeMaxAgesProvider() {
return [
[Cache::PERMANENT, Cache::PERMANENT, Cache::PERMANENT],
[60, 60, 60],
[0, 0, 0],
[60, 0, 0],
[0, 60, 0],
[Cache::PERMANENT, 0, 0],
[0, Cache::PERMANENT, 0],
[Cache::PERMANENT, 60, 60],
[60, Cache::PERMANENT, 60],
];
}
/**
* @covers ::mergeMaxAges
*
* @dataProvider mergeMaxAgesProvider
*/
public function testMergeMaxAges($a, $b, $expected) {
$this->assertSame($expected, Cache::mergeMaxAges($a, $b));
}
/**
* Provides a list of pairs of (prefix, suffixes) to build cache tags from.
*
......
......@@ -7,6 +7,7 @@
namespace Drupal\Tests\Core\Render;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Render\Element;
......@@ -35,13 +36,17 @@ public function providerTestApplyTo() {
$data = [];
$empty_metadata = new BubbleableMetadata();
$nonempty_metadata = new BubbleableMetadata(['qux'], ['foo:bar'], ['settings' => ['foo' => 'bar']]);
$nonempty_metadata = new BubbleableMetadata();
$nonempty_metadata->setCacheContexts(['qux'])
->setCacheTags(['foo:bar'])
->setAssets(['settings' => ['foo' => 'bar']]);
$empty_render_array = [];
$nonempty_render_array = [
'#cache' => [
'contexts' => ['qux'],
'tags' => ['llamas:are:awesome:but:kittens:too'],
'max-age' => Cache::PERMANENT,
],
'#attached' => [
'library' => [
......@@ -56,6 +61,7 @@ public function providerTestApplyTo() {
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
'#attached' => [],
'#post_render_cache' => [],
......@@ -66,6 +72,7 @@ public function providerTestApplyTo() {
'#cache' => [
'contexts' => ['qux'],
'tags' => ['foo:bar'],
'max-age' => Cache::PERMANENT,
],
'#attached' => [