Chris@0: requestStack = $request_stack; Chris@0: $this->cacheFactory = $cache_factory; Chris@0: $this->cacheContextsManager = $cache_contexts_manager; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function get(array $elements) { Chris@0: // Form submissions rely on the form being built during the POST request, Chris@0: // and render caching of forms prevents this from happening. Chris@0: // @todo remove the isMethodCacheable() check when Chris@0: // https://www.drupal.org/node/2367555 lands. Chris@0: if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$cid = $this->createCacheID($elements)) { Chris@0: return FALSE; Chris@0: } Chris@0: $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; Chris@0: Chris@0: if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) { Chris@0: $cached_element = $cache->data; Chris@0: // Two-tier caching: redirect to actual (post-bubbling) cache item. Chris@0: // @see \Drupal\Core\Render\RendererInterface::render() Chris@0: // @see ::set() Chris@0: if (isset($cached_element['#cache_redirect'])) { Chris@0: return $this->get($cached_element); Chris@0: } Chris@0: // Return the cached element. Chris@0: return $cached_element; Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function set(array &$elements, array $pre_bubbling_elements) { Chris@0: // Form submissions rely on the form being built during the POST request, Chris@0: // and render caching of forms prevents this from happening. Chris@0: // @todo remove the isMethodCacheable() check when Chris@0: // https://www.drupal.org/node/2367555 lands. Chris@0: if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$cid = $this->createCacheID($elements)) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: $data = $this->getCacheableRenderArray($elements); Chris@0: Chris@0: $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; Chris@0: $cache = $this->cacheFactory->get($bin); Chris@0: Chris@0: // Calculate the pre-bubbling CID. Chris@0: $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements); Chris@0: Chris@0: // Two-tier caching: detect different CID post-bubbling, create redirect, Chris@0: // update redirect if different set of cache contexts. Chris@0: // @see \Drupal\Core\Render\RendererInterface::render() Chris@0: // @see ::get() Chris@0: if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) { Chris@0: // The cache redirection strategy we're implementing here is pretty Chris@0: // simple in concept. Suppose we have the following render structure: Chris@0: // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) Chris@0: // -- B (specifies #cache['contexts'] = ['b']) Chris@0: // Chris@0: // At the time that we're evaluating whether A's rendering can be Chris@0: // retrieved from cache, we won't know the contexts required by its Chris@0: // children (the children might not even be built yet), so cacheGet() Chris@0: // will only be able to get what is cached for a $cid of 'foo'. But at Chris@0: // the time we're writing to that cache, we do know all the contexts that Chris@0: // were specified by all children, so what we need is a way to Chris@0: // persist that information between the cache write and the next cache Chris@0: // read. So, what we can do is store the following into 'foo': Chris@0: // [ Chris@0: // '#cache_redirect' => TRUE, Chris@0: // '#cache' => [ Chris@0: // ... Chris@0: // 'contexts' => ['b'], Chris@0: // ], Chris@0: // ] Chris@0: // Chris@0: // This efficiently lets cacheGet() redirect to a $cid that includes all Chris@0: // of the required contexts. The strategy is on-demand: in the case where Chris@0: // there aren't any additional contexts required by children that aren't Chris@0: // already included in the parent's pre-bubbled #cache information, no Chris@0: // cache redirection is needed. Chris@0: // Chris@0: // When implementing this redirection strategy, special care is needed to Chris@0: // resolve potential cache ping-pong problems. For example, consider the Chris@0: // following render structure: Chris@0: // - A (pre-bubbling, specifies #cache['keys'] = ['foo']) Chris@0: // -- B (pre-bubbling, specifies #cache['contexts'] = ['b']) Chris@0: // --- C (pre-bubbling, specifies #cache['contexts'] = ['c']) Chris@0: // --- D (pre-bubbling, specifies #cache['contexts'] = ['d']) Chris@0: // Chris@0: // Additionally, suppose that: Chris@0: // - C only exists for a 'b' context value of 'b1' Chris@0: // - D only exists for a 'b' context value of 'b2' Chris@0: // This is an acceptable variation, since B specifies that its contents Chris@0: // vary on context 'b'. Chris@0: // Chris@0: // A naive implementation of cache redirection would result in the Chris@0: // following: Chris@0: // - When a request is processed where context 'b' = 'b1', what would be Chris@0: // cached for a $pre_bubbling_cid of 'foo' is: Chris@0: // [ Chris@0: // '#cache_redirect' => TRUE, Chris@0: // '#cache' => [ Chris@0: // ... Chris@0: // 'contexts' => ['b', 'c'], Chris@0: // ], Chris@0: // ] Chris@0: // - When a request is processed where context 'b' = 'b2', we would Chris@0: // retrieve the above from cache, but when following that redirection, Chris@0: // get a cache miss, since we're processing a 'b' context value that Chris@0: // has not yet been cached. Given the cache miss, we would continue Chris@0: // with rendering the structure, perform the required context bubbling Chris@0: // and then overwrite the above item with: Chris@0: // [ Chris@0: // '#cache_redirect' => TRUE, Chris@0: // '#cache' => [ Chris@0: // ... Chris@0: // 'contexts' => ['b', 'd'], Chris@0: // ], Chris@0: // ] Chris@0: // - Now, if a request comes in where context 'b' = 'b1' again, the above Chris@0: // would redirect to a cache key that doesn't exist, since we have not Chris@0: // yet cached an item that includes 'b'='b1' and something for 'd'. So Chris@0: // we would process this request as a cache miss, at the end of which, Chris@0: // we would overwrite the above item back to: Chris@0: // [ Chris@0: // '#cache_redirect' => TRUE, Chris@0: // '#cache' => [ Chris@0: // ... Chris@0: // 'contexts' => ['b', 'c'], Chris@0: // ], Chris@0: // ] Chris@0: // - The above would always result in accurate renderings, but would Chris@0: // result in poor performance as we keep processing requests as cache Chris@0: // misses even though the target of the redirection is cached, and Chris@0: // it's only the redirection element itself that is creating the Chris@0: // ping-pong problem. Chris@0: // Chris@0: // A way to resolve the ping-pong problem is to eventually reach a cache Chris@0: // state where the redirection element includes all of the contexts used Chris@0: // throughout all requests: Chris@0: // [ Chris@0: // '#cache_redirect' => TRUE, Chris@0: // '#cache' => [ Chris@0: // ... Chris@0: // 'contexts' => ['b', 'c', 'd'], Chris@0: // ], Chris@0: // ] Chris@0: // Chris@0: // We can't reach that state right away, since we don't know what the Chris@0: // result of future requests will be, but we can incrementally move Chris@0: // towards that state by progressively merging the 'contexts' value Chris@0: // across requests. That's the strategy employed below and tested in Chris@0: // \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing(). Chris@0: Chris@0: // Get the cacheability of this element according to the current (stored) Chris@0: // redirecting cache item, if any. Chris@0: $redirect_cacheability = new CacheableMetadata(); Chris@0: if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { Chris@0: $redirect_cacheability = CacheableMetadata::createFromRenderArray($stored_cache_redirect->data); Chris@0: } Chris@0: Chris@0: // Calculate the union of the cacheability for this request and the Chris@0: // current (stored) redirecting cache item. We need: Chris@0: // - the union of cache contexts, because that is how we know which cache Chris@0: // item to redirect to; Chris@0: // - the union of cache tags, because that is how we know when the cache Chris@0: // redirect cache item itself is invalidated; Chris@0: // - the union of max ages, because that is how we know when the cache Chris@0: // redirect cache item itself becomes stale. (Without this, we might end Chris@0: // up toggling between a permanently and a briefly cacheable cache Chris@0: // redirect, because the last update's max-age would always "win".) Chris@0: $redirect_cacheability_updated = CacheableMetadata::createFromRenderArray($data)->merge($redirect_cacheability); Chris@0: Chris@0: // Stored cache contexts incomplete: this request causes cache contexts to Chris@0: // be added to the redirecting cache item. Chris@0: if (array_diff($redirect_cacheability_updated->getCacheContexts(), $redirect_cacheability->getCacheContexts())) { Chris@0: $redirect_data = [ Chris@0: '#cache_redirect' => TRUE, Chris@0: '#cache' => [ Chris@0: // The cache keys of the current element; this remains the same Chris@0: // across requests. Chris@0: 'keys' => $elements['#cache']['keys'], Chris@0: // The union of the current element's and stored cache contexts. Chris@0: 'contexts' => $redirect_cacheability_updated->getCacheContexts(), Chris@0: // The union of the current element's and stored cache tags. Chris@0: 'tags' => $redirect_cacheability_updated->getCacheTags(), Chris@0: // The union of the current element's and stored cache max-ages. Chris@0: 'max-age' => $redirect_cacheability_updated->getCacheMaxAge(), Chris@0: // The same cache bin as the one for the actual render cache items. Chris@0: 'bin' => $bin, Chris@0: ], Chris@0: ]; Chris@0: $cache->set($pre_bubbling_cid, $redirect_data, $this->maxAgeToExpire($redirect_cacheability_updated->getCacheMaxAge()), Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); Chris@0: } Chris@0: Chris@0: // Current cache contexts incomplete: this request only uses a subset of Chris@0: // the cache contexts stored in the redirecting cache item. Vary by these Chris@0: // additional (conditional) cache contexts as well, otherwise the Chris@0: // redirecting cache item would be pointing to a cache item that can never Chris@0: // exist. Chris@0: if (array_diff($redirect_cacheability_updated->getCacheContexts(), $data['#cache']['contexts'])) { Chris@0: // Recalculate the cache ID. Chris@0: $recalculated_cid_pseudo_element = [ Chris@0: '#cache' => [ Chris@0: 'keys' => $elements['#cache']['keys'], Chris@0: 'contexts' => $redirect_cacheability_updated->getCacheContexts(), Chris@17: ], Chris@0: ]; Chris@0: $cid = $this->createCacheID($recalculated_cid_pseudo_element); Chris@0: // Ensure the about-to-be-cached data uses the merged cache contexts. Chris@0: $data['#cache']['contexts'] = $redirect_cacheability_updated->getCacheContexts(); Chris@0: } Chris@0: } Chris@0: $cache->set($cid, $data, $this->maxAgeToExpire($elements['#cache']['max-age']), Cache::mergeTags($data['#cache']['tags'], ['rendered'])); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Maps a #cache[max-age] value to an "expire" value for the Cache API. Chris@0: * Chris@0: * @param int $max_age Chris@0: * A #cache[max-age] value. Chris@0: * Chris@0: * @return int Chris@0: * A corresponding "expire" value. Chris@0: * Chris@0: * @see \Drupal\Core\Cache\CacheBackendInterface::set() Chris@0: */ Chris@0: protected function maxAgeToExpire($max_age) { Chris@0: return ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $max_age; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Creates the cache ID for a renderable element. Chris@0: * Chris@0: * Creates the cache ID string based on #cache['keys'] + #cache['contexts']. Chris@0: * Chris@0: * @param array &$elements Chris@0: * A renderable array. Chris@0: * Chris@0: * @return string Chris@0: * The cache ID string, or FALSE if the element may not be cached. Chris@0: */ Chris@0: protected function createCacheID(array &$elements) { Chris@0: // If the maximum age is zero, then caching is effectively prohibited. Chris@0: if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: if (isset($elements['#cache']['keys'])) { Chris@0: $cid_parts = $elements['#cache']['keys']; Chris@0: if (!empty($elements['#cache']['contexts'])) { Chris@0: $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']); Chris@0: $cid_parts = array_merge($cid_parts, $context_cache_keys->getKeys()); Chris@0: CacheableMetadata::createFromRenderArray($elements) Chris@0: ->merge($context_cache_keys) Chris@0: ->applyTo($elements); Chris@0: } Chris@0: return implode(':', $cid_parts); Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getCacheableRenderArray(array $elements) { Chris@0: $data = [ Chris@0: '#markup' => $elements['#markup'], Chris@0: '#attached' => $elements['#attached'], Chris@0: '#cache' => [ Chris@0: 'contexts' => $elements['#cache']['contexts'], Chris@0: 'tags' => $elements['#cache']['tags'], Chris@0: 'max-age' => $elements['#cache']['max-age'], Chris@0: ], Chris@0: ]; Chris@0: Chris@0: // Preserve cacheable items if specified. If we are preserving any cacheable Chris@0: // children of the element, we assume we are only interested in their Chris@0: // individual markup and not the parent's one, thus we empty it to minimize Chris@0: // the cache entry size. Chris@0: if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) { Chris@0: $data['#cache_properties'] = $elements['#cache_properties']; Chris@0: Chris@0: // Extract all the cacheable items from the element using cache Chris@0: // properties. Chris@0: $cacheable_items = array_intersect_key($elements, array_flip($elements['#cache_properties'])); Chris@0: $cacheable_children = Element::children($cacheable_items); Chris@0: if ($cacheable_children) { Chris@0: $data['#markup'] = ''; Chris@0: // Cache only cacheable children's markup. Chris@0: foreach ($cacheable_children as $key) { Chris@0: // We can assume that #markup is safe at this point. Chris@0: $cacheable_items[$key] = ['#markup' => Markup::create($cacheable_items[$key]['#markup'])]; Chris@0: } Chris@0: } Chris@0: $data += $cacheable_items; Chris@0: } Chris@0: Chris@0: $data['#markup'] = Markup::create($data['#markup']); Chris@0: return $data; Chris@0: } Chris@0: Chris@0: }