Issue #2429257 by Wim Leers, Fabianx, effulgentsia: Bubble cache contexts
parent
9dfa950c9c
commit
fe04699eed
|
@ -21,6 +21,26 @@ class Cache {
|
|||
*/
|
||||
const PERMANENT = CacheBackendInterface::CACHE_PERMANENT;
|
||||
|
||||
/**
|
||||
* Merges arrays of cache contexts and removes duplicates.
|
||||
*
|
||||
* @param string[] …
|
||||
* Arrays of cache contexts to merge.
|
||||
*
|
||||
* @return string[]
|
||||
* The merged array of cache contexts.
|
||||
*/
|
||||
public static function mergeContexts() {
|
||||
$cache_context_arrays = func_get_args();
|
||||
$cache_contexts = [];
|
||||
foreach ($cache_context_arrays as $contexts) {
|
||||
$cache_contexts = array_merge($cache_contexts, $contexts);
|
||||
}
|
||||
$cache_contexts = array_unique($cache_contexts);
|
||||
sort($cache_contexts);
|
||||
return $cache_contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges arrays of cache tags and removes duplicates.
|
||||
*
|
||||
|
|
|
@ -105,6 +105,7 @@ class CacheContexts {
|
|||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function convertTokensToKeys(array $context_tokens) {
|
||||
sort($context_tokens);
|
||||
$keys = [];
|
||||
foreach (static::parseTokens($context_tokens) as $context) {
|
||||
list($context_id, $parameter) = $context;
|
||||
|
|
|
@ -185,9 +185,6 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
|
|||
'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,
|
||||
);
|
||||
|
|
|
@ -30,6 +30,9 @@ class LanguageFormatter extends FormatterBase {
|
|||
public function viewElements(FieldItemListInterface $items) {
|
||||
$elements = array();
|
||||
|
||||
// The 'language' cache context is not necessary, because what is printed
|
||||
// here is the language's name in English, not in the language of the
|
||||
// response.
|
||||
foreach ($items as $delta => $item) {
|
||||
$elements[$delta] = array('#markup' => $item->language ? String::checkPlain($item->language->getName()) : '');
|
||||
}
|
||||
|
|
|
@ -31,7 +31,14 @@ class TimestampFormatter extends FormatterBase {
|
|||
$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;
|
||||
|
|
|
@ -17,6 +17,13 @@ use Drupal\Core\Cache\Cache;
|
|||
*/
|
||||
class BubbleableMetadata {
|
||||
|
||||
/**
|
||||
* Cache contexts.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $contexts;
|
||||
|
||||
/**
|
||||
* Cache tags.
|
||||
*
|
||||
|
@ -41,6 +48,8 @@ class BubbleableMetadata {
|
|||
/**
|
||||
* Constructs a BubbleableMetadata value object.
|
||||
*
|
||||
* @param string[] $contexts
|
||||
* An array of cache contexts.
|
||||
* @param string[] $tags
|
||||
* An array of cache tags.
|
||||
* @param array $attached
|
||||
|
@ -48,7 +57,8 @@ class BubbleableMetadata {
|
|||
* @param array $post_render_cache
|
||||
* An array of #post_render_cache metadata.
|
||||
*/
|
||||
public function __construct(array $tags = [], array $attached = [], array $post_render_cache = []) {
|
||||
public function __construct(array $contexts = [], array $tags = [], array $attached = [], array $post_render_cache = []) {
|
||||
$this->contexts = $contexts;
|
||||
$this->tags = $tags;
|
||||
$this->attached = $attached;
|
||||
$this->postRenderCache = $post_render_cache;
|
||||
|
@ -69,6 +79,7 @@ class BubbleableMetadata {
|
|||
*/
|
||||
public function merge(BubbleableMetadata $other) {
|
||||
$result = new BubbleableMetadata();
|
||||
$result->contexts = Cache::mergeContexts($this->contexts, $other->contexts);
|
||||
$result->tags = Cache::mergeTags($this->tags, $other->tags);
|
||||
$result->attached = Renderer::mergeAttachments($this->attached, $other->attached);
|
||||
$result->postRenderCache = NestedArray::mergeDeep($this->postRenderCache, $other->postRenderCache);
|
||||
|
@ -82,6 +93,7 @@ class BubbleableMetadata {
|
|||
* A render array.
|
||||
*/
|
||||
public function applyTo(array &$build) {
|
||||
$build['#cache']['contexts'] = $this->contexts;
|
||||
$build['#cache']['tags'] = $this->tags;
|
||||
$build['#attached'] = $this->attached;
|
||||
$build['#post_render_cache'] = $this->postRenderCache;
|
||||
|
@ -97,6 +109,7 @@ class BubbleableMetadata {
|
|||
*/
|
||||
public static function createFromRenderArray(array $build) {
|
||||
$meta = new static();
|
||||
$meta->contexts = (isset($build['#cache']['contexts'])) ? $build['#cache']['contexts'] : [];
|
||||
$meta->tags = (isset($build['#cache']['tags'])) ? $build['#cache']['tags'] : [];
|
||||
$meta->attached = (isset($build['#attached'])) ? $build['#attached'] : [];
|
||||
$meta->postRenderCache = (isset($build['#post_render_cache'])) ? $build['#post_render_cache'] : [];
|
||||
|
|
|
@ -166,6 +166,7 @@ class Renderer implements RendererInterface {
|
|||
|
||||
// Try to fetch the prerendered element from cache, run any
|
||||
// #post_render_cache callbacks and return the final markup.
|
||||
$pre_bubbling_cid = NULL;
|
||||
if (isset($elements['#cache'])) {
|
||||
$cached_element = $this->cacheGet($elements);
|
||||
if ($cached_element !== FALSE) {
|
||||
|
@ -185,6 +186,15 @@ class Renderer implements RendererInterface {
|
|||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the default values for this element have not been loaded yet, populate
|
||||
|
@ -206,6 +216,7 @@ class Renderer implements RendererInterface {
|
|||
}
|
||||
|
||||
// Defaults for bubbleable rendering metadata.
|
||||
$elements['#cache']['contexts'] = isset($elements['#cache']['contexts']) ? $elements['#cache']['contexts'] : array();
|
||||
$elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
|
||||
$elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
|
||||
$elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array();
|
||||
|
@ -347,7 +358,7 @@ class Renderer implements RendererInterface {
|
|||
// 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']) || isset($elements['#cache']['cid']))) {
|
||||
$this->cacheSet($elements);
|
||||
$this->cacheSet($elements, $pre_bubbling_cid);
|
||||
}
|
||||
|
||||
// Only when we're in a root (non-recursive) drupal_render() call,
|
||||
|
@ -495,6 +506,12 @@ class Renderer implements RendererInterface {
|
|||
|
||||
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;
|
||||
}
|
||||
|
@ -508,13 +525,15 @@ class Renderer implements RendererInterface {
|
|||
*
|
||||
* @param array $elements
|
||||
* A renderable array.
|
||||
* @param string|null $pre_bubbling_cid
|
||||
* The pre-bubbling cache ID.
|
||||
*
|
||||
* @return bool|null
|
||||
* Returns FALSE if no cache item could be created, NULL otherwise.
|
||||
*
|
||||
* @see ::getFromCache()
|
||||
*/
|
||||
protected function cacheSet(array &$elements) {
|
||||
protected function cacheSet(array &$elements, $pre_bubbling_cid) {
|
||||
// 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
|
||||
|
@ -525,14 +544,174 @@ class Renderer implements RendererInterface {
|
|||
|
||||
$data = $this->getCacheableRenderArray($elements);
|
||||
|
||||
// Cache tags are cached, but we also want to assocaite the "rendered" cache
|
||||
// Cache tags are cached, but we also want to associate the "rendered" cache
|
||||
// tag. This allows us to invalidate the entire render cache, regardless of
|
||||
// the cache bin.
|
||||
$data['#cache']['tags'][] = 'rendered';
|
||||
|
||||
$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']);
|
||||
$cache = $this->cacheFactory->get($bin);
|
||||
|
||||
// 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) {
|
||||
// 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, $redirect_data['#cache']['tags']);
|
||||
}
|
||||
|
||||
// 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, $data['#cache']['tags']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -572,6 +751,7 @@ class Renderer implements RendererInterface {
|
|||
'#attached' => $elements['#attached'],
|
||||
'#post_render_cache' => $elements['#post_render_cache'],
|
||||
'#cache' => [
|
||||
'contexts' => $elements['#cache']['contexts'],
|
||||
'tags' => $elements['#cache']['tags'],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -72,13 +72,31 @@ interface RendererInterface {
|
|||
* provided by the children is typically inserted into the markup generated by
|
||||
* 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.
|
||||
* An important aspect of rendering is caching the result, when appropriate.
|
||||
* Because the HTML of a rendered item includes all of the HTML of the
|
||||
* rendered children, caching it requires certain information to bubble up
|
||||
* from child elements to their parents:
|
||||
* - Cache contexts, so that the render cache is varied by every context that
|
||||
* affects the rendered HTML. Because cache contexts affect the cache ID,
|
||||
* and therefore must be resolved for cache hits as well as misses, it is
|
||||
* up to the implementation of this interface to decide how to implement
|
||||
* the caching of items whose children specify cache contexts not directly
|
||||
* specified by the parent. \Drupal\Core\Render\Renderer implements this
|
||||
* with a lazy two-tier caching strategy. Alternate strategies could be to
|
||||
* not cache such parents at all or to cache them with the child elements
|
||||
* replaced by placeholder tokens that are dynamically rendered after cache
|
||||
* retrieval.
|
||||
* - Cache tags, so that cached renderings are invalidated when site content
|
||||
* or configuration that can affect that rendering changes.
|
||||
* - #post_render_cache callbacks, for executing code to handle dynamic
|
||||
* requirements that cannot be cached.
|
||||
* A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to
|
||||
* perform this bubbling.
|
||||
*
|
||||
* Additionally, whether retrieving from cache or not, it is important to
|
||||
* know all of the assets (CSS and JavaScript) required by the rendered HTML,
|
||||
* and this must also bubble from child to parent. Therefore,
|
||||
* \Drupal\Core\Render\BubbleableMetadata includes that data as well.
|
||||
*
|
||||
* The process of rendering an element is recursive unless the element defines
|
||||
* an implemented theme hook in #theme. During each call to
|
||||
|
@ -111,6 +129,20 @@ interface RendererInterface {
|
|||
* 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
|
||||
* $pre_bubbling_cid.
|
||||
* - 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 +242,12 @@ interface RendererInterface {
|
|||
* - 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 $pre_bubbling_cid 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 $pre_bubbling_cid 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
|
||||
|
|
|
@ -157,6 +157,7 @@ class CommentDefaultFormatter extends FormatterBase implements ContainerFactoryP
|
|||
// Unpublished comments are not included in
|
||||
// $entity->get($field_name)->comment_count, but unpublished comments
|
||||
// should display if the user is an administrator.
|
||||
$elements['#cache']['contexts'][] = 'user.roles';
|
||||
if ($this->currentUser->hasPermission('access comments') || $this->currentUser->hasPermission('administer comments')) {
|
||||
// This is a listing of Comment entities, so associate its list cache
|
||||
// tag for correct invalidation.
|
||||
|
@ -182,6 +183,7 @@ class CommentDefaultFormatter extends FormatterBase implements ContainerFactoryP
|
|||
// display below the entity. Do not show the form for the print view mode.
|
||||
if ($status == CommentItemInterface::OPEN && $comment_settings['form_location'] == CommentItemInterface::FORM_BELOW && $this->viewMode != 'print') {
|
||||
// Only show the add comment form if the user has permission.
|
||||
$elements['#cache']['contexts'][] = 'user.roles';
|
||||
if ($this->currentUser->hasPermission('post comments')) {
|
||||
// All users in the "anonymous" role can use the same form: it is fine
|
||||
// for this form to be stored in the render cache.
|
||||
|
|
|
@ -127,6 +127,11 @@ class DateTimeDefaultFormatter extends FormatterBase implements ContainerFactory
|
|||
|
||||
// Display the date using theme datetime.
|
||||
$elements[$delta] = array(
|
||||
'#cache' => [
|
||||
'contexts' => [
|
||||
'timezone',
|
||||
],
|
||||
],
|
||||
'#theme' => 'time',
|
||||
'#text' => $formatted_date,
|
||||
'#html' => FALSE,
|
||||
|
|
|
@ -46,7 +46,14 @@ class DateTimePlainFormatter extends FormatterBase {
|
|||
}
|
||||
$output = $date->format($format);
|
||||
}
|
||||
$elements[$delta] = array('#markup' => $output);
|
||||
$elements[$delta] = [
|
||||
'#cache' => [
|
||||
'contexts' => [
|
||||
'timezone',
|
||||
],
|
||||
],
|
||||
'#markup' => $output,
|
||||
];
|
||||
}
|
||||
|
||||
return $elements;
|
||||
|
|
|
@ -85,6 +85,13 @@ class FilterProcessResult {
|
|||
*/
|
||||
protected $cacheTags;
|
||||
|
||||
/**
|
||||
* The associated cache contexts.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $cacheContexts;
|
||||
|
||||
/**
|
||||
* The associated #post_render_cache callbacks.
|
||||
*
|
||||
|
@ -105,6 +112,7 @@ class FilterProcessResult {
|
|||
|
||||
$this->assets = array();
|
||||
$this->cacheTags = array();
|
||||
$this->cacheContexts = array();
|
||||
$this->postRenderCacheCallbacks = array();
|
||||
}
|
||||
|
||||
|
@ -174,6 +182,41 @@ class FilterProcessResult {
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets cache contexts associated with the processed text.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
return $this->cacheContexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cache contexts associated with the processed text.
|
||||
*
|
||||
* @param string[] $cache_contexts
|
||||
* The cache contexts to be added.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addCacheContexts(array $cache_contexts) {
|
||||
$this->cacheContexts = Cache::mergeContexts($this->cacheContexts, $cache_contexts);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets cache contexts associated with the processed text.
|
||||
*
|
||||
* @param string[] $cache_contexts
|
||||
* The cache contexts to be associated.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCacheContexts(array $cache_contexts) {
|
||||
$this->cacheContexts = $cache_contexts;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets assets associated with the processed text.
|
||||
*
|
||||
|
@ -256,6 +299,7 @@ class FilterProcessResult {
|
|||
*/
|
||||
public function getBubbleableMetadata() {
|
||||
return new BubbleableMetadata(
|
||||
$this->getCacheContexts(),
|
||||
$this->getCacheTags(),
|
||||
$this->getAssets(),
|
||||
$this->getPostRenderCacheCallbacks()
|
||||
|
|
|
@ -216,6 +216,10 @@ class FilterAPITest extends EntityUnitTestBase {
|
|||
'weight' => 0,
|
||||
'status' => TRUE,
|
||||
),
|
||||
'filter_test_cache_contexts' => array(
|
||||
'weight' => 0,
|
||||
'status' => TRUE,
|
||||
),
|
||||
'filter_test_post_render_cache' => array(
|
||||
'weight' => 1,
|
||||
'status' => TRUE,
|
||||
|
@ -253,6 +257,11 @@ class FilterAPITest extends EntityUnitTestBase {
|
|||
'foo:baz',
|
||||
);
|
||||
$this->assertEqual($expected_cache_tags, $build['#cache']['tags'], 'Expected cache tags present.');
|
||||
$expected_cache_contexts = [
|
||||
// The cache context set by the filter_test_cache_contexts filter.
|
||||
'language',
|
||||
];
|
||||
$this->assertEqual($expected_cache_contexts, $build['#cache']['contexts'], 'Expected cache contexts present.');
|
||||
$expected_markup = '<p>Hello, world!</p><p>This is a dynamic llama.</p>';
|
||||
$this->assertEqual($expected_markup, $build['#markup'], 'Expected #post_render_cache callback has been applied.');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\filter_test\Plugin\Filter\FilterTestCacheContexts.
|
||||
*/
|
||||
|
||||
namespace Drupal\filter_test\Plugin\Filter;
|
||||
|
||||
use Drupal\filter\FilterProcessResult;
|
||||
use Drupal\filter\Plugin\FilterBase;
|
||||
|
||||
/**
|
||||
* Provides a test filter to associate cache contexts.
|
||||
*
|
||||
* @Filter(
|
||||
* id = "filter_test_cache_contexts",
|
||||
* title = @Translation("Testing filter"),
|
||||
* description = @Translation("Does not change content; associates cache contexts."),
|
||||
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
|
||||
* )
|
||||
*/
|
||||
class FilterTestCacheContexts extends FilterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process($text, $langcode) {
|
||||
$result = new FilterProcessResult($text);
|
||||
// The changes made by this filter are language-specific.
|
||||
$result->addCacheContexts(['language']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
|
@ -42,6 +42,13 @@ class NodeCacheTagsTest extends EntityWithUriCacheTagsTestBase {
|
|||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) {
|
||||
return ['timezone'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
|
|
|
@ -127,6 +127,21 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
|
|||
*/
|
||||
abstract protected function createEntity();
|
||||
|
||||
/**
|
||||
* Returns the additional (non-standard) cache contexts for the tested entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to be tested, as created by createEntity().
|
||||
*
|
||||
* @return string[]
|
||||
* An array of the additional cache contexts.
|
||||
*
|
||||
* @see \Drupal\system\Tests\Entity\EntityCacheTagsTestBase::createEntity()
|
||||
*/
|
||||
protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional (non-standard) cache tags for the tested entity.
|
||||
*
|
||||
|
@ -288,6 +303,9 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
|
|||
$empty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_empty', ['entity_type_id' => $entity_type]);
|
||||
$nonempty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_labels_alphabetically', ['entity_type_id' => $entity_type]);
|
||||
|
||||
// The default cache contexts for rendered entities.
|
||||
$entity_cache_contexts = ['theme', 'user.roles'];
|
||||
|
||||
// Cache tags present on every rendered page.
|
||||
$page_cache_tags = Cache::mergeTags(
|
||||
['rendered'],
|
||||
|
@ -337,15 +355,18 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
|
|||
// Verify a cache hit, but also the presence of the correct cache tags.
|
||||
$this->verifyPageCache($referencing_entity_url, 'HIT', Cache::mergeTags($referencing_entity_cache_tags, $page_cache_tags));
|
||||
// Also verify the existence of an entity render cache entry.
|
||||
$cid = 'entity_view:entity_test:' . $this->referencing_entity->id() . ':full:classy:r.anonymous:' . date_default_timezone_get();
|
||||
$this->verifyRenderCache($cid, $referencing_entity_cache_tags);
|
||||
$cache_keys = ['entity_view', 'entity_test', $this->referencing_entity->id(), 'full'];
|
||||
$cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
|
||||
$redirected_cid = $this->createRedirectedCacheId($cache_keys, $entity_cache_contexts);
|
||||
$this->verifyRenderCache($cid, $referencing_entity_cache_tags, $redirected_cid);
|
||||
|
||||
$this->pass("Test non-referencing entity.", 'Debug');
|
||||
$this->verifyPageCache($non_referencing_entity_url, 'MISS');
|
||||
// Verify a cache hit, but also the presence of the correct cache tags.
|
||||
$this->verifyPageCache($non_referencing_entity_url, 'HIT', Cache::mergeTags($non_referencing_entity_cache_tags, $page_cache_tags));
|
||||
// Also verify the existence of an entity render cache entry.
|
||||
$cid = 'entity_view:entity_test:' . $this->non_referencing_entity->id() . ':full:classy:r.anonymous:' . date_default_timezone_get();
|
||||
$cache_keys = ['entity_view', 'entity_test', $this->non_referencing_entity->id(), 'full'];
|
||||
$cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
|
||||
$this->verifyRenderCache($cid, $non_referencing_entity_cache_tags);
|
||||
|
||||
|
||||
|
@ -578,6 +599,52 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
|
|||
$this->verifyPageCache($nonempty_entity_listing_url, 'HIT', Cache::mergeTags($this->entity->getEntityType()->getListCacheTags(), $this->getAdditionalCacheTagsForEntityListing(), $page_cache_tags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cache ID from a list of cache keys and a set of cache contexts.
|
||||
*
|
||||
* @param string[] $keys
|
||||
* A list of cache keys.
|
||||
* @param string[] $contexts
|
||||
* A set of cache contexts.
|
||||
*
|
||||
* @return string
|
||||
* The cache ID string.
|
||||
*/
|
||||
protected function createCacheId(array $keys, array $contexts) {
|
||||
$cid_parts = $keys;
|
||||
|
||||
$contexts = \Drupal::service('cache_contexts')->convertTokensToKeys($contexts);
|
||||
$cid_parts = array_merge($cid_parts, $contexts);
|
||||
|
||||
return implode(':', $cid_parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the redirected cache ID, if any.
|
||||
*
|
||||
* If a subclass overrides ::getAdditionalCacheContextsForEntity(), it can
|
||||
* specify the additional cache contexts by which the given entity must be
|
||||
* varied, because those are the cache contexts that are bubbled from the
|
||||
* field formatters.
|
||||
*
|
||||
* @param string[] $keys
|
||||
* A list of cache keys used for the regular (pre-bubbling) CID.
|
||||
* @param string[] $contexts
|
||||
* A set of cache contexts used for the regular (pre-bubbling) CID.
|
||||
*
|
||||
* @return string|null
|
||||
* The redirected (post-bubbling) CID, if any.
|
||||
*/
|
||||
protected function createRedirectedCacheId(array $keys, array $contexts) {
|
||||
$additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->referencing_entity);
|
||||
if (count($additional_cache_contexts)) {
|
||||
return $this->createCacheId($keys, Cache::mergeContexts($contexts, $additional_cache_contexts));
|
||||
}
|
||||
else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a given render cache entry exists, with the correct cache tags.
|
||||
*
|
||||
|
@ -585,14 +652,33 @@ abstract class EntityCacheTagsTestBase extends PageCacheTagsTestBase {
|
|||
* The render cache item ID.
|
||||
* @param array $tags
|
||||
* An array of expected cache tags.
|
||||
* @param string|null $redirected_cid
|
||||
* (optional) The redirected render cache item ID.
|
||||
*/
|
||||
protected function verifyRenderCache($cid, array $tags) {
|
||||
protected function verifyRenderCache($cid, array $tags, $redirected_cid = NULL) {
|
||||
// Also verify the existence of an entity render cache entry.
|
||||
$cache_entry = \Drupal::cache('render')->get($cid);
|
||||
$this->assertTrue($cache_entry, 'A render cache entry exists.');
|
||||
sort($cache_entry->tags);
|
||||
sort($tags);
|
||||
$this->assertIdentical($cache_entry->tags, $tags);
|
||||
if ($redirected_cid === NULL) {
|
||||
$this->assertTrue(!isset($cache_entry->data['#cache_redirect']), 'Render cache entry is not a redirect.');
|
||||
}
|
||||
else {
|
||||
// Verify that $cid contains a cache redirect.
|
||||
$this->assertTrue(isset($cache_entry->data['#cache_redirect']), 'Render cache entry is a redirect.');
|
||||
// Verify that the cache redirect points to the expected CID.
|
||||
$redirect_cache_metadata = $cache_entry->data['#cache'];
|
||||
$actual_redirection_cid = $this->createCacheId(
|
||||
$redirect_cache_metadata['keys'],
|
||||
$redirect_cache_metadata['contexts']
|
||||
);
|
||||
$this->assertIdentical($redirected_cid, $actual_redirection_cid);
|
||||
// Finally, verify that the redirected CID exists and has the same cache
|
||||
// tags.
|
||||
$this->verifyRenderCache($redirected_cid, $tags);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ abstract class EntityWithUriCacheTagsTestBase extends EntityCacheTagsTestBase {
|
|||
// Selects the view mode that will be used.
|
||||
$view_mode = $this->selectViewMode($entity_type);
|
||||
|
||||
// The default cache contexts for rendered entities.
|
||||
$entity_cache_contexts = ['theme', 'user.roles'];
|
||||
|
||||
// Generate the standardized entity cache tags.
|
||||
$cache_tag = $this->entity->getCacheTags();
|
||||
$view_cache_tag = \Drupal::entityManager()->getViewBuilder($entity_type)->getCacheTags();
|
||||
|
@ -45,10 +48,11 @@ abstract class EntityWithUriCacheTagsTestBase extends EntityCacheTagsTestBase {
|
|||
// Also verify the existence of an entity render cache entry, if this entity
|
||||
// type supports render caching.
|
||||
if (\Drupal::entityManager()->getDefinition($entity_type)->isRenderCacheable()) {
|
||||
$cid = 'entity_view:' . $entity_type . ':' . $this->entity->id() . ':' . $view_mode . ':classy:r.anonymous:' . date_default_timezone_get();
|
||||
$cache_entry = \Drupal::cache('render')->get($cid);
|
||||
$cache_keys = ['entity_view', $entity_type, $this->entity->id(), $view_mode];
|
||||
$cid = $this->createCacheId($cache_keys, $entity_cache_contexts);
|
||||
$redirected_cid = $this->createRedirectedCacheId($cache_keys, $entity_cache_contexts);
|
||||
$expected_cache_tags = Cache::mergeTags($cache_tag, $view_cache_tag, $this->getAdditionalCacheTagsForEntity($this->entity), array($render_cache_tag));
|
||||
$this->verifyRenderCache($cid, $expected_cache_tags);
|
||||
$this->verifyRenderCache($cid, $expected_cache_tags, $redirected_cid);
|
||||
}
|
||||
|
||||
// Verify that after modifying the entity, there is a cache miss.
|
||||
|
|
|
@ -34,6 +34,8 @@ class TextDefaultFormatter extends FormatterBase {
|
|||
public function viewElements(FieldItemListInterface $items) {
|
||||
$elements = array();
|
||||
|
||||
// The ProcessedText element already handles cache context & tag bubbling.
|
||||
// @see \Drupal\filter\Element\ProcessedText::preRenderText()
|
||||
foreach ($items as $delta => $item) {
|
||||
$elements[$delta] = array(
|
||||
'#type' => 'processed_text',
|
||||
|
|
|
@ -81,6 +81,8 @@ class TextTrimmedFormatter extends FormatterBase {
|
|||
$element['#text_summary_trim_length'] = $this->getSetting('trim_length');
|
||||
};
|
||||
|
||||
// The ProcessedText element already handles cache context & tag bubbling.
|
||||
// @see \Drupal\filter\Element\ProcessedText::preRenderText()
|
||||
foreach ($items as $delta => $item) {
|
||||
$elements[$delta] = array(
|
||||
'#type' => 'processed_text',
|
||||
|
|
|
@ -33,9 +33,9 @@ class CacheContextsTest extends UnitTestCase {
|
|||
]);
|
||||
|
||||
$expected = [
|
||||
'bar',
|
||||
'baz.cnenzrgreN',
|
||||
'baz.cnenzrgreO',
|
||||
'bar',
|
||||
];
|
||||
$this->assertEquals($expected, $new_keys);
|
||||
}
|
||||
|
|
|
@ -35,11 +35,12 @@ class BubbleableMetadataTest extends UnitTestCase {
|
|||
$data = [];
|
||||
|
||||
$empty_metadata = new BubbleableMetadata();
|
||||
$nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['settings' => ['foo' => 'bar']]);
|
||||
$nonempty_metadata = new BubbleableMetadata(['qux'], ['foo:bar'], ['settings' => ['foo' => 'bar']]);
|
||||
|
||||
$empty_render_array = [];
|
||||
$nonempty_render_array = [
|
||||
'#cache' => [
|
||||
'contexts' => ['qux'],
|
||||
'tags' => ['llamas:are:awesome:but:kittens:too'],
|
||||
],
|
||||
'#attached' => [
|
||||
|
@ -53,7 +54,8 @@ class BubbleableMetadataTest extends UnitTestCase {
|
|||
|
||||
$expected_when_empty_metadata = [
|
||||
'#cache' => [
|
||||
'tags' => []
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
],
|
||||
'#attached' => [],
|
||||
'#post_render_cache' => [],
|
||||
|
@ -61,7 +63,10 @@ class BubbleableMetadataTest extends UnitTestCase {
|
|||
$data[] = [$empty_metadata, $empty_render_array, $expected_when_empty_metadata];
|
||||
$data[] = [$empty_metadata, $nonempty_render_array, $expected_when_empty_metadata];
|
||||
$expected_when_nonempty_metadata = [
|
||||
'#cache' => ['tags' => ['foo:bar']],
|
||||
'#cache' => [
|
||||
'contexts' => ['qux'],
|
||||
'tags' => ['foo:bar'],
|
||||
],
|
||||
'#attached' => [
|
||||
'settings' => [
|
||||
'foo' => 'bar',
|
||||
|
@ -92,11 +97,12 @@ class BubbleableMetadataTest extends UnitTestCase {
|
|||
$data = [];
|
||||
|
||||
$empty_metadata = new BubbleableMetadata();
|
||||
$nonempty_metadata = new BubbleableMetadata(['foo:bar'], ['settings' => ['foo' => 'bar']]);
|
||||
$nonempty_metadata = new BubbleableMetadata(['qux'], ['foo:bar'], ['settings' => ['foo' => 'bar']]);
|
||||
|
||||
$empty_render_array = [];
|
||||
$nonempty_render_array = [
|
||||
'#cache' => [
|
||||
'contexts' => ['qux'],
|
||||
'tags' => ['foo:bar'],
|
||||
],
|
||||
'#attached' => [
|
||||
|
|
|
@ -34,6 +34,9 @@ class RendererBubblingTest extends RendererTestBase {
|
|||
$this->elementInfo->expects($this->any())
|
||||
->method('getInfo')
|
||||
->willReturn([]);
|
||||
$this->cacheContexts->expects($this->any())
|
||||
->method('convertTokensToKeys')
|
||||
->willReturnArgument(0);
|
||||
|
||||
// Create an element with a child and subchild. Each element loads a
|
||||
// different library using #attached.
|
||||
|
@ -67,6 +70,392 @@ class RendererBubblingTest extends RendererTestBase {
|
|||
$this->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests cache context bubbling in edge cases, because it affects the CID.
|
||||
*
|
||||
* ::testBubblingWithPrerender() already tests the common case.
|
||||
*
|
||||
* @dataProvider providerTestContextBubblingEdgeCases
|
||||
*/
|
||||
public function testContextBubblingEdgeCases(array $element, array $expected_top_level_contexts, array $expected_cache_items) {
|
||||
$this->setUpRequest();
|
||||
$this->setupMemoryCache();
|
||||
$this->cacheContexts->expects($this->any())
|
||||
->method('convertTokensToKeys')
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->renderer->render($element);
|
||||
|
||||
$this->assertEquals($expected_top_level_contexts, $element['#cache']['contexts'], 'Expected cache contexts found.');
|
||||
foreach ($expected_cache_items as $cid => $expected_cache_item) {
|
||||
$this->assertRenderCacheItem($cid, $expected_cache_item);
|
||||
}
|
||||
}
|
||||
|
||||
public function providerTestContextBubblingEdgeCases() {
|
||||
$data = [];
|
||||
|
||||
// Bubbled cache contexts cannot override a cache ID set by #cache['cid'].
|
||||
// But the cache context is bubbled nevertheless.
|
||||
$test_element = [
|
||||
'#cache' => [
|
||||
'cid' => 'parent',
|
||||
],
|
||||
'#markup' => 'parent',
|
||||
'child' => [
|
||||
'#cache' => [
|
||||
'contexts' => ['foo'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$expected_cache_items = [
|
||||
'parent' => [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['foo'],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
],
|
||||
];
|
||||
$data[] = [$test_element, ['foo'], $expected_cache_items];
|
||||
|
||||
// Cache contexts of inaccessible children aren't bubbled (because those
|
||||
// children are not rendered at all).
|
||||
$test_element = [
|
||||
'#cache' => [
|
||||
'keys' => ['parent'],
|
||||
'contexts' => [],
|
||||
],
|
||||
'#markup' => 'parent',
|
||||
'child' => [
|
||||
'#access' => FALSE,
|
||||
'#cache' => [
|
||||
'contexts' => ['foo'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$expected_cache_items = [
|
||||
'parent' => [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
],
|
||||
];
|
||||
$data[] = [$test_element, [], $expected_cache_items];
|
||||
|
||||
// Assert cache contexts are sorted when they are used to generate a CID.
|
||||
// (Necessary to ensure that different render arrays where the same keys +
|
||||
// set of contexts are present point to the same cache item. Regardless of
|
||||
// the contexts' order. A sad necessity because PHP doesn't have sets.)
|
||||
$test_element = [
|
||||
'#cache' => [
|
||||
'keys' => ['set_test'],
|
||||
'contexts' => [],
|
||||
],
|
||||
];
|
||||
$expected_cache_items = [
|
||||
'set_test:bar:baz:foo' => [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => '',
|
||||
],
|
||||
];
|
||||
$context_orders = [
|
||||
['foo', 'bar', 'baz'],
|
||||
['foo', 'baz', 'bar'],
|
||||
['bar', 'foo', 'baz'],
|
||||
['bar', 'baz', 'foo'],
|
||||
['baz', 'foo', 'bar'],
|
||||
['baz', 'bar', 'foo'],
|
||||
];
|
||||
foreach ($context_orders as $context_order) {
|
||||
$test_element['#cache']['contexts'] = $context_order;
|
||||
sort($context_order);
|
||||
$expected_cache_items['set_test:bar:baz:foo']['#cache']['contexts'] = $context_order;
|
||||
$data[] = [$test_element, $context_order, $expected_cache_items];
|
||||
}
|
||||
|
||||
// A parent with a certain set of cache contexts is unaffected by a child
|
||||
// that has a subset of those contexts.
|
||||
$test_element = [
|
||||
'#cache' => [
|
||||
'keys' => ['parent'],
|
||||
'contexts' => ['foo', 'bar', 'baz'],
|
||||
],
|
||||
'#markup' => 'parent',
|
||||
'child' => [
|
||||
'#cache' => [
|
||||
'contexts' => ['foo', 'baz'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$expected_cache_items = [
|
||||
'parent:bar:baz:foo' => [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['bar', 'baz', 'foo'],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
],
|
||||
];
|
||||
$data[] = [$test_element, ['bar', 'baz', 'foo'], $expected_cache_items];
|
||||
|
||||
// A parent with a certain set of cache contexts that is a subset of the
|
||||
// cache contexts of a child gets a redirecting cache item for the cache ID
|
||||
// created pre-bubbling (without the child's additional cache contexts). It
|
||||
// points to a cache item with a post-bubbling cache ID (i.e. with the
|
||||
// child's additional cache contexts).
|
||||
// Furthermore, the redirecting cache item also includes the children's
|
||||
// cache tags, since changes in the children may cause those children to get
|
||||
// different cache contexts and therefore cause different cache contexts to
|
||||
// be stored in the redirecting cache item.
|
||||
$test_element = [
|
||||
'#cache' => [
|
||||
'keys' => ['parent'],
|
||||
'contexts' => ['foo'],
|
||||
'tags' => ['yar', 'har'],
|
||||
],
|
||||
'#markup' => 'parent',
|
||||
'child' => [
|
||||
'#cache' => [
|
||||
'contexts' => ['bar'],
|
||||
'tags' => ['fiddle', 'dee'],
|
||||
],
|
||||
'#markup' => '',
|
||||
],
|
||||
];
|
||||
$expected_cache_items = [
|
||||
'parent:foo' => [
|
||||
'#cache_redirect' => TRUE,
|
||||
'#cache' => [
|
||||
// The keys + contexts this redirects to.
|
||||
'keys' => ['parent'],
|
||||
'contexts' => ['bar', 'foo'],
|
||||
// The 'rendered' cache tag is also present for the redirecting cache
|
||||
// item, to ensure it is considered to be part of the render cache
|
||||
// and thus invalidated along with everything else.
|
||||
'tags' => ['dee', 'fiddle', 'har', 'rendered', 'yar'],
|
||||
],
|
||||
],
|
||||
'parent:bar:foo' => [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['bar', 'foo'],
|
||||
'tags' => ['dee', 'fiddle', 'har', 'yar', 'rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
],
|
||||
];
|
||||
$data[] = [$test_element, ['bar', 'foo'], $expected_cache_items];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the self-healing of the redirect with conditional cache contexts.
|
||||
*/
|
||||
public function testConditionalCacheContextBubblingSelfHealing() {
|
||||
global $current_user_role;
|
||||
|
||||
$this->setUpRequest();
|
||||
$this->setupMemoryCache();
|
||||
$this->cacheContexts->expects($this->any())
|
||||
->method('convertTokensToKeys')
|
||||
->willReturnCallback(function($context_tokens) {
|
||||
global $current_user_role;
|
||||
$keys = [];
|
||||
foreach ($context_tokens as $context_id) {
|
||||
if ($context_id === 'user.roles') {
|
||||
$keys[] = 'r.' . $current_user_role;
|
||||
}
|
||||
else {
|
||||
$keys[] = $context_id;
|
||||
}
|
||||
}
|
||||
return $keys;
|
||||
});
|
||||
|
||||
$test_element = [
|
||||
'#cache' => [
|
||||
'keys' => ['parent'],
|
||||
'tags' => ['a'],
|
||||
],
|
||||
'#markup' => 'parent',
|
||||
'child' => [
|
||||
'#cache' => [
|
||||
'contexts' => ['user.roles'],
|
||||
'tags' => ['b'],
|
||||
],
|
||||
'grandchild' => [
|
||||
'#access_callback' => function () {
|
||||
global $current_user_role;
|
||||
// Only role A cannot access this subtree.
|
||||
return $current_user_role !== 'A';
|
||||
},
|
||||
'#cache' => [
|
||||
'contexts' => ['foo'],
|
||||
'tags' => ['c'],
|
||||
],
|
||||
'grandgrandchild' => [
|
||||
'#access_callback' => function () {
|
||||
global $current_user_role;
|
||||
// Only role C can access this subtree.
|
||||
return $current_user_role === 'C';
|
||||
},
|
||||
'#cache' => [
|
||||
'contexts' => ['bar'],
|
||||
'tags' => ['d'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Request 1: role A, the grandchild isn't accessible => bubbled cache
|
||||
// contexts: user.roles.
|
||||
$element = $test_element;
|
||||
$current_user_role = 'A';
|
||||
$this->renderer->render($element);
|
||||
$this->assertRenderCacheItem('parent', [
|
||||
'#cache_redirect' => TRUE,
|
||||
'#cache' => [
|
||||
'keys' => ['parent'],
|
||||
'contexts' => ['user.roles'],
|
||||
'tags' => ['a', 'b', 'rendered'],
|
||||
],
|
||||
]);
|
||||
$this->assertRenderCacheItem('parent:r.A', [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['user.roles'],
|
||||
'tags' => ['a', 'b', 'rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
]);
|
||||
|
||||
// Request 2: role B, the grandchild is accessible => bubbled cache
|
||||
// contexts: foo, user.roles.
|
||||
$element = $test_element;
|
||||
$current_user_role = 'B';
|
||||
$this->renderer->render($element);
|
||||
$this->assertRenderCacheItem('parent', [
|
||||
'#cache_redirect' => TRUE,
|
||||
'#cache' => [
|
||||
'keys' => ['parent'],
|
||||
'contexts' => ['foo', 'user.roles'],
|
||||
'tags' => ['a', 'b', 'c', 'rendered'],
|
||||
],
|
||||
]);
|
||||
$this->assertRenderCacheItem('parent:foo:r.B', [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['foo', 'user.roles'],
|
||||
'tags' => ['a', 'b', 'c', 'rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
]);
|
||||
|
||||
// Request 3: role A again, the grandchild is inaccessible again => bubbled
|
||||
// cache contexts: user.roles; but that's a subset of the already-bubbled
|
||||
// cache contexts, so nothing is actually changed in the redirecting cache
|
||||
// item. However, the cache item we were looking for in request 1 is
|
||||
// technically the same one we're looking for now (it's the exact same
|
||||
// request), but with one additional cache context. This is necessary to
|
||||
// avoid "cache ping-pong". (Requests 1 and 3 are identical, but without the
|
||||
// right merging logic to handle request 2, the redirecting cache item would
|
||||
// toggle between only the 'user.roles' cache context and both the 'foo'
|
||||
// and 'user.roles' cache contexts, resulting in a cache miss every time.)
|
||||
$element = $test_element;
|
||||
$current_user_role = 'A';
|
||||
$this->renderer->render($element);
|
||||
$this->assertRenderCacheItem('parent', [
|
||||
'#cache_redirect' => TRUE,
|
||||
'#cache' => [
|
||||
'keys' => ['parent'],
|
||||
'contexts' => ['foo', 'user.roles'],
|
||||
'tags' => ['a', 'b', 'c', 'rendered'],
|
||||
],
|
||||
]);
|
||||
$this->assertRenderCacheItem('parent:foo:r.A', [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['foo', 'user.roles'],
|
||||
'tags' => ['a', 'b', 'rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
]);
|
||||
|
||||
// Request 4: role C, both the grandchild and the grandgrandchild are
|
||||
// accessible => bubbled cache contexts: foo, bar, user.roles.
|
||||
$element = $test_element;
|
||||
$current_user_role = 'C';
|
||||
$this->renderer->render($element);
|
||||
$final_parent_cache_item = [
|
||||
'#cache_redirect' => TRUE,
|
||||
'#cache' => [
|
||||
'keys' => ['parent'],
|
||||
'contexts' => ['bar', 'foo', 'user.roles'],
|
||||
'tags' => ['a', 'b', 'c', 'd', 'rendered'],
|
||||
],
|
||||
];
|
||||
$this->assertRenderCacheItem('parent', $final_parent_cache_item);
|
||||
$this->assertRenderCacheItem('parent:bar:foo:r.C', [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['bar', 'foo', 'user.roles'],
|
||||
'tags' => ['a', 'b', 'c', 'd', 'rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
]);
|
||||
|
||||
// Request 5: role A again, verifying the merging like we did for request 3.
|
||||
$element = $test_element;
|
||||
$current_user_role = 'A';
|
||||
$this->renderer->render($element);
|
||||
$this->assertRenderCacheItem('parent', $final_parent_cache_item);
|
||||
$this->assertRenderCacheItem('parent:bar:foo:r.A', [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['bar', 'foo', 'user.roles'],
|
||||
'tags' => ['a', 'b', 'rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
]);
|
||||
|
||||
// Request 6: role B again, verifying the merging like we did for request 3.
|
||||
$element = $test_element;
|
||||
$current_user_role = 'B';
|
||||
$this->renderer->render($element);
|
||||
$this->assertRenderCacheItem('parent', $final_parent_cache_item);
|
||||
$this->assertRenderCacheItem('parent:bar:foo:r.B', [
|
||||
'#attached' => [],
|
||||
'#cache' => [
|
||||
'contexts' => ['bar', 'foo', 'user.roles'],
|
||||
'tags' => ['a', 'b', 'c', 'rendered'],
|
||||
],
|
||||
'#post_render_cache' => [],
|
||||
'#markup' => 'parent',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests bubbling of bubbleable metadata added by #pre_render callbacks.
|
||||
*
|
||||
|
@ -94,13 +483,14 @@ class RendererBubblingTest extends RendererTestBase {
|
|||
// - … is not cached DOES get called.
|
||||
\Drupal::state()->set('bubbling_nested_pre_render_cached', FALSE);
|
||||
\Drupal::state()->set('bubbling_nested_pre_render_uncached', FALSE);
|
||||
$this->memoryCache->set('cached_nested', ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['tags' => []], '#post_render_cache' => []]);
|
||||
$this->memoryCache->set('cached_nested', ['#markup' => 'Cached nested!', '#attached' => [], '#cache' => ['contexts' => [], 'tags' => []], '#post_render_cache' => []]);
|
||||
|
||||
// Simulate the rendering of an entire response (i.e. a root call).
|
||||
$output = $this->renderer->renderRoot($test_element);
|
||||
|
||||
// First, assert the render array is of the expected form.
|
||||
$this->assertEquals('Cache tag!Asset!Post-render cache!barquxNested!Cached nested!', trim($output), 'Expected HTML generated.');
|
||||
$this->assertEquals('Cache context!Cache tag!Asset!Post-render cache!barquxNested!Cached nested!', trim($output), 'Expected HTML generated.');
|
||||
$this->assertEquals(['child.cache_context'], $test_element['#cache']['contexts'], 'Expected cache contexts found.');
|
||||
$this->assertEquals(['child:cache_tag'], $test_element['#cache']['tags'], 'Expected cache tags found.');
|
||||
$expected_attached = [
|
||||
'drupalSettings' => ['foo' => 'bar'],
|
||||
|
@ -155,6 +545,12 @@ class BubblingTest {
|
|||
];
|
||||
$placeholder = \Drupal::service('renderer')->generateCachePlaceholder($callback, $context);
|
||||
$elements += [
|
||||
'child_cache_context' => [
|
||||
'#cache' => [
|
||||
'contexts' => ['child.cache_context'],
|
||||
],
|
||||
'#markup' => 'Cache context!',
|
||||
],
|
||||
'child_cache_tag' => [
|
||||
'#cache' => [
|
||||
'tags' => ['child:cache_tag'],
|
||||
|
|
|
@ -89,7 +89,10 @@ class RendererPostRenderCacheTest extends RendererTestBase {
|
|||
'#markup' => '<p>#cache enabled, GET</p>',
|
||||
'#attached' => $test_element['#attached'],
|
||||
'#post_render_cache' => $test_element['#post_render_cache'],
|
||||
'#cache' => ['tags' => ['rendered']],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
];
|
||||
$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.');
|
||||
|
||||
|
@ -222,7 +225,10 @@ class RendererPostRenderCacheTest extends RendererTestBase {
|
|||
$context_3,
|
||||
]
|
||||
],
|
||||
'#cache' => ['tags' => ['rendered']],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
];
|
||||
|
||||
$dom = Html::load($cached_element['#markup']);
|
||||
|
@ -314,7 +320,10 @@ class RendererPostRenderCacheTest extends RendererTestBase {
|
|||
$context_3,
|
||||
]
|
||||
],
|
||||
'#cache' => ['tags' => ['rendered']],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
];
|
||||
|
||||
$dom = Html::load($cached_parent_element['#markup']);
|
||||
|
@ -337,7 +346,10 @@ class RendererPostRenderCacheTest extends RendererTestBase {
|
|||
$context_3,
|
||||
]
|
||||
],
|
||||
'#cache' => ['tags' => ['rendered']],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
];
|
||||
|
||||
$dom = Html::load($cached_child_element['#markup']);
|
||||
|
@ -448,7 +460,10 @@ class RendererPostRenderCacheTest extends RendererTestBase {
|
|||
$context
|
||||
],
|
||||
],
|
||||
'#cache' => ['tags' => ['rendered']],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
];
|
||||
$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.');
|
||||
|
||||
|
@ -543,7 +558,10 @@ class RendererPostRenderCacheTest extends RendererTestBase {
|
|||
$context,
|
||||
],
|
||||
],
|
||||
'#cache' => ['tags' => ['rendered']],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
];
|
||||
$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.');
|
||||
|
||||
|
@ -568,7 +586,10 @@ class RendererPostRenderCacheTest extends RendererTestBase {
|
|||
$context,
|
||||
],
|
||||
],
|
||||
'#cache' => ['tags' => ['rendered']],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
];
|
||||
$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.');
|
||||
|
||||
|
@ -596,7 +617,10 @@ class RendererPostRenderCacheTest extends RendererTestBase {
|
|||
$context,
|
||||
],
|
||||
],
|
||||
'#cache' => ['tags' => ['rendered']],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
];
|
||||
$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.');
|
||||
|
||||
|
|
|
@ -17,7 +17,10 @@ use Drupal\Core\Template\Attribute;
|
|||
class RendererTest extends RendererTestBase {
|
||||
|
||||
protected $defaultThemeVars = [
|
||||
'#cache' => ['tags' => []],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
],
|
||||
'#attached' => [],
|
||||
'#post_render_cache' => [],
|
||||
'#children' => '',
|
||||
|
|
|
@ -139,6 +139,22 @@ class RendererTestBase extends UnitTestCase {
|
|||
$this->requestStack->push($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a render cache item.
|
||||
*
|
||||
* @param string $cid
|
||||
* The expected cache ID.
|
||||
* @param mixed $data
|
||||
* The expected data for that cache ID.
|
||||
*/
|
||||
protected function assertRenderCacheItem($cid, $data) {
|
||||
$cached = $this->memoryCache->get($cid);
|
||||
$this->assertNotFalse($cached, sprintf('Expected cache item "%s" exists.', $cid));
|
||||
if ($cached !== FALSE) {
|
||||
$this->assertEquals($data, $cached->data, sprintf('Cache item "%s" has the expected data.', $cid));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue