diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php index bf7812e..7eefbb3 100644 --- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php +++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php @@ -54,6 +54,13 @@ class BubbleableMetadata implements CacheableDependencyInterface { protected $postRenderCache = []; /** + * #placeholders metadata. + * + * @var array[] + */ + protected $placeholders = []; + + /** * Merges the values of another bubbleable metadata object with this one. * * @param \Drupal\Core\Render\BubbleableMetadata $other @@ -71,6 +78,7 @@ public function merge(BubbleableMetadata $other) { $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->placeholders = NestedArray::mergeDeep($this->placeholders, $other->placeholders); $result->attached = \Drupal::service('renderer')->mergeAttachments($this->attached, $other->attached); $result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache); return $result; @@ -86,6 +94,7 @@ public function applyTo(array &$build) { $build['#cache']['contexts'] = $this->contexts; $build['#cache']['tags'] = $this->tags; $build['#cache']['max-age'] = $this->maxAge; + $build['#cache']['placeholders'] = $this->placeholders; $build['#attached'] = $this->attached; $build['#post_render_cache'] = $this->postRenderCache; } @@ -103,6 +112,7 @@ public static function createFromRenderArray(array $build) { $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->placeholders = (isset($build['#cache']['placeholders'])) ? $build['#cache']['placeholders'] : []; $meta->attached = (isset($build['#attached'])) ? $build['#attached'] : []; $meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : []; return $meta; diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index 8821930..64a48c7 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -121,6 +121,11 @@ public function renderResponse(array $main_content, Request $request, RouteMatch $page['#title'] = $title; + // @todo Hack: Remove need of jQuery for BigPipe. + $page['#attached']['library'][] = 'core/drupal'; + $page['#attached']['library'][] = 'core/drupalSettings'; + $page['#attached']['library'][] = 'core/jquery'; + // Now render the rendered page.html.twig template inside the html.html.twig // template, and use the bubbled #attached metadata from $page to ensure we // load all attached assets. @@ -158,18 +163,93 @@ public function renderResponse(array $main_content, Request $request, RouteMatch $cache_contexts = []; $cache_tags = ['rendered']; $cache_max_age = Cache::PERMANENT; + $placeholders = []; 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']); + $placeholders = NestedArray::mergeDeep($placeholders, $html[$region]['#cache']['placeholders']); } } // Set the generator in the HTTP header. list($version) = explode('.', \Drupal::VERSION, 2); - $response = new Response($content, 200,[ + $class = 'Symfony\Component\HttpFoundation\Response'; + + // If there are still placeholders to process use a StreamedResponse to render them. + if (!empty($placeholders)) { + $class='Symfony\Component\HttpFoundation\StreamedResponse'; + $markup = $content; + + $content = function() use ($markup, $placeholders) { + // Split it up in various chunks. + $split = ''; + if (strpos($markup, $split) === FALSE) { + $split = ''; + } + $page_parts = explode($split, $markup); + + if (count($page_parts) !== 2) { + throw new \LogicException("You need to have only one body or one tag in your html.html.twig template file."); + } + print $page_parts[0]; + + // @todo Add helper function. + $behaviors = << +// We know we are at the end of the request parsing, so start processing behaviors. +Drupal.attachBehaviors(); + +EOF; + print $behaviors; + + // Support streaming on NGINX + php-fpm (nginx >= 1.5.6). + header('X-Accel-Buffering: no'); + + ob_end_flush(); + flush(); + + ksort($placeholders); + + foreach ($placeholders as $placeholder => $placeholder_elements) { + // Check if the placeholder is present at all. + if (strpos($markup, $placeholder) === FALSE) { + continue; + } + + // Ensure we don't render another placeholder. + $placeholder_elements['#cache_no_placeholder'] = TRUE; + $placeholder_markup = $this->renderer->render($placeholder_elements, TRUE); + + if ($placeholder_markup == '') { + continue; + } + + // @todo Process attached, etc. + $html = \Drupal\Component\Serialization\Json::encode($placeholder_markup); + + // @todo Add helper function. + // @todo Output as AJAX response so we can re-use the AJAX code. + print << +jQuery('#' + "$placeholder").replaceWith($html); + +EOF; + flush(); + } + + // Now that we have processed all the placeholders, attach the behaviors + // on the page again. + print $behaviors; + + echo $split; + echo $page_parts[1]; + }; + } + + $response = new $class($content, 200,[ 'X-Drupal-Cache-Tags' => implode(' ', $cache_tags), 'X-Drupal-Cache-Contexts' => implode(' ', $this->cacheContexts->optimizeTokens($cache_contexts)), 'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)' diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 5041a78..e837b18 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -192,15 +192,23 @@ 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. - $pre_bubbling_cid = NULL; + + // Two-tier caching: set pre-bubbling elements for later comparison. + // @see ::cacheGet() + // @see ::cacheSet() + $pre_bubbling_elements = $elements; + if (isset($elements['#cache']['keys'])) { $cached_element = $this->cacheGet($elements); + if ($cached_element !== FALSE) { $elements = $cached_element; // Only when we're not in a root (non-recursive) drupal_render() call, // #post_render_cache callbacks must be executed, to prevent breaking // the render cache in case of nested elements with #cache set. if ($is_root_call) { + // @todo This does not work in all cases as recursive #post_render_cache + // is not supported. $this->processPostRenderCache($elements); } $elements['#markup'] = SafeMarkup::set($elements['#markup']); @@ -212,14 +220,15 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $this->bubbleStack(); return $elements['#markup']; } - else { - // Two-tier caching: set pre-bubbling cache ID, if this element is - // cacheable.. - // @see ::cacheGet() - // @see ::cacheSet() - if ($this->requestStack->getCurrentRequest()->isMethodSafe() && $cid = $this->createCacheID($elements)) { - $pre_bubbling_cid = $cid; + } + + // Ensure the item we want to render is loaded. + if (!isset($elements['#pre_render']) && isset($elements['#pre_render_cache'])) { + foreach ($elements['#pre_render_cache'] as $callable => $context) { + if (is_string($callable) && strpos($callable, '::') === FALSE) { + $callable = $this->controllerResolver->getControllerFromDefinition($callable); } + $elements = call_user_func($callable, $elements, $context); } } @@ -244,6 +253,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Defaults for bubbleable rendering metadata. $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['#cache']['placeholders'] = isset($elements['#cache']['placeholders']) ? $elements['#cache']['placeholders'] : array(); $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); @@ -381,12 +391,27 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // We've rendered this element (and its subtree!), now update the stack. $this->updateStack($elements); - // Cache the processed element if #cache is set, and the metadata necessary - // to generate a cache ID is present. - if (isset($elements['#cache']) && isset($elements['#cache']['keys'])) { - $this->cacheSet($elements, $pre_bubbling_cid); + // All data is now in $elements, so discard stack as cacheSet can change this. + // @todo Simplify this! + static::$stack->pop(); + static::$stack->push(new BubbleableMetadata()); + + // Cache the processed element if both $pre_bubbling_elements and $elements + // have the metadata necessary to generate a cache ID. + if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { + if ($pre_bubbling_elements['#cache']['keys'] != $elements['#cache']['keys']) { + throw new \LogicException('Cache keys cannot be changed after initial setup. Use the contexts property instead to bubble added metadata.'); + } + $this->cacheSet($elements, $pre_bubbling_elements); + } + + if ($is_root_call && !empty($elements['#cache']['placeholders'])) { + $this->processPlaceholders($elements); } + // We've rendered this element (and its subtree!), now update the stack. + $this->updateStack($elements); + // Only when we're in a root (non-recursive) drupal_render() call, // #post_render_cache callbacks must be executed, to prevent breaking the // render cache in case of nested elements with #cache set. @@ -508,10 +533,30 @@ protected function processPostRenderCache(array &$elements) { } /** + * Processes #cache placeholders. + * + * @param array &$elements + * The structured array describing the data being rendered. + */ + protected function processPlaceholders(array &$elements) { + if (isset($elements['#cache']['placeholders'])) { + foreach ($elements['#cache']['placeholders'] as $placeholder => $placeholder_elements) { + // Ensure we don't render another placeholder. + // $placeholder_elements['#cache_no_placeholder'] = TRUE; + // $markup = $this->doRender($placeholder_elements); +// $elements['#markup'] = str_replace($placeholder, $markup, $elements['#markup']); +// unset($$elements['#cache']['placeholders'][$placeholder]); + } + } + } + + /** * Gets the cached, prerendered element of a renderable element from the cache. * * @param array $elements * A renderable array. + * @param bool $is_recursive + * Whether this function was called recursively. * * @return array * A renderable array, with the original element and all its children pre- @@ -520,15 +565,18 @@ protected function processPostRenderCache(array &$elements) { * @see ::render() * @see ::saveToCache() */ - protected function cacheGet(array $elements) { + protected function cacheGet(array $elements, $is_recursive = FALSE) { // 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)) { + if (!$this->requestStack->getCurrentRequest()->isMethodSafe()) { return FALSE; } + + $cid = $this->createCacheID($elements); $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; + $cached_element = FALSE; if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) { $cached_element = $cache->data; @@ -536,12 +584,69 @@ protected function cacheGet(array $elements) { // @see ::doRender() // @see ::cacheSet() if (isset($cached_element['#cache_redirect'])) { - return $this->cacheGet($cached_element); + $cache_redirect = $cached_element; + $cached_element = $this->cacheGet($cached_element, TRUE); } - // Return the cached element. + } + + // Placeholder support: A bubbled up max-age=0 or high-frequency cache context + // could have added a #cache_placeholder tag to the $cache_redirect entry. + // Or the original $elements could have a max-age=0 or a #cache_placeholder tag. + // In all those case we want to return a placeholder instead of the element. + // This should be considered a cache hit by the renderer. + + // If this is a recursive call or has a no-placeholder tag, return what we have now. + if ($is_recursive || isset($elements['#cache_no_placeholder'])) { return $cached_element; } - return FALSE; + + // If the data is not cached and not re-creatable, return. + // Is this placeholder eligible because it is either: + // - Cached and therefore cache retrievable? + // - Or has a #pre_render_cache callback? + if (!$cached_element && !isset($elements['#pre_render_cache'])) { + return $cached_element; + } + + // Do we need to create a placeholder? + if (!empty($cache_redirect['#cache_placeholder']) || (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] == 0)) { + $elements['#cache_placeholder'] = TRUE; + } + + // Are we asked to create a placeholder? + if (!empty($elements['#cache_placeholder'])) { + return $this->createRenderCacheArrayPlaceholder($elements, $cached_element); + } + + // Return the cached element. + return $cached_element; + } + + /** + * Create a render cache array placeholder. + * + * @param array $elements + * The elements to create a placeholder for. + * @param array|null|false $data + * Already retrieved render array data that can be used for replacing the + * placeholder in this request. + * + * @return array + * The render array placeholder. + */ + protected function createRenderCacheArrayPlaceholder(array $elements, $data = FALSE) { + $render_cache_array = array_intersect_key($elements, ['#cache' => 0, '#pre_render_cache' => 1]); + + // @todo Use hash of $pre_bubbling_elements['#pre_render_cache','#cache']? + $pholder_id = \Drupal\Component\Utility\Html::getId('X-FF-PH--' . implode(':', $render_cache_array['#cache']['keys'])); + + $placeholder = []; + $placeholder['#markup'] = '
'; + $placeholder['#cache']['placeholders'][$pholder_id] = $render_cache_array; + if ($data) { + // @todo If we already have data, then store it in the renderer. + } + return $placeholder; } /** @@ -551,22 +656,23 @@ protected function cacheGet(array $elements) { * * @param array $elements * A renderable array. - * @param string|null $pre_bubbling_cid - * The pre-bubbling cache ID. + * @param array $pre_bubbling_elements + * The pre-bubbling render array. * * @return bool|null * Returns FALSE if no cache item could be created, NULL otherwise. * * @see ::getFromCache() */ - protected function cacheSet(array &$elements, $pre_bubbling_cid) { + 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)) { + if (!$this->requestStack->getCurrentRequest()->isMethodSafe()) { return FALSE; } + $cid = $this->createCacheID($elements); $data = $this->getCacheableRenderArray($elements); @@ -574,11 +680,53 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) { $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 pre_bubbling_cid. + $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements); + + // Placeholdering at work, check if we can placeholder. + if (!isset($pre_bubbling_elements['#cache_no_placeholder']) && $pre_bubbling_cid && ($cid || isset($pre_bubbling_elements['#pre_render_cache']))) { + // Check if we want to placeholder. + if (!empty($elements['#cache_placeholder']) || (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] == 0)) { + $elements = $this->createRenderCacheArrayPlaceholder($pre_bubbling_elements, $elements); + + // Now update the #cache_redirect array or create it. + // @todo Consolidate logic using a helper function. + $redirect_data = [ + '#cache' => [ + 'keys' => [], + 'contexts' => [], + 'tags' => [], + ], + ]; + + if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { + $redirect_data['#cache']['keys'] = $stored_cache_redirect->data['#cache']['keys']; + $redirect_data['#cache']['contexts'] = $stored_cache_redirect->data['#cache']['contexts']; + $redirect_data['#cache']['tags'] = $stored_cache_redirect->data['#cache']['tags']; + if (isset($stored_cache_redirect->data['#cache']['#cache_placeholder'])) { + $redirect_data['#cache_placeholder'] = $stored_cache_redirect->data['#cache_placeholder']; + } + } + if (empty($redirect_data['#cache_placeholder'])) { + $redirect_data['#cache_redirect'] = TRUE; + $redirect_data['#cache_placeholder'] = TRUE; + $cache->set($pre_bubbling_cid, $redirect_data, Cache::PERMANENT, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); + } + + return; + } + } + + // Early return if $cid is empty. + if (!$cid) { + return FALSE; + } + // Two-tier caching: detect different CID post-bubbling, create redirect, // update redirect if different set of cache contexts. // @see ::doRender() // @see ::cacheGet() - if (isset($pre_bubbling_cid) && $pre_bubbling_cid !== $cid) { + 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']) @@ -711,6 +859,12 @@ protected function cacheSet(array &$elements, $pre_bubbling_cid) { 'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']), ], ]; + + // Ensure #cache_placeholder setting remains. + if (isset($stored_cache_redirect->data['#cache']['#cache_placeholder'])) { + $redirect_data['#cache_placeholder'] = $stored_cache_redirect->data['#cache_placeholder']; + } + $cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); } @@ -775,6 +929,7 @@ public function getCacheableRenderArray(array $elements) { 'contexts' => $elements['#cache']['contexts'], 'tags' => $elements['#cache']['tags'], 'max-age' => $elements['#cache']['max-age'], + 'placeholders' => $elements['#cache']['placeholders'], ], ]; } diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php index 216dfa8..c892150 100644 --- a/core/modules/block/src/BlockViewBuilder.php +++ b/core/modules/block/src/BlockViewBuilder.php @@ -36,33 +36,17 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N * {@inheritdoc} */ public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) { + $class = get_class($this); + /** @var \Drupal\block\BlockInterface[] $entities */ $build = array(); - foreach ($entities as $entity) { + foreach ($entities as $entity) { + $entity_type = $entity->getEntityType()->id(); $entity_id = $entity->id(); $plugin = $entity->getPlugin(); - $plugin_id = $plugin->getPluginId(); $base_id = $plugin->getBaseId(); - $derivative_id = $plugin->getDerivativeId(); - $configuration = $plugin->getConfiguration(); - // Create the render array for the block as a whole. - // @see template_preprocess_block(). $build[$entity_id] = array( - '#theme' => 'block', - '#attributes' => array(), - // All blocks get a "Configure block" contextual link. - '#contextual_links' => array( - 'block' => array( - 'route_parameters' => array('block' => $entity->id()), - ), - ), - '#weight' => $entity->getWeight(), - '#configuration' => $configuration, - '#plugin_id' => $plugin_id, - '#base_plugin_id' => $base_id, - '#derivative_plugin_id' => $derivative_id, - '#id' => $entity->id(), '#cache' => [ 'keys' => ['entity_view', 'block', $entity->id()], 'contexts' => $plugin->getCacheContexts(), @@ -73,19 +57,70 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la ), 'max-age' => $plugin->getCacheMaxAge(), ], - '#pre_render' => [ - [$this, 'buildBlock'], + '#pre_render_cache' => [ + $class . '::preRenderBlock' => compact('class', 'entity_type', 'entity_id', 'view_mode', 'langcode'), ], - // Add the entity so that it can be used in the #pre_render method. - '#block' => $entity, ); - $build[$entity_id]['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']); - // Don't run in ::buildBlock() to ensure cache keys can be altered. If an - // alter hook wants to modify the block contents, it can append another - // #pre_render hook. - $this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$entity_id], $plugin); + $module_handler = $this->moduleHandler; + $build[$entity_id] = static::preRenderBlock($build[$entity_id], compact('class', 'entity', 'view_mode', 'langcode', 'module_handler')); + + // @todo Hack: Need an $entity->isDirty() function. + if ($base_id == 'system_main_block') { + unset($build[$entity_id]['#pre_render_cache']); + } } + + return $build; + } + + public static function preRenderBlock($build, $context) { + extract($context); + if (!isset($entity)) { + $entity = entity_load($entity_type, $entity_id); + } + if (!isset($module_handler)) { + $module_handler = \Drupal::service('moduleHandler'); + } + + $entity_id = $entity->id(); + $plugin = $entity->getPlugin(); + $plugin_id = $plugin->getPluginId(); + $base_id = $plugin->getBaseId(); + $derivative_id = $plugin->getDerivativeId(); + $configuration = $plugin->getConfiguration(); + + // Create the render array for the block as a whole. + // @see template_preprocess_block(). + $build = array( + '#theme' => 'block', + '#attributes' => array(), + // All blocks get a "Configure block" contextual link. + '#contextual_links' => array( + 'block' => array( + 'route_parameters' => array('block' => $entity->id()), + ), + ), + '#weight' => $entity->getWeight(), + '#configuration' => $configuration, + '#plugin_id' => $plugin_id, + '#base_plugin_id' => $base_id, + '#derivative_plugin_id' => $derivative_id, + '#id' => $entity->id(), + '#pre_render' => [ + $class . '::buildBlock', + ], + // Add the entity so that it can be used in the #pre_render method. + '#block' => $entity, + ) + $build; + + $build['#configuration']['label'] = SafeMarkup::checkPlain($configuration['label']); + + // Don't run in ::buildBlock() to ensure cache keys can be altered. If an + // alter hook wants to modify the block contents, it can append another + // #pre_render hook. + $module_handler->alter(array('block_view', "block_view_$base_id"), $build, $plugin); + return $build; } @@ -98,7 +133,7 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la * - if there is content, moves the contextual links from the block content to * the block itself. */ - public function buildBlock($build) { + public static function buildBlock($build) { $content = $build['#block']->getPlugin()->build(); // Remove the block entity from the render array, to ensure that blocks // can be rendered without the block config entity. diff --git a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php index c5ebaa5..a86fccc 100644 --- a/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php @@ -177,6 +177,20 @@ public function build() { '#access' => $this->configuration['use_site_slogan'], ); + $name = \Drupal::service('current_user')->getUsername(); + + $build['site_slogan'] = array( + '#markup' => t('Hello @name', ['@name' => $name]), + '#access' => TRUE, + ); + + usleep(2000*1000); + + // Bubble up that this is uncacheable. + $build['site_slogan']['#cache']['max-age'] = 0; + $build['site_slogan']['#cache']['contexts'] = ['user']; + $build['site_slogan']['#debug'] = TRUE; + return $build; } @@ -190,4 +204,20 @@ public function getCacheTags() { ); } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return []; + //return [ 'route', 'user', 'languages' ]; + } + + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { +// return 0; + return Cache::PERMANENT; + } } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 88f9023..4124b2f 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -776,7 +776,7 @@ function system_preprocess_block(&$variables) { } $variables['site_slogan'] = ''; if ($variables['content']['site_slogan']['#access'] && $variables['content']['site_slogan']['#markup']) { - $variables['site_slogan'] = $variables['content']['site_slogan']['#markup']; + $variables['site_slogan'] = drupal_render($variables['content']['site_slogan']); } break; diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php index 721d992..6fa564b 100644 --- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php +++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php @@ -63,6 +63,7 @@ public function providerTestApplyTo() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#attached' => [], '#post_render_cache' => [], @@ -74,6 +75,7 @@ public function providerTestApplyTo() { 'contexts' => ['qux'], 'tags' => ['foo:bar'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#attached' => [ 'settings' => [ diff --git a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php index 7e5abf4..f3b65ce 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php @@ -123,6 +123,7 @@ public function providerTestContextBubblingEdgeCases() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -147,6 +148,7 @@ public function providerTestContextBubblingEdgeCases() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => '', @@ -179,6 +181,7 @@ public function providerTestContextBubblingEdgeCases() { '#cache' => [ 'contexts' => ['foo', 'baz'], 'max-age' => 3600, + 'placeholders' => [], ], ], ]; @@ -189,6 +192,7 @@ public function providerTestContextBubblingEdgeCases() { 'contexts' => ['bar', 'baz', 'foo'], 'tags' => [], 'max-age' => 3600, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -236,6 +240,7 @@ public function providerTestContextBubblingEdgeCases() { 'contexts' => ['bar', 'foo'], 'tags' => ['dee', 'fiddle', 'har', 'yar'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -310,6 +315,7 @@ public function testConditionalCacheContextBubblingSelfHealing() { 'contexts' => ['user.roles'], 'tags' => ['a', 'b'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -334,6 +340,7 @@ public function testConditionalCacheContextBubblingSelfHealing() { 'contexts' => ['foo', 'user.roles'], 'tags' => ['a', 'b', 'c'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -366,6 +373,7 @@ public function testConditionalCacheContextBubblingSelfHealing() { 'contexts' => ['foo', 'user.roles'], 'tags' => ['a', 'b'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -391,6 +399,7 @@ public function testConditionalCacheContextBubblingSelfHealing() { 'contexts' => ['bar', 'foo', 'user.roles'], 'tags' => ['a', 'b', 'c', 'd'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -407,6 +416,7 @@ public function testConditionalCacheContextBubblingSelfHealing() { 'contexts' => ['bar', 'foo', 'user.roles'], 'tags' => ['a', 'b'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -423,6 +433,7 @@ public function testConditionalCacheContextBubblingSelfHealing() { 'contexts' => ['bar', 'foo', 'user.roles'], 'tags' => ['a', 'b', 'c'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#post_render_cache' => [], '#markup' => 'parent', @@ -502,6 +513,45 @@ public function providerTestBubblingWithPrerender() { return $data; } + /** + * Tests that overwriting #cache keys does not corrupt the shared bubble cache. + * + * @expectedException LogicException + * @expectedExceptionMessage Cache keys cannot be changed after initial setup. Use the contexts property instead to bubble added metadata. + */ + public function testOverWriteCacheKeys() { + global $current_user_role; + + $this->setUpRequest(); + $this->setupMemoryCache(); + + $current_user_role = 1; + // Setup a shared cache redirect at llama::bar pointing to llama:bar:r.1 + $data = [ + '#cache' => [ + 'keys' => ['llama', 'bar'], + ], + '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingCacheNoOverwritePrerender'], + ]; + + $this->renderer->render($data); + $redirect = $this->memoryCache->get('llama:bar'); + + $current_user_role = 2; + + // Ensure the cached redirect is not overwritten when non-matching keys + // are given. + $data = [ + '#cache' => [ + 'keys' => ['llama', 'bar'], + ], + '#pre_render' => [__NAMESPACE__ . '\\BubblingTest::bubblingCacheOverwritePrerender'], + ]; + $this->renderer->render($data); + $redirect_after = $this->memoryCache->get('llama:bar'); + + $this->assertEquals($redirect, $redirect_after, 'Invalid bubbled keys cause redirect to be the same.'); + } } @@ -583,4 +633,27 @@ public static function bubblingPostRenderCache(array $element, array $context) { return $element; } + /** + * #pre_render callback for testOverWriteCacheKeys(). + */ + public static function bubblingCacheNoOverwritePrerender($elements) { + // Overwrite the #cache entry with new data. + $elements['#cache']['contexts'] = ['user.roles']; + $elements['#markup'] = 'Setting cache contexts just now!'; + return $elements; + } + + /** + * #pre_render callback for testOverWriteCacheKeys(). + */ + public static function bubblingCacheOverwritePrerender($elements) { + // Overwrite the #cache entry with new data. + $elements['#cache'] = [ + 'keys' => ['llama', 'foo'], + 'contexts' => ['overwritefoo'], + ]; + $elements['#markup'] = 'Setting cache keys just now!'; + return $elements; + } + } diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php index 99a639e..de12003 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererPostRenderCacheTest.php @@ -105,6 +105,7 @@ public function testPostRenderCacheWithColdCache() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -242,6 +243,7 @@ public function testRenderChildrenPostRenderCacheDifferentContexts() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], ]; @@ -338,6 +340,7 @@ public function testRenderChildrenPostRenderCacheComplex() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], ]; @@ -365,6 +368,7 @@ public function testRenderChildrenPostRenderCacheComplex() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], ]; @@ -480,6 +484,7 @@ public function testPlaceholder() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -579,6 +584,7 @@ public function testChildElementPlaceholder() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -608,6 +614,7 @@ public function testChildElementPlaceholder() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); @@ -640,6 +647,7 @@ public function testChildElementPlaceholder() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], ]; $this->assertSame($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index 56d9db1..f0cb111 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -26,6 +26,7 @@ class RendererTest extends RendererTestBase { ], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#attached' => [], '#post_render_cache' => [], @@ -642,6 +643,7 @@ public function providerTestAddDependency() { 'contexts' => [], 'tags' => [], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#attached' => [], '#post_render_cache' => [], @@ -656,6 +658,7 @@ public function providerTestAddDependency() { 'contexts' => ['user.roles'], 'tags' => ['foo'], 'max-age' => Cache::PERMANENT, + 'placeholders' => [], ], '#attached' => [], '#post_render_cache' => [], @@ -668,6 +671,7 @@ public function providerTestAddDependency() { 'contexts' => ['theme'], 'tags' => ['bar'], 'max-age' => 600, + 'placeholders' => [], ] ], new TestCacheableDependency(['user.roles'], ['foo'], Cache::PERMANENT), @@ -676,6 +680,7 @@ public function providerTestAddDependency() { 'contexts' => ['theme', 'user.roles'], 'tags' => ['bar', 'foo'], 'max-age' => 600, + 'placeholders' => [], ], '#attached' => [], '#post_render_cache' => [], @@ -696,6 +701,7 @@ public function providerTestAddDependency() { 'contexts' => ['theme'], 'tags' => ['bar'], 'max-age' => 0, + 'placeholders' => [], ], '#attached' => [], '#post_render_cache' => [],