annotate core/lib/Drupal/Core/Render/RenderCache.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\Render;
Chris@0 4
Chris@0 5 use Drupal\Core\Cache\Cache;
Chris@0 6 use Drupal\Core\Cache\CacheableMetadata;
Chris@0 7 use Drupal\Core\Cache\Context\CacheContextsManager;
Chris@0 8 use Drupal\Core\Cache\CacheFactoryInterface;
Chris@0 9 use Symfony\Component\HttpFoundation\RequestStack;
Chris@0 10
Chris@0 11 /**
Chris@0 12 * Wraps the caching logic for the render caching system.
Chris@0 13 *
Chris@0 14 * @internal
Chris@0 15 *
Chris@0 16 * @todo Refactor this out into a generic service capable of cache redirects,
Chris@0 17 * and let RenderCache use that. https://www.drupal.org/node/2551419
Chris@0 18 */
Chris@0 19 class RenderCache implements RenderCacheInterface {
Chris@0 20
Chris@0 21 /**
Chris@0 22 * The request stack.
Chris@0 23 *
Chris@0 24 * @var \Symfony\Component\HttpFoundation\RequestStack
Chris@0 25 */
Chris@0 26 protected $requestStack;
Chris@0 27
Chris@0 28 /**
Chris@0 29 * The cache factory.
Chris@0 30 *
Chris@0 31 * @var \Drupal\Core\Cache\CacheFactoryInterface
Chris@0 32 */
Chris@0 33 protected $cacheFactory;
Chris@0 34
Chris@0 35 /**
Chris@0 36 * The cache contexts manager.
Chris@0 37 *
Chris@0 38 * @var \Drupal\Core\Cache\Context\CacheContextsManager
Chris@0 39 */
Chris@0 40 protected $cacheContextsManager;
Chris@0 41
Chris@0 42 /**
Chris@0 43 * Constructs a new RenderCache object.
Chris@0 44 *
Chris@0 45 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
Chris@0 46 * The request stack.
Chris@0 47 * @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory
Chris@0 48 * The cache factory.
Chris@0 49 * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
Chris@0 50 * The cache contexts manager.
Chris@0 51 */
Chris@0 52 public function __construct(RequestStack $request_stack, CacheFactoryInterface $cache_factory, CacheContextsManager $cache_contexts_manager) {
Chris@0 53 $this->requestStack = $request_stack;
Chris@0 54 $this->cacheFactory = $cache_factory;
Chris@0 55 $this->cacheContextsManager = $cache_contexts_manager;
Chris@0 56 }
Chris@0 57
Chris@0 58 /**
Chris@0 59 * {@inheritdoc}
Chris@0 60 */
Chris@0 61 public function get(array $elements) {
Chris@0 62 // Form submissions rely on the form being built during the POST request,
Chris@0 63 // and render caching of forms prevents this from happening.
Chris@0 64 // @todo remove the isMethodCacheable() check when
Chris@0 65 // https://www.drupal.org/node/2367555 lands.
Chris@0 66 if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$cid = $this->createCacheID($elements)) {
Chris@0 67 return FALSE;
Chris@0 68 }
Chris@0 69 $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
Chris@0 70
Chris@0 71 if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) {
Chris@0 72 $cached_element = $cache->data;
Chris@0 73 // Two-tier caching: redirect to actual (post-bubbling) cache item.
Chris@0 74 // @see \Drupal\Core\Render\RendererInterface::render()
Chris@0 75 // @see ::set()
Chris@0 76 if (isset($cached_element['#cache_redirect'])) {
Chris@0 77 return $this->get($cached_element);
Chris@0 78 }
Chris@0 79 // Return the cached element.
Chris@0 80 return $cached_element;
Chris@0 81 }
Chris@0 82 return FALSE;
Chris@0 83 }
Chris@0 84
Chris@0 85 /**
Chris@0 86 * {@inheritdoc}
Chris@0 87 */
Chris@0 88 public function set(array &$elements, array $pre_bubbling_elements) {
Chris@0 89 // Form submissions rely on the form being built during the POST request,
Chris@0 90 // and render caching of forms prevents this from happening.
Chris@0 91 // @todo remove the isMethodCacheable() check when
Chris@0 92 // https://www.drupal.org/node/2367555 lands.
Chris@0 93 if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$cid = $this->createCacheID($elements)) {
Chris@0 94 return FALSE;
Chris@0 95 }
Chris@0 96
Chris@0 97 $data = $this->getCacheableRenderArray($elements);
Chris@0 98
Chris@0 99 $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
Chris@0 100 $cache = $this->cacheFactory->get($bin);
Chris@0 101
Chris@0 102 // Calculate the pre-bubbling CID.
Chris@0 103 $pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements);
Chris@0 104
Chris@0 105 // Two-tier caching: detect different CID post-bubbling, create redirect,
Chris@0 106 // update redirect if different set of cache contexts.
Chris@0 107 // @see \Drupal\Core\Render\RendererInterface::render()
Chris@0 108 // @see ::get()
Chris@0 109 if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) {
Chris@0 110 // The cache redirection strategy we're implementing here is pretty
Chris@0 111 // simple in concept. Suppose we have the following render structure:
Chris@0 112 // - A (pre-bubbling, specifies #cache['keys'] = ['foo'])
Chris@0 113 // -- B (specifies #cache['contexts'] = ['b'])
Chris@0 114 //
Chris@0 115 // At the time that we're evaluating whether A's rendering can be
Chris@0 116 // retrieved from cache, we won't know the contexts required by its
Chris@0 117 // children (the children might not even be built yet), so cacheGet()
Chris@0 118 // will only be able to get what is cached for a $cid of 'foo'. But at
Chris@0 119 // the time we're writing to that cache, we do know all the contexts that
Chris@0 120 // were specified by all children, so what we need is a way to
Chris@0 121 // persist that information between the cache write and the next cache
Chris@0 122 // read. So, what we can do is store the following into 'foo':
Chris@0 123 // [
Chris@0 124 // '#cache_redirect' => TRUE,
Chris@0 125 // '#cache' => [
Chris@0 126 // ...
Chris@0 127 // 'contexts' => ['b'],
Chris@0 128 // ],
Chris@0 129 // ]
Chris@0 130 //
Chris@0 131 // This efficiently lets cacheGet() redirect to a $cid that includes all
Chris@0 132 // of the required contexts. The strategy is on-demand: in the case where
Chris@0 133 // there aren't any additional contexts required by children that aren't
Chris@0 134 // already included in the parent's pre-bubbled #cache information, no
Chris@0 135 // cache redirection is needed.
Chris@0 136 //
Chris@0 137 // When implementing this redirection strategy, special care is needed to
Chris@0 138 // resolve potential cache ping-pong problems. For example, consider the
Chris@0 139 // following render structure:
Chris@0 140 // - A (pre-bubbling, specifies #cache['keys'] = ['foo'])
Chris@0 141 // -- B (pre-bubbling, specifies #cache['contexts'] = ['b'])
Chris@0 142 // --- C (pre-bubbling, specifies #cache['contexts'] = ['c'])
Chris@0 143 // --- D (pre-bubbling, specifies #cache['contexts'] = ['d'])
Chris@0 144 //
Chris@0 145 // Additionally, suppose that:
Chris@0 146 // - C only exists for a 'b' context value of 'b1'
Chris@0 147 // - D only exists for a 'b' context value of 'b2'
Chris@0 148 // This is an acceptable variation, since B specifies that its contents
Chris@0 149 // vary on context 'b'.
Chris@0 150 //
Chris@0 151 // A naive implementation of cache redirection would result in the
Chris@0 152 // following:
Chris@0 153 // - When a request is processed where context 'b' = 'b1', what would be
Chris@0 154 // cached for a $pre_bubbling_cid of 'foo' is:
Chris@0 155 // [
Chris@0 156 // '#cache_redirect' => TRUE,
Chris@0 157 // '#cache' => [
Chris@0 158 // ...
Chris@0 159 // 'contexts' => ['b', 'c'],
Chris@0 160 // ],
Chris@0 161 // ]
Chris@0 162 // - When a request is processed where context 'b' = 'b2', we would
Chris@0 163 // retrieve the above from cache, but when following that redirection,
Chris@0 164 // get a cache miss, since we're processing a 'b' context value that
Chris@0 165 // has not yet been cached. Given the cache miss, we would continue
Chris@0 166 // with rendering the structure, perform the required context bubbling
Chris@0 167 // and then overwrite the above item with:
Chris@0 168 // [
Chris@0 169 // '#cache_redirect' => TRUE,
Chris@0 170 // '#cache' => [
Chris@0 171 // ...
Chris@0 172 // 'contexts' => ['b', 'd'],
Chris@0 173 // ],
Chris@0 174 // ]
Chris@0 175 // - Now, if a request comes in where context 'b' = 'b1' again, the above
Chris@0 176 // would redirect to a cache key that doesn't exist, since we have not
Chris@0 177 // yet cached an item that includes 'b'='b1' and something for 'd'. So
Chris@0 178 // we would process this request as a cache miss, at the end of which,
Chris@0 179 // we would overwrite the above item back to:
Chris@0 180 // [
Chris@0 181 // '#cache_redirect' => TRUE,
Chris@0 182 // '#cache' => [
Chris@0 183 // ...
Chris@0 184 // 'contexts' => ['b', 'c'],
Chris@0 185 // ],
Chris@0 186 // ]
Chris@0 187 // - The above would always result in accurate renderings, but would
Chris@0 188 // result in poor performance as we keep processing requests as cache
Chris@0 189 // misses even though the target of the redirection is cached, and
Chris@0 190 // it's only the redirection element itself that is creating the
Chris@0 191 // ping-pong problem.
Chris@0 192 //
Chris@0 193 // A way to resolve the ping-pong problem is to eventually reach a cache
Chris@0 194 // state where the redirection element includes all of the contexts used
Chris@0 195 // throughout all requests:
Chris@0 196 // [
Chris@0 197 // '#cache_redirect' => TRUE,
Chris@0 198 // '#cache' => [
Chris@0 199 // ...
Chris@0 200 // 'contexts' => ['b', 'c', 'd'],
Chris@0 201 // ],
Chris@0 202 // ]
Chris@0 203 //
Chris@0 204 // We can't reach that state right away, since we don't know what the
Chris@0 205 // result of future requests will be, but we can incrementally move
Chris@0 206 // towards that state by progressively merging the 'contexts' value
Chris@0 207 // across requests. That's the strategy employed below and tested in
Chris@0 208 // \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing().
Chris@0 209
Chris@0 210 // Get the cacheability of this element according to the current (stored)
Chris@0 211 // redirecting cache item, if any.
Chris@0 212 $redirect_cacheability = new CacheableMetadata();
Chris@0 213 if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) {
Chris@0 214 $redirect_cacheability = CacheableMetadata::createFromRenderArray($stored_cache_redirect->data);
Chris@0 215 }
Chris@0 216
Chris@0 217 // Calculate the union of the cacheability for this request and the
Chris@0 218 // current (stored) redirecting cache item. We need:
Chris@0 219 // - the union of cache contexts, because that is how we know which cache
Chris@0 220 // item to redirect to;
Chris@0 221 // - the union of cache tags, because that is how we know when the cache
Chris@0 222 // redirect cache item itself is invalidated;
Chris@0 223 // - the union of max ages, because that is how we know when the cache
Chris@0 224 // redirect cache item itself becomes stale. (Without this, we might end
Chris@0 225 // up toggling between a permanently and a briefly cacheable cache
Chris@0 226 // redirect, because the last update's max-age would always "win".)
Chris@0 227 $redirect_cacheability_updated = CacheableMetadata::createFromRenderArray($data)->merge($redirect_cacheability);
Chris@0 228
Chris@0 229 // Stored cache contexts incomplete: this request causes cache contexts to
Chris@0 230 // be added to the redirecting cache item.
Chris@0 231 if (array_diff($redirect_cacheability_updated->getCacheContexts(), $redirect_cacheability->getCacheContexts())) {
Chris@0 232 $redirect_data = [
Chris@0 233 '#cache_redirect' => TRUE,
Chris@0 234 '#cache' => [
Chris@0 235 // The cache keys of the current element; this remains the same
Chris@0 236 // across requests.
Chris@0 237 'keys' => $elements['#cache']['keys'],
Chris@0 238 // The union of the current element's and stored cache contexts.
Chris@0 239 'contexts' => $redirect_cacheability_updated->getCacheContexts(),
Chris@0 240 // The union of the current element's and stored cache tags.
Chris@0 241 'tags' => $redirect_cacheability_updated->getCacheTags(),
Chris@0 242 // The union of the current element's and stored cache max-ages.
Chris@0 243 'max-age' => $redirect_cacheability_updated->getCacheMaxAge(),
Chris@0 244 // The same cache bin as the one for the actual render cache items.
Chris@0 245 'bin' => $bin,
Chris@0 246 ],
Chris@0 247 ];
Chris@0 248 $cache->set($pre_bubbling_cid, $redirect_data, $this->maxAgeToExpire($redirect_cacheability_updated->getCacheMaxAge()), Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
Chris@0 249 }
Chris@0 250
Chris@0 251 // Current cache contexts incomplete: this request only uses a subset of
Chris@0 252 // the cache contexts stored in the redirecting cache item. Vary by these
Chris@0 253 // additional (conditional) cache contexts as well, otherwise the
Chris@0 254 // redirecting cache item would be pointing to a cache item that can never
Chris@0 255 // exist.
Chris@0 256 if (array_diff($redirect_cacheability_updated->getCacheContexts(), $data['#cache']['contexts'])) {
Chris@0 257 // Recalculate the cache ID.
Chris@0 258 $recalculated_cid_pseudo_element = [
Chris@0 259 '#cache' => [
Chris@0 260 'keys' => $elements['#cache']['keys'],
Chris@0 261 'contexts' => $redirect_cacheability_updated->getCacheContexts(),
Chris@17 262 ],
Chris@0 263 ];
Chris@0 264 $cid = $this->createCacheID($recalculated_cid_pseudo_element);
Chris@0 265 // Ensure the about-to-be-cached data uses the merged cache contexts.
Chris@0 266 $data['#cache']['contexts'] = $redirect_cacheability_updated->getCacheContexts();
Chris@0 267 }
Chris@0 268 }
Chris@0 269 $cache->set($cid, $data, $this->maxAgeToExpire($elements['#cache']['max-age']), Cache::mergeTags($data['#cache']['tags'], ['rendered']));
Chris@0 270 }
Chris@0 271
Chris@0 272 /**
Chris@0 273 * Maps a #cache[max-age] value to an "expire" value for the Cache API.
Chris@0 274 *
Chris@0 275 * @param int $max_age
Chris@0 276 * A #cache[max-age] value.
Chris@0 277 *
Chris@0 278 * @return int
Chris@0 279 * A corresponding "expire" value.
Chris@0 280 *
Chris@0 281 * @see \Drupal\Core\Cache\CacheBackendInterface::set()
Chris@0 282 */
Chris@0 283 protected function maxAgeToExpire($max_age) {
Chris@0 284 return ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $max_age;
Chris@0 285 }
Chris@0 286
Chris@0 287 /**
Chris@0 288 * Creates the cache ID for a renderable element.
Chris@0 289 *
Chris@0 290 * Creates the cache ID string based on #cache['keys'] + #cache['contexts'].
Chris@0 291 *
Chris@0 292 * @param array &$elements
Chris@0 293 * A renderable array.
Chris@0 294 *
Chris@0 295 * @return string
Chris@0 296 * The cache ID string, or FALSE if the element may not be cached.
Chris@0 297 */
Chris@0 298 protected function createCacheID(array &$elements) {
Chris@0 299 // If the maximum age is zero, then caching is effectively prohibited.
Chris@0 300 if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) {
Chris@0 301 return FALSE;
Chris@0 302 }
Chris@0 303
Chris@0 304 if (isset($elements['#cache']['keys'])) {
Chris@0 305 $cid_parts = $elements['#cache']['keys'];
Chris@0 306 if (!empty($elements['#cache']['contexts'])) {
Chris@0 307 $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']);
Chris@0 308 $cid_parts = array_merge($cid_parts, $context_cache_keys->getKeys());
Chris@0 309 CacheableMetadata::createFromRenderArray($elements)
Chris@0 310 ->merge($context_cache_keys)
Chris@0 311 ->applyTo($elements);
Chris@0 312 }
Chris@0 313 return implode(':', $cid_parts);
Chris@0 314 }
Chris@0 315 return FALSE;
Chris@0 316 }
Chris@0 317
Chris@0 318 /**
Chris@0 319 * {@inheritdoc}
Chris@0 320 */
Chris@0 321 public function getCacheableRenderArray(array $elements) {
Chris@0 322 $data = [
Chris@0 323 '#markup' => $elements['#markup'],
Chris@0 324 '#attached' => $elements['#attached'],
Chris@0 325 '#cache' => [
Chris@0 326 'contexts' => $elements['#cache']['contexts'],
Chris@0 327 'tags' => $elements['#cache']['tags'],
Chris@0 328 'max-age' => $elements['#cache']['max-age'],
Chris@0 329 ],
Chris@0 330 ];
Chris@0 331
Chris@0 332 // Preserve cacheable items if specified. If we are preserving any cacheable
Chris@0 333 // children of the element, we assume we are only interested in their
Chris@0 334 // individual markup and not the parent's one, thus we empty it to minimize
Chris@0 335 // the cache entry size.
Chris@0 336 if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) {
Chris@0 337 $data['#cache_properties'] = $elements['#cache_properties'];
Chris@0 338
Chris@0 339 // Extract all the cacheable items from the element using cache
Chris@0 340 // properties.
Chris@0 341 $cacheable_items = array_intersect_key($elements, array_flip($elements['#cache_properties']));
Chris@0 342 $cacheable_children = Element::children($cacheable_items);
Chris@0 343 if ($cacheable_children) {
Chris@0 344 $data['#markup'] = '';
Chris@0 345 // Cache only cacheable children's markup.
Chris@0 346 foreach ($cacheable_children as $key) {
Chris@0 347 // We can assume that #markup is safe at this point.
Chris@0 348 $cacheable_items[$key] = ['#markup' => Markup::create($cacheable_items[$key]['#markup'])];
Chris@0 349 }
Chris@0 350 }
Chris@0 351 $data += $cacheable_items;
Chris@0 352 }
Chris@0 353
Chris@0 354 $data['#markup'] = Markup::create($data['#markup']);
Chris@0 355 return $data;
Chris@0 356 }
Chris@0 357
Chris@0 358 }