You are here

public function RenderCache::set in Drupal 8

Same name and namespace in other branches
  1. 9 core/lib/Drupal/Core/Render/RenderCache.php \Drupal\Core\Render\RenderCache::set()

Caches the rendered output of a renderable array.

May be called by an implementation of \Drupal\Core\Render\RendererInterface while rendering, if the #cache property is set.

Parameters

array $elements: A renderable array.

array $pre_bubbling_elements: A renderable array corresponding to the state (in particular, the cacheability metadata) of $elements prior to the beginning of its rendering process, and therefore before any bubbling of child information has taken place. Only the #cache property is used by this function, so the caller may omit all other properties and children from this array.

Return value

bool|null Returns FALSE if no cache item could be created, NULL otherwise.

Overrides RenderCacheInterface::set

See also

::get()

1 call to RenderCache::set()
PlaceholderingRenderCache::set in core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
Caches the rendered output of a renderable array.
1 method overrides RenderCache::set()
PlaceholderingRenderCache::set in core/lib/Drupal/Core/Render/PlaceholderingRenderCache.php
Caches the rendered output of a renderable array.

File

core/lib/Drupal/Core/Render/RenderCache.php, line 88

Class

RenderCache
Wraps the caching logic for the render caching system.

Namespace

Drupal\Core\Render

Code

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',
  ]));
}