core/core.services.yml | 7 +- .../Core/Render/MainContent/HtmlRenderer.php | 16 +- core/lib/Drupal/Core/Render/RenderCache.php | 310 ++++++++++++++++++++ .../Drupal/Core/Render/RenderCacheInterface.php | 77 +++++ core/lib/Drupal/Core/Render/Renderer.php | 323 +-------------------- core/lib/Drupal/Core/Render/RendererInterface.php | 19 -- .../src/Plugin/views/cache/CachePluginBase.php | 18 +- core/modules/views/src/Plugin/views/cache/Time.php | 8 +- .../Core/Render/RendererPostRenderCacheTest.php | 14 +- .../Drupal/Tests/Core/Render/RendererTest.php | 12 +- .../Drupal/Tests/Core/Render/RendererTestBase.php | 12 +- 11 files changed, 461 insertions(+), 355 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 2f3a696..e5d8a26 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -853,7 +853,7 @@ services: - { name: event_subscriber } main_content_renderer.html: class: Drupal\Core\Render\MainContent\HtmlRenderer - arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@element_info', '@module_handler', '@renderer', '@cache_contexts_manager'] + arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@element_info', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager'] tags: - { name: render.main_content_renderer, format: html } main_content_renderer.ajax: @@ -1346,8 +1346,11 @@ services: tags: - { name: mime_type_guesser } lazy: true + render_cache: + class: Drupal\Core\Render\RenderCache + arguments: ['@request_stack', '@cache_factory', '@cache_contexts_manager'] renderer: class: Drupal\Core\Render\Renderer - arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@request_stack', '@cache_factory', '@cache_contexts_manager', '%renderer.config%'] + arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '%renderer.config%'] email.validator: class: Egulias\EmailValidator\EmailValidator diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index 17568d1..f403c62 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -16,7 +16,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\Render\PageDisplayVariantSelectionEvent; -use Drupal\Core\Render\Renderer; +use Drupal\Core\Render\RenderCacheInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RenderEvents; use Drupal\Core\Routing\RouteMatchInterface; @@ -73,6 +73,13 @@ class HtmlRenderer implements MainContentRendererInterface { protected $renderer; /** + * The render cache service. + * + * @var \Drupal\Core\Render\RenderCacheInterface + */ + protected $renderCache; + + /** * The cache contexts manager service. * * @var \Drupal\Core\Cache\CacheContextsManager @@ -94,16 +101,19 @@ class HtmlRenderer implements MainContentRendererInterface { * The module handler. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer service. + * @param \Drupal\Core\Render\RenderCacheInterface $render_cache + * The render cache service. * @param \Drupal\Core\Cache\CacheContextsManager $cache_contexts_manager * The cache contexts manager service. */ - public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ElementInfoManagerInterface $element_info_manager, ModuleHandlerInterface $module_handler, RendererInterface $renderer, CacheContextsManager $cache_contexts_manager) { + public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ElementInfoManagerInterface $element_info_manager, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, CacheContextsManager $cache_contexts_manager) { $this->titleResolver = $title_resolver; $this->displayVariantManager = $display_variant_manager; $this->eventDispatcher = $event_dispatcher; $this->elementInfoManager = $element_info_manager; $this->moduleHandler = $module_handler; $this->renderer = $renderer; + $this->renderCache = $render_cache; $this->cacheContextsManager = $cache_contexts_manager; } @@ -217,7 +227,7 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte // @todo Remove this once https://www.drupal.org/node/2359901 lands. if (!empty($main_content)) { $this->renderer->render($main_content, FALSE); - $main_content = $this->renderer->getCacheableRenderArray($main_content) + [ + $main_content = $this->renderCache->getCacheableRenderArray($main_content) + [ '#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL ]; } diff --git a/core/lib/Drupal/Core/Render/RenderCache.php b/core/lib/Drupal/Core/Render/RenderCache.php new file mode 100644 index 0000000..0589059 --- /dev/null +++ b/core/lib/Drupal/Core/Render/RenderCache.php @@ -0,0 +1,310 @@ +requestStack = $request_stack; + $this->cacheFactory = $cache_factory; + $this->cacheContextsManager = $cache_contexts_manager; + } + + /** + * {@inheritdoc} + */ + public function get(array $elements) { + // Form submissions rely on the form being built during the POST request, + // and render caching of forms prevents this from happening. + // @todo remove the isMethodSafe() check when + // https://www.drupal.org/node/2367555 lands. + if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) { + return FALSE; + } + $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; + + if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) { + $cached_element = $cache->data; + // Two-tier caching: redirect to actual (post-bubbling) cache item. + // @see \Drupal\Core\Render\RendererInterface::render() + // @see ::set() + if (isset($cached_element['#cache_redirect'])) { + return $this->get($cached_element); + } + // Return the cached element. + return $cached_element; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function cacheSet(array &$elements, array $pre_bubbling_elements) { + // Form submissions rely on the form being built during the POST request, + // and render caching of forms prevents this from happening. + // @todo remove the isMethodSafe() check when + // https://www.drupal.org/node/2367555 lands. + if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) { + return FALSE; + } + + $data = $this->getCacheableRenderArray($elements); + + $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; + $expire = ($elements['#cache']['max-age'] === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $elements['#cache']['max-age']; + $cache = $this->cacheFactory->get($bin); + + // Calculate the pre-bubbling CID. + $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements); + + // Two-tier caching: detect different CID post-bubbling, create redirect, + // update redirect if different set of cache contexts. + // @see \Drupal\Core\Render\RendererInterface::render() + // @see ::get() + if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) { + // The cache redirection strategy we're implementing here is pretty + // simple in concept. Suppose we have the following render structure: + // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) + // -- B (specifies #cache['contexts'] = ['b']) + // + // At the time that we're evaluating whether A's rendering can be + // retrieved from cache, we won't know the contexts required by its + // children (the children might not even be built yet), so cacheGet() + // will only be able to get what is cached for a $cid of 'foo'. But at + // the time we're writing to that cache, we do know all the contexts that + // were specified by all children, so what we need is a way to + // persist that information between the cache write and the next cache + // read. So, what we can do is store the following into 'foo': + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b'], + // ], + // ] + // + // This efficiently lets cacheGet() redirect to a $cid that includes all + // of the required contexts. The strategy is on-demand: in the case where + // there aren't any additional contexts required by children that aren't + // already included in the parent's pre-bubbled #cache information, no + // cache redirection is needed. + // + // When implementing this redirection strategy, special care is needed to + // resolve potential cache ping-pong problems. For example, consider the + // following render structure: + // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) + // -- B (pre-bubbling, specifies #cache['contexts'] = ['b']) + // --- C (pre-bubbling, specifies #cache['contexts'] = ['c']) + // --- D (pre-bubbling, specifies #cache['contexts'] = ['d']) + // + // Additionally, suppose that: + // - C only exists for a 'b' context value of 'b1' + // - D only exists for a 'b' context value of 'b2' + // This is an acceptable variation, since B specifies that its contents + // vary on context 'b'. + // + // A naive implementation of cache redirection would result in the + // following: + // - When a request is processed where context 'b' = 'b1', what would be + // cached for a $pre_bubbling_cid of 'foo' is: + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b', 'c'], + // ], + // ] + // - When a request is processed where context 'b' = 'b2', we would + // retrieve the above from cache, but when following that redirection, + // get a cache miss, since we're processing a 'b' context value that + // has not yet been cached. Given the cache miss, we would continue + // with rendering the structure, perform the required context bubbling + // and then overwrite the above item with: + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b', 'd'], + // ], + // ] + // - Now, if a request comes in where context 'b' = 'b1' again, the above + // would redirect to a cache key that doesn't exist, since we have not + // yet cached an item that includes 'b'='b1' and something for 'd'. So + // we would process this request as a cache miss, at the end of which, + // we would overwrite the above item back to: + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b', 'c'], + // ], + // ] + // - The above would always result in accurate renderings, but would + // result in poor performance as we keep processing requests as cache + // misses even though the target of the redirection is cached, and + // it's only the redirection element itself that is creating the + // ping-pong problem. + // + // A way to resolve the ping-pong problem is to eventually reach a cache + // state where the redirection element includes all of the contexts used + // throughout all requests: + // [ + // '#cache_redirect' => TRUE, + // '#cache' => [ + // ... + // 'contexts' => ['b', 'c', 'd'], + // ], + // ] + // + // We can't reach that state right away, since we don't know what the + // result of future requests will be, but we can incrementally move + // towards that state by progressively merging the 'contexts' value + // across requests. That's the strategy employed below and tested in + // \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing(). + + // The set of cache contexts for this element, including the bubbled ones, + // for which we are handling a cache miss. + $cache_contexts = $data['#cache']['contexts']; + + // Get the contexts by which this element should be varied according to + // the current redirecting cache item, if any. + $stored_cache_contexts = []; + $stored_cache_tags = []; + if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { + $stored_cache_contexts = $stored_cache_redirect->data['#cache']['contexts']; + $stored_cache_tags = $stored_cache_redirect->data['#cache']['tags']; + } + + // Calculate the union of the cache contexts for this request and the + // stored cache contexts. + $merged_cache_contexts = Cache::mergeContexts($stored_cache_contexts, $cache_contexts); + + // Stored cache contexts incomplete: this request causes cache contexts to + // be added to the redirecting cache item. + if (array_diff($merged_cache_contexts, $stored_cache_contexts)) { + $redirect_data = [ + '#cache_redirect' => TRUE, + '#cache' => [ + // The cache keys of the current element; this remains the same + // across requests. + 'keys' => $elements['#cache']['keys'], + // The union of the current element's and stored cache contexts. + 'contexts' => $merged_cache_contexts, + // The union of the current element's and stored cache tags. + 'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']), + ], + ]; + $cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); + } + + // Current cache contexts incomplete: this request only uses a subset of + // the cache contexts stored in the redirecting cache item. Vary by these + // additional (conditional) cache contexts as well, otherwise the + // redirecting cache item would be pointing to a cache item that can never + // exist. + if (array_diff($merged_cache_contexts, $cache_contexts)) { + // Recalculate the cache ID. + $recalculated_cid_pseudo_element = [ + '#cache' => [ + 'keys' => $elements['#cache']['keys'], + 'contexts' => $merged_cache_contexts, + ] + ]; + $cid = $this->createCacheID($recalculated_cid_pseudo_element); + // Ensure the about-to-be-cached data uses the merged cache contexts. + $data['#cache']['contexts'] = $merged_cache_contexts; + } + } + $cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered'])); + } + + /** + * Creates the cache ID for a renderable element. + * + * Creates the cache ID string based on #cache['keys'] + #cache['contexts']. + * + * @param array $elements + * A renderable array. + * + * @return string + * 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']['keys'])) { + $cid_parts = $elements['#cache']['keys']; + if (!empty($elements['#cache']['contexts'])) { + $contexts = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']); + $cid_parts = array_merge($cid_parts, $contexts); + } + return implode(':', $cid_parts); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getCacheableRenderArray(array $elements) { + return [ + '#markup' => $elements['#markup'], + '#attached' => $elements['#attached'], + '#post_render_cache' => $elements['#post_render_cache'], + '#cache' => [ + 'contexts' => $elements['#cache']['contexts'], + 'tags' => $elements['#cache']['tags'], + 'max-age' => $elements['#cache']['max-age'], + ], + ]; + } + +} diff --git a/core/lib/Drupal/Core/Render/RenderCacheInterface.php b/core/lib/Drupal/Core/Render/RenderCacheInterface.php new file mode 100644 index 0000000..71e7bed --- /dev/null +++ b/core/lib/Drupal/Core/Render/RenderCacheInterface.php @@ -0,0 +1,77 @@ +controllerResolver = $controller_resolver; $this->theme = $theme; $this->elementInfo = $element_info; - $this->requestStack = $request_stack; - $this->cacheFactory = $cache_factory; - $this->cacheContextsManager = $cache_contexts_manager; + $this->renderCache = $render_cache; $this->rendererConfig = $renderer_config; } @@ -194,7 +173,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Try to fetch the prerendered element from cache, run any // #post_render_cache callbacks and return the final markup. if (isset($elements['#cache']['keys'])) { - $cached_element = $this->cacheGet($elements); + $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; // Only when we're not in a root (non-recursive) drupal_render() call, @@ -215,8 +194,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) { } // Two-tier caching: track pre-bubbling elements' #cache for later // comparison. - // @see ::cacheGet() - // @see ::cacheSet() + // @see \Drupal\Core\Render\RenderCacheInterface::get() + // @see \Drupal\Core\Render\RenderCacheInterface::set() $pre_bubbling_elements = []; $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : []; @@ -384,7 +363,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); } - $this->cacheSet($elements, $pre_bubbling_elements); + $this->renderCache->cacheSet($elements, $pre_bubbling_elements); } // Only when we're in a root (non-recursive) drupal_render() call, @@ -508,286 +487,6 @@ protected function processPostRenderCache(array &$elements) { } /** - * Gets the cached, prerendered element of a renderable element from the cache. - * - * @param array $elements - * A renderable array. - * - * @return array - * A renderable array, with the original element and all its children pre- - * rendered, or FALSE if no cached copy of the element is available. - * - * @see ::render() - * @see ::saveToCache() - */ - protected function cacheGet(array $elements) { - // Form submissions rely on the form being built during the POST request, - // and render caching of forms prevents this from happening. - // @todo remove the isMethodSafe() check when - // https://www.drupal.org/node/2367555 lands. - if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) { - return FALSE; - } - $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; - - if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) { - $cached_element = $cache->data; - // Two-tier caching: redirect to actual (post-bubbling) cache item. - // @see ::doRender() - // @see ::cacheSet() - if (isset($cached_element['#cache_redirect'])) { - return $this->cacheGet($cached_element); - } - // Return the cached element. - return $cached_element; - } - return FALSE; - } - - /** - * Caches the rendered output of a renderable element. - * - * This is called by ::render() if the #cache property is set on an element. - * - * @param array $elements - * A renderable array. - * @param array $pre_bubbling_elements - * A renderable array corresponding to the state (in particular, the - * cacheability metadata) of $elements prior to the beginning of its - * rendering process, and therefore before any bubbling of child - * information has taken place. Only the #cache property is used by this - * function, so the caller may omit all other properties and children from - * this array. - * - * @return bool|null - * Returns FALSE if no cache item could be created, NULL otherwise. - * - * @see ::getFromCache() - */ - protected function cacheSet(array &$elements, array $pre_bubbling_elements) { - // Form submissions rely on the form being built during the POST request, - // and render caching of forms prevents this from happening. - // @todo remove the isMethodSafe() check when - // https://www.drupal.org/node/2367555 lands. - if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) { - return FALSE; - } - - $data = $this->getCacheableRenderArray($elements); - - $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; - $expire = ($elements['#cache']['max-age'] === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $elements['#cache']['max-age']; - $cache = $this->cacheFactory->get($bin); - - // Calculate the pre-bubbling CID. - $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements); - - // Two-tier caching: detect different CID post-bubbling, create redirect, - // update redirect if different set of cache contexts. - // @see ::doRender() - // @see ::cacheGet() - if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) { - // The cache redirection strategy we're implementing here is pretty - // simple in concept. Suppose we have the following render structure: - // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) - // -- B (specifies #cache['contexts'] = ['b']) - // - // At the time that we're evaluating whether A's rendering can be - // retrieved from cache, we won't know the contexts required by its - // children (the children might not even be built yet), so cacheGet() - // will only be able to get what is cached for a $cid of 'foo'. But at - // the time we're writing to that cache, we do know all the contexts that - // were specified by all children, so what we need is a way to - // persist that information between the cache write and the next cache - // read. So, what we can do is store the following into 'foo': - // [ - // '#cache_redirect' => TRUE, - // '#cache' => [ - // ... - // 'contexts' => ['b'], - // ], - // ] - // - // This efficiently lets cacheGet() redirect to a $cid that includes all - // of the required contexts. The strategy is on-demand: in the case where - // there aren't any additional contexts required by children that aren't - // already included in the parent's pre-bubbled #cache information, no - // cache redirection is needed. - // - // When implementing this redirection strategy, special care is needed to - // resolve potential cache ping-pong problems. For example, consider the - // following render structure: - // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) - // -- B (pre-bubbling, specifies #cache['contexts'] = ['b']) - // --- C (pre-bubbling, specifies #cache['contexts'] = ['c']) - // --- D (pre-bubbling, specifies #cache['contexts'] = ['d']) - // - // Additionally, suppose that: - // - C only exists for a 'b' context value of 'b1' - // - D only exists for a 'b' context value of 'b2' - // This is an acceptable variation, since B specifies that its contents - // vary on context 'b'. - // - // A naive implementation of cache redirection would result in the - // following: - // - When a request is processed where context 'b' = 'b1', what would be - // cached for a $pre_bubbling_cid of 'foo' is: - // [ - // '#cache_redirect' => TRUE, - // '#cache' => [ - // ... - // 'contexts' => ['b', 'c'], - // ], - // ] - // - When a request is processed where context 'b' = 'b2', we would - // retrieve the above from cache, but when following that redirection, - // get a cache miss, since we're processing a 'b' context value that - // has not yet been cached. Given the cache miss, we would continue - // with rendering the structure, perform the required context bubbling - // and then overwrite the above item with: - // [ - // '#cache_redirect' => TRUE, - // '#cache' => [ - // ... - // 'contexts' => ['b', 'd'], - // ], - // ] - // - Now, if a request comes in where context 'b' = 'b1' again, the above - // would redirect to a cache key that doesn't exist, since we have not - // yet cached an item that includes 'b'='b1' and something for 'd'. So - // we would process this request as a cache miss, at the end of which, - // we would overwrite the above item back to: - // [ - // '#cache_redirect' => TRUE, - // '#cache' => [ - // ... - // 'contexts' => ['b', 'c'], - // ], - // ] - // - The above would always result in accurate renderings, but would - // result in poor performance as we keep processing requests as cache - // misses even though the target of the redirection is cached, and - // it's only the redirection element itself that is creating the - // ping-pong problem. - // - // A way to resolve the ping-pong problem is to eventually reach a cache - // state where the redirection element includes all of the contexts used - // throughout all requests: - // [ - // '#cache_redirect' => TRUE, - // '#cache' => [ - // ... - // 'contexts' => ['b', 'c', 'd'], - // ], - // ] - // - // We can't reach that state right away, since we don't know what the - // result of future requests will be, but we can incrementally move - // towards that state by progressively merging the 'contexts' value - // across requests. That's the strategy employed below and tested in - // \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing(). - - // The set of cache contexts for this element, including the bubbled ones, - // for which we are handling a cache miss. - $cache_contexts = $data['#cache']['contexts']; - - // Get the contexts by which this element should be varied according to - // the current redirecting cache item, if any. - $stored_cache_contexts = []; - $stored_cache_tags = []; - if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { - $stored_cache_contexts = $stored_cache_redirect->data['#cache']['contexts']; - $stored_cache_tags = $stored_cache_redirect->data['#cache']['tags']; - } - - // Calculate the union of the cache contexts for this request and the - // stored cache contexts. - $merged_cache_contexts = Cache::mergeContexts($stored_cache_contexts, $cache_contexts); - - // Stored cache contexts incomplete: this request causes cache contexts to - // be added to the redirecting cache item. - if (array_diff($merged_cache_contexts, $stored_cache_contexts)) { - $redirect_data = [ - '#cache_redirect' => TRUE, - '#cache' => [ - // The cache keys of the current element; this remains the same - // across requests. - 'keys' => $elements['#cache']['keys'], - // The union of the current element's and stored cache contexts. - 'contexts' => $merged_cache_contexts, - // The union of the current element's and stored cache tags. - 'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']), - ], - ]; - $cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); - } - - // Current cache contexts incomplete: this request only uses a subset of - // the cache contexts stored in the redirecting cache item. Vary by these - // additional (conditional) cache contexts as well, otherwise the - // redirecting cache item would be pointing to a cache item that can never - // exist. - if (array_diff($merged_cache_contexts, $cache_contexts)) { - // Recalculate the cache ID. - $recalculated_cid_pseudo_element = [ - '#cache' => [ - 'keys' => $elements['#cache']['keys'], - 'contexts' => $merged_cache_contexts, - ] - ]; - $cid = $this->createCacheID($recalculated_cid_pseudo_element); - // Ensure the about-to-be-cached data uses the merged cache contexts. - $data['#cache']['contexts'] = $merged_cache_contexts; - } - } - $cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered'])); - } - - /** - * Creates the cache ID for a renderable element. - * - * Creates the cache ID string based on #cache['keys'] + #cache['contexts']. - * - * @param array $elements - * A renderable array. - * - * @return string - * 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']['keys'])) { - $cid_parts = $elements['#cache']['keys']; - if (!empty($elements['#cache']['contexts'])) { - $contexts = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']); - $cid_parts = array_merge($cid_parts, $contexts); - } - return implode(':', $cid_parts); - } - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function getCacheableRenderArray(array $elements) { - return [ - '#markup' => $elements['#markup'], - '#attached' => $elements['#attached'], - '#post_render_cache' => $elements['#post_render_cache'], - '#cache' => [ - 'contexts' => $elements['#cache']['contexts'], - 'tags' => $elements['#cache']['tags'], - 'max-age' => $elements['#cache']['max-age'], - ], - ]; - } - - /** * {@inheritdoc} */ public function mergeBubbleableMetadata(array $a, array $b) { diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index d2b7990..7e97526 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -309,25 +309,6 @@ public function renderPlain(&$elements); public function render(&$elements, $is_root_call = FALSE); /** - * Gets a cacheable render array for a render array and its rendered output. - * - * Given a render array and its rendered output (HTML string), return an array - * data structure that allows the render array and its associated metadata to - * be cached reliably (and is serialization-safe). - * - * If Drupal needs additional rendering metadata to be cached at some point, - * consumers of this method will continue to work. Those who only cache - * certain parts of a render array will cease to work. - * - * @param array $elements - * A renderable array, on which ::render() has already been invoked. - * - * @return array - * An array representing the cacheable data for this render array. - */ - public function getCacheableRenderArray(array $elements); - - /** * Merges the bubbleable rendering metadata o/t 2nd render array with the 1st. * * @param array $a diff --git a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php index 21e666f..224a964 100644 --- a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php +++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php @@ -8,6 +8,7 @@ namespace Drupal\views\Plugin\views\cache; use Drupal\Core\Cache\Cache; +use Drupal\Core\Render\RenderCacheInterface; use Drupal\Core\Render\RendererInterface; use Drupal\views\Plugin\views\PluginBase; use Drupal\Core\Database\Query\Select; @@ -81,6 +82,13 @@ protected $renderer; /** + * The render cache service. + * + * @var \Drupal\Core\Render\RenderCacheInterface + */ + protected $renderCache; + + /** * Constructs a CachePluginBase object. * * @param array $configuration @@ -91,11 +99,14 @@ * The plugin implementation definition. * @param \Drupal\Core\Render\RendererInterface $renderer * The HTML renderer. + * @param \Drupal\Core\Render\RenderCacheInterface $render_cache + * The render cache service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, RendererInterface $renderer) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, RendererInterface $renderer, RenderCacheInterface $render_cache) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->renderer = $renderer; + $this->renderCache = $render_cache; } /** @@ -106,7 +117,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('renderer') + $container->get('renderer'), + $container->get('render_cache') ); } @@ -190,7 +202,7 @@ public function cacheSet($type) { // Also assign the cacheable render array back to the display handler so // that is used to render the view for this request and rendering does // not happen twice. - $this->storage = $this->view->display_handler->output = $this->renderer->getCacheableRenderArray($output); + $this->storage = $this->view->display_handler->output = $this->renderCache->getCacheableRenderArray($output); \Drupal::cache($this->outputBin)->set($this->generateOutputKey(), $this->storage, $this->cacheSetExpire($type), Cache::mergeTags($this->storage['#cache']['tags'], ['rendered'])); break; } diff --git a/core/modules/views/src/Plugin/views/cache/Time.php b/core/modules/views/src/Plugin/views/cache/Time.php index cd576c1..1aff4f0 100644 --- a/core/modules/views/src/Plugin/views/cache/Time.php +++ b/core/modules/views/src/Plugin/views/cache/Time.php @@ -9,6 +9,7 @@ use Drupal\Core\Datetime\DateFormatter; use Drupal\Core\Cache\Cache; +use Drupal\Core\Render\RenderCacheInterface; use Drupal\Core\Render\RendererInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Form\FormStateInterface; @@ -51,10 +52,12 @@ class Time extends CachePluginBase { * The HTML renderer. * @param \Drupal\Core\Datetime\DateFormatter $date_formatter * The date formatter service. + * @param \Drupal\Core\Render\RenderCacheInterface $render_cache + * The render cache service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, RendererInterface $renderer, DateFormatter $date_formatter) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, RendererInterface $renderer, RenderCacheInterface $render_cache, DateFormatter $date_formatter) { $this->dateFormatter = $date_formatter; - parent::__construct($configuration, $plugin_id, $plugin_definition, $renderer); + parent::__construct($configuration, $plugin_id, $plugin_definition, $renderer, $render_cache); } /** @@ -66,6 +69,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_id, $plugin_definition, $container->get('renderer'), + $container->get('render_cache'), $container->get('date.formatter') ); } diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php index dd33b58..43e2d91 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php @@ -71,9 +71,9 @@ public function testPostRenderCacheWithCacheDisabled() { /** * @covers ::render * @covers ::doRender - * @covers ::cacheGet - * @covers ::cacheSet - * @covers ::createCacheID + * @covers \Drupal\Core\Render\RenderCache::get + * @covers \Drupal\Core\Render\RenderCache::cacheSet + * @covers \Drupal\Core\Render\RenderCache::createCacheID */ public function testPostRenderCacheWithColdCache() { list($test_element, $context) = $this->generatePostRenderCacheElement(); @@ -127,7 +127,7 @@ public function testPostRenderCacheWithColdCache() { /** * @covers ::render * @covers ::doRender - * @covers ::cacheGet + * @covers \Drupal\Core\Render\RenderCache::get * @covers ::processPostRenderCache */ public function testPostRenderCacheWithPostRequest() { @@ -189,7 +189,7 @@ public function testRenderRecursivePostRenderCache() { * * @covers ::render * @covers ::doRender - * @covers ::cacheGet + * @covers \Drupal\Core\Render\RenderCache::get * @covers ::processPostRenderCache */ public function testRenderChildrenPostRenderCacheDifferentContexts() { @@ -284,7 +284,7 @@ public function testRenderChildrenPostRenderCacheDifferentContexts() { * * @covers ::render * @covers ::doRender - * @covers ::cacheGet + * @covers \Drupal\Core\Render\RenderCache::get * @covers ::processPostRenderCache */ public function testRenderChildrenPostRenderCacheComplex() { @@ -404,7 +404,7 @@ public function testRenderChildrenPostRenderCacheComplex() { * * @covers ::render * @covers ::doRender - * @covers ::cacheGet + * @covers \Drupal\Core\Render\RenderCache::get * @covers ::processPostRenderCache * @covers ::generateCachePlaceholder */ diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index bbea4b9..0f7a47a 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -531,9 +531,9 @@ public function testRenderWithThemeArguments() { /** * @covers ::render * @covers ::doRender - * @covers ::cacheGet - * @covers ::cacheSet - * @covers ::createCacheID + * @covers \Drupal\Core\Render\RenderCache::get + * @covers \Drupal\Core\Render\RenderCache::cacheSet + * @covers \Drupal\Core\Render\RenderCache::createCacheID */ public function testRenderCache() { $this->setUpRequest(); @@ -584,9 +584,9 @@ public function testRenderCache() { /** * @covers ::render * @covers ::doRender - * @covers ::cacheGet - * @covers ::cacheSet - * @covers ::createCacheID + * @covers \Drupal\Core\Render\RenderCache::get + * @covers \Drupal\Core\Render\RenderCache::cacheSet + * @covers \Drupal\Core\Render\RenderCache::createCacheID * * @dataProvider providerTestRenderCacheMaxAge */ diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php index c433e30..903adce 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php @@ -12,6 +12,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\Renderer; +use Drupal\Core\Render\RenderCache; use Drupal\Tests\UnitTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpFoundation\Request; @@ -30,6 +31,13 @@ class RendererTestBase extends UnitTestCase { protected $renderer; /** + * The tested render cache. + * + * @var \Drupal\Core\Render\RenderCache + */ + protected $renderCache; + + /** * @var \Symfony\Component\HttpFoundation\RequestStack */ protected $requestStack; @@ -118,10 +126,12 @@ protected function setUp() { } return $keys; }); - $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->requestStack, $this->cacheFactory, $this->cacheContextsManager, $this->rendererConfig); + $this->renderCache = new RenderCache($this->requestStack, $this->cacheFactory, $this->cacheContextsManager); + $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo, $this->renderCache, $this->rendererConfig); $container = new ContainerBuilder(); $container->set('cache_contexts_manager', $this->cacheContextsManager); + $container->set('render_cache', $this->renderCache); $container->set('renderer', $this->renderer); \Drupal::setContainer($container); }