core/lib/Drupal/Core/Entity/EntityViewBuilder.php | 3 -- .../Field/FieldFormatter/TimestampFormatter.php | 9 ++++- core/lib/Drupal/Core/Render/Renderer.php | 30 ++++++++++++++++- core/lib/Drupal/Core/Render/RendererInterface.php | 39 ++++++++++++++++++---- .../FieldFormatter/DateTimeDefaultFormatter.php | 5 +++ .../FieldFormatter/DateTimePlainFormatter.php | 9 ++++- 6 files changed, 83 insertions(+), 12 deletions(-) diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index 2f506c2..af5abe0 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -185,9 +185,6 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco 'contexts' => array( 'theme', 'user.roles', - // @todo Move this out of here and into field formatters that depend - // on the timezone. Blocked on https://drupal.org/node/2099137. - 'timezone', ), 'bin' => $this->cacheBin, ); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php index 6861a00..6de42ee 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampFormatter.php @@ -31,7 +31,14 @@ public function viewElements(FieldItemListInterface $items) { $elements = array(); foreach ($items as $delta => $item) { - $elements[$delta] = array('#markup' => format_date($item->value)); + $elements[$delta] = [ + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + '#markup' => format_date($item->value) + ]; } return $elements; diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 7d4dcce..148e0f8 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -184,6 +184,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)) { + $elements['#cid_pre_bubbling'] = $cid; + } + } } // If the default values for this element have not been loaded yet, populate @@ -494,6 +503,12 @@ protected function cacheGet(array $elements) { if (!empty($cid) && $cache = $this->cacheFactory->get($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; } @@ -531,7 +546,20 @@ protected function cacheSet(array &$elements) { $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; $expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : Cache::PERMANENT; - $this->cacheFactory->get($bin)->set($cid, $data, $expire, $data['#cache']['tags']); + $tags = $data['#cache']['tags']; + $cache = $this->cacheFactory->get($bin); + + // Two-tier caching: detect different CID post-bubbling, create redirect. + // @see ::doRender() + // @see ::cacheGet() + if (isset($elements['#cid_pre_bubbling']) && $elements['#cid_pre_bubbling'] !== $cid) { + $redirect_data = [ + '#cache_redirect' => TRUE, + '#cache' => $elements['#cache'], + ]; + $cache->set($elements['#cid_pre_bubbling'], $redirect_data, $expire, $tags); + } + $cache->set($cid, $data, $expire, $tags); } /** diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 06f4968..0010b86 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -73,12 +73,19 @@ public function renderPlain(&$elements); * the parent array. * * An important aspect of rendering is the bubbling of rendering metadata: - * cache tags, attached assets and #post_render_cache metadata all need to be - * bubbled up. That information is needed once the rendering to a HTML string - * is completed: the resulting HTML for the page must know by which cache tags - * it should be invalidated, which (CSS and JavaScript) assets must be loaded, - * and which #post_render_cache callbacks should be executed. A stack data - * structure is used to perform this bubbling. + * cache contexts, cache tags, attached assets and #post_render_cache metadata + * all need to be bubbled up. That information is needed once the rendering to + * a HTML string is completed: we must know the bubbled cache contexts to be + * able to calculate the correct cache ID for render caching, the the + * resulting HTML for the page must know by which cache tags it should be + * invalidated, which (CSS and JavaScript) assets must be loaded, and which + * #post_render_cache callbacks should be executed. A stack data structure is + * used to perform this bubbling. + * + * Another important aspect is render caching. A two-tier caching approach + * (i.e. with one cache item redirecting to another one) to make render + * caching compatible with the bubbling of cache contexts, because cache + * contexts affect the cache ID. * * The process of rendering an element is recursive unless the element defines * an implemented theme hook in #theme. During each call to @@ -111,6 +118,20 @@ public function renderPlain(&$elements); * metadata from the element retrieved from render cache. Then, this stack * frame is bubbled: the two topmost frames are popped from the stack, * they are merged, and the result is pushed back onto the stack. + * However, also in case of a cache miss we have to do something. Note + * that a Renderer renders top-down, which means that we try to render a + * parent first, and we try to avoid the work of rendering the children by + * using the render cache. Though in this case, we are dealing with a + * cache miss. So a Renderer traverses down the tree, rendering all + * children. In doing so, the render stack is updated with the bubbleable + * metadata of the children. That means that once the children are + * rendered, we can render cache this element. But the cache ID may have + * *changed* at that point, because the children's cache contexts have + * been bubbled! + * It is for that case that we must store the current (pre-bubbling) cache + * ID, so that we can compare it with the new (post-bubbling) cache ID + * when writing to the cache. We store the current cache ID in + * #cid_pre_bubbling. * - If this element has #type defined and the default attributes for this * element have not already been merged in (#defaults_loaded = TRUE) then * the defaults for this type of element, defined in hook_element_info(), @@ -210,6 +231,12 @@ public function renderPlain(&$elements); * - If this element has #cache defined, the rendered output of this element * is saved to Renderer::render()'s internal cache. This includes the * changes made by #post_render. + * At the same time, if #cid_pre_bubbling is set, it is compared to the + * calculated cache ID. If they are different, then a redirecting cache + * item is created, containing the #cache metadata of the current element, + * and written to cache using the value of #cid_pre_bubbling as the cache + * ID. This ensures the pre-bubbling ("wrong") cache ID redirects to the + * post-bubbling ("right") cache ID. * - If this element has an array of #post_render_cache functions defined, * or any of its children has (which we would know thanks to the stack * having been updated just before the render caching step), they are diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php index f983c8d..a7cb32a 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php @@ -127,6 +127,11 @@ public function viewElements(FieldItemListInterface $items) { // Display the date using theme datetime. $elements[$delta] = array( + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], '#theme' => 'time', '#text' => $formatted_date, '#html' => FALSE, diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php index 24fc69c..3f68a7f 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php +++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php @@ -46,7 +46,14 @@ public function viewElements(FieldItemListInterface $items) { } $output = $date->format($format); } - $elements[$delta] = array('#markup' => $output); + $elements[$delta] = [ + '#cache' => [ + 'contexts' => [ + 'timezone', + ], + ], + '#markup' => $output, + ]; } return $elements;