core/lib/Drupal/Core/Access/AccessResult.php | 9 +- core/lib/Drupal/Core/Cache/Cache.php | 30 +++ core/lib/Drupal/Core/Render/BubbleableMetadata.php | 212 ++++++++++++++++++--- .../Core/Render/MainContent/HtmlRenderer.php | 10 +- core/lib/Drupal/Core/Render/Renderer.php | 6 + core/modules/block/src/BlockViewBuilder.php | 2 + .../block/src/Tests/BlockViewBuilderTest.php | 2 +- core/modules/filter/src/Element/ProcessedText.php | 2 +- core/modules/filter/src/FilterProcessResult.php | 212 ++------------------- .../Tests/Core/Render/BubbleableMetadataTest.php | 15 +- .../Drupal/Tests/Core/Render/RendererTest.php | 2 + 11 files changed, 265 insertions(+), 237 deletions(-) diff --git a/core/lib/Drupal/Core/Access/AccessResult.php b/core/lib/Drupal/Core/Access/AccessResult.php index d515933..db78d89 100644 --- a/core/lib/Drupal/Core/Access/AccessResult.php +++ b/core/lib/Drupal/Core/Access/AccessResult.php @@ -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 diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php index 91cc4a8..ff24324 100644 --- a/core/lib/Drupal/Core/Cache/Cache.php +++ b/core/lib/Drupal/Core/Cache/Cache.php @@ -71,6 +71,36 @@ public static function mergeTags() { } /** + * 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. * * Can be called before using cache tags in operations, to ensure validity. diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php index fa2edf0..d822d82 100644 --- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php @@ -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; + } + } diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index ee2a42e..5378d7c 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -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 . ' (http://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; } /** diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 189c2de..a3f72a8 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -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(); @@ -728,6 +729,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']; } diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php index e56b24d..8430393 100644 --- a/core/modules/block/src/BlockViewBuilder.php +++ b/core/modules/block/src/BlockViewBuilder.php @@ -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. diff --git a/core/modules/block/src/Tests/BlockViewBuilderTest.php b/core/modules/block/src/Tests/BlockViewBuilderTest.php index 506ce51..d4f99f3 100644 --- a/core/modules/block/src/Tests/BlockViewBuilderTest.php +++ b/core/modules/block/src/Tests/BlockViewBuilderTest.php @@ -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( diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php index 52f0649..d007b5f 100644 --- a/core/modules/filter/src/Element/ProcessedText.php +++ b/core/modules/filter/src/Element/ProcessedText.php @@ -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(); } } diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php index e8487d1..67e49fa 100644 --- a/core/modules/filter/src/FilterProcessResult.php +++ b/core/modules/filter/src/FilterProcessResult.php @@ -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. @@ -68,40 +77,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. * * @param string $processed_text @@ -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() - ); - } - } diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php index ea336da..f6c4cfb 100644 --- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php @@ -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' => [ 'settings' => [ @@ -97,13 +104,17 @@ public function providerTestCreateFromRenderArray() { $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' => ['foo:bar'], + 'max-age' => Cache::PERMANENT, ], '#attached' => [ 'settings' => [ diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index 455834e..d705ede 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Render; +use Drupal\Core\Cache\Cache; use Drupal\Core\Render\Element; use Drupal\Core\Template\Attribute; @@ -20,6 +21,7 @@ class RendererTest extends RendererTestBase { '#cache' => [ 'contexts' => [], 'tags' => [], + 'max-age' => Cache::PERMANENT, ], '#attached' => [], '#post_render_cache' => [],