Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
83.33% |
5 / 6 |
CRAP | |
98.59% |
70 / 71 |
RenderCache | |
0.00% |
0 / 1 |
|
83.33% |
5 / 6 |
34 | |
98.59% |
70 / 71 |
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
get | |
100.00% |
1 / 1 |
8 | |
100.00% |
9 / 9 |
|||
set | |
100.00% |
1 / 1 |
9 | |
100.00% |
26 / 26 |
|||
maxAgeToExpire | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
createCacheID | |
0.00% |
0 / 1 |
5.01 | |
91.67% |
11 / 12 |
|||
getCacheableRenderArray | |
100.00% |
1 / 1 |
9 | |
100.00% |
19 / 19 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Render\RenderCache. | |
*/ | |
namespace Drupal\Core\Render; | |
use Drupal\Component\Utility\SafeMarkup; | |
use Drupal\Core\Cache\Cache; | |
use Drupal\Core\Cache\CacheableMetadata; | |
use Drupal\Core\Cache\Context\CacheContextsManager; | |
use Drupal\Core\Cache\CacheFactoryInterface; | |
use Symfony\Component\HttpFoundation\RequestStack; | |
/** | |
* Wraps the caching logic for the render caching system. | |
* | |
* @internal | |
* | |
* @todo Refactor this out into a generic service capable of cache redirects, | |
* and let RenderCache use that. https://www.drupal.org/node/2551419 | |
*/ | |
class RenderCache implements RenderCacheInterface { | |
/** | |
* The request stack. | |
* | |
* @var \Symfony\Component\HttpFoundation\RequestStack | |
*/ | |
protected $requestStack; | |
/** | |
* The cache factory. | |
* | |
* @var \Drupal\Core\Cache\CacheFactoryInterface | |
*/ | |
protected $cacheFactory; | |
/** | |
* The cache contexts manager. | |
* | |
* @var \Drupal\Core\Cache\Context\CacheContextsManager | |
*/ | |
protected $cacheContextsManager; | |
/** | |
* Constructs a new RenderCache object. | |
* | |
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack | |
* The request stack. | |
* @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory | |
* The cache factory. | |
* @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager | |
* The cache contexts manager. | |
*/ | |
public function __construct(RequestStack $request_stack, CacheFactoryInterface $cache_factory, CacheContextsManager $cache_contexts_manager) { | |
$this->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 set(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'; | |
$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(). | |
// Get the cacheability of this element according to the current (stored) | |
// redirecting cache item, if any. | |
$redirect_cacheability = new CacheableMetadata(); | |
if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) { | |
$redirect_cacheability = CacheableMetadata::createFromRenderArray($stored_cache_redirect->data); | |
} | |
// Calculate the union of the cacheability for this request and the | |
// current (stored) redirecting cache item. We need: | |
// - the union of cache contexts, because that is how we know which cache | |
// item to redirect to; | |
// - the union of cache tags, because that is how we know when the cache | |
// redirect cache item itself is invalidated; | |
// - the union of max ages, because that is how we know when the cache | |
// redirect cache item itself becomes stale. (Without this, we might end | |
// up toggling between a permanently and a briefly cacheable cache | |
// redirect, because the last update's max-age would always "win".) | |
$redirect_cacheability_updated = CacheableMetadata::createFromRenderArray($data)->merge($redirect_cacheability); | |
// Stored cache contexts incomplete: this request causes cache contexts to | |
// be added to the redirecting cache item. | |
if (array_diff($redirect_cacheability_updated->getCacheContexts(), $redirect_cacheability->getCacheContexts())) { | |
$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' => $redirect_cacheability_updated->getCacheContexts(), | |
// The union of the current element's and stored cache tags. | |
'tags' => $redirect_cacheability_updated->getCacheTags(), | |
// The union of the current element's and stored cache max-ages. | |
'max-age' => $redirect_cacheability_updated->getCacheMaxAge(), | |
// The same cache bin as the one for the actual render cache items. | |
'bin' => $bin, | |
], | |
]; | |
$cache->set($pre_bubbling_cid, $redirect_data, $this->maxAgeToExpire($redirect_cacheability_updated->getCacheMaxAge()), 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($redirect_cacheability_updated->getCacheContexts(), $data['#cache']['contexts'])) { | |
// Recalculate the cache ID. | |
$recalculated_cid_pseudo_element = [ | |
'#cache' => [ | |
'keys' => $elements['#cache']['keys'], | |
'contexts' => $redirect_cacheability_updated->getCacheContexts(), | |
] | |
]; | |
$cid = $this->createCacheID($recalculated_cid_pseudo_element); | |
// Ensure the about-to-be-cached data uses the merged cache contexts. | |
$data['#cache']['contexts'] = $redirect_cacheability_updated->getCacheContexts(); | |
} | |
} | |
$cache->set($cid, $data, $this->maxAgeToExpire($elements['#cache']['max-age']), Cache::mergeTags($data['#cache']['tags'], ['rendered'])); | |
} | |
/** | |
* Maps a #cache[max-age] value to an "expire" value for the Cache API. | |
* | |
* @param int $max_age | |
* A #cache[max-age] value. | |
* | |
* @return int | |
* A corresponding "expire" value. | |
* | |
* @see \Drupal\Core\Cache\CacheBackendInterface::set() | |
*/ | |
protected function maxAgeToExpire($max_age) { | |
return ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $max_age; | |
} | |
/** | |
* 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'])) { | |
$context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']); | |
$cid_parts = array_merge($cid_parts, $context_cache_keys->getKeys()); | |
CacheableMetadata::createFromRenderArray($elements) | |
->merge($context_cache_keys) | |
->applyTo($elements); | |
} | |
return implode(':', $cid_parts); | |
} | |
return FALSE; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getCacheableRenderArray(array $elements) { | |
$data = [ | |
'#markup' => $elements['#markup'], | |
'#attached' => $elements['#attached'], | |
'#cache' => [ | |
'contexts' => $elements['#cache']['contexts'], | |
'tags' => $elements['#cache']['tags'], | |
'max-age' => $elements['#cache']['max-age'], | |
], | |
]; | |
// Preserve cacheable items if specified. If we are preserving any cacheable | |
// children of the element, we assume we are only interested in their | |
// individual markup and not the parent's one, thus we empty it to minimize | |
// the cache entry size. | |
if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) { | |
$data['#cache_properties'] = $elements['#cache_properties']; | |
// Ensure that any safe strings are a Markup object. | |
foreach (Element::properties(array_flip($elements['#cache_properties'])) as $cache_property) { | |
if (isset($elements[$cache_property]) && is_scalar($elements[$cache_property]) && SafeMarkup::isSafe($elements[$cache_property])) { | |
$elements[$cache_property] = Markup::create($elements[$cache_property]); | |
} | |
} | |
// Extract all the cacheable items from the element using cache | |
// properties. | |
$cacheable_items = array_intersect_key($elements, array_flip($elements['#cache_properties'])); | |
$cacheable_children = Element::children($cacheable_items); | |
if ($cacheable_children) { | |
$data['#markup'] = ''; | |
// Cache only cacheable children's markup. | |
foreach ($cacheable_children as $key) { | |
// We can assume that #markup is safe at this point. | |
$cacheable_items[$key] = ['#markup' => Markup::create($cacheable_items[$key]['#markup'])]; | |
} | |
} | |
$data += $cacheable_items; | |
} | |
$data['#markup'] = Markup::create($data['#markup']); | |
return $data; | |
} | |
} |