diff core/lib/Drupal/Core/Render/RenderCache.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 129ea1e6d783
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/lib/Drupal/Core/Render/RenderCache.php	Wed Nov 29 16:09:58 2017 +0000
@@ -0,0 +1,358 @@
+<?php
+
+namespace Drupal\Core\Render;
+
+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 isMethodCacheable() check when
+    //       https://www.drupal.org/node/2367555 lands.
+    if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$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 isMethodCacheable() check when
+    //       https://www.drupal.org/node/2367555 lands.
+    if (!$this->requestStack->getCurrentRequest()->isMethodCacheable() || !$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'];
+
+      // 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;
+  }
+
+}