Version 1
[yaffs-website] / web / core / modules / dynamic_page_cache / src / EventSubscriber / DynamicPageCacheSubscriber.php
diff --git a/web/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php b/web/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
new file mode 100644 (file)
index 0000000..22a1f2c
--- /dev/null
@@ -0,0 +1,323 @@
+<?php
+
+namespace Drupal\dynamic_page_cache\EventSubscriber;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Render\RenderCacheInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Returns cached responses as early and avoiding as much work as possible.
+ *
+ * Dynamic Page Cache is able to cache so much because it utilizes cache
+ * contexts: the cache contexts that are present capture the variations of every
+ * component of the page. That, combined with the fact that cacheability
+ * metadata is bubbled, means that the cache contexts at the page level
+ * represent the complete set of contexts that the page varies by.
+ *
+ * The reason Dynamic Page Cache is implemented as two event subscribers (a late
+ * REQUEST subscriber immediately after routing for cache hits, and an early
+ * RESPONSE subscriber for cache misses) is because many cache contexts can only
+ * be evaluated after routing. (Examples: 'user', 'user.permissions', 'route' …)
+ * Consequently, it is impossible to implement Dynamic Page Cache as a kernel
+ * middleware that simply caches per URL.
+ *
+ * @see \Drupal\Core\Render\MainContent\HtmlRenderer
+ * @see \Drupal\Core\Cache\CacheableResponseInterface
+ */
+class DynamicPageCacheSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Name of Dynamic Page Cache's response header.
+   */
+  const HEADER = 'X-Drupal-Dynamic-Cache';
+
+  /**
+   * A request policy rule determining the cacheability of a response.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyInterface
+   */
+  protected $requestPolicy;
+
+  /**
+   * A response policy rule determining the cacheability of the response.
+   *
+   * @var \Drupal\Core\PageCache\ResponsePolicyInterface
+   */
+  protected $responsePolicy;
+
+  /**
+   * The render cache.
+   *
+   * @var \Drupal\Core\Render\RenderCacheInterface
+   */
+  protected $renderCache;
+
+  /**
+   * The renderer configuration array.
+   *
+   * @var array
+   */
+  protected $rendererConfig;
+
+  /**
+   * Dynamic Page Cache's redirect render array.
+   *
+   * @var array
+   */
+  protected $dynamicPageCacheRedirectRenderArray = [
+    '#cache' => [
+      'keys' => ['response'],
+      'contexts' => [
+        'route',
+        // Some routes' controllers rely on the request format (they don't have
+        // a separate route for each request format). Additionally, a controller
+        // may be returning a domain object that a KernelEvents::VIEW subscriber
+        // must turn into an actual response, but perhaps a format is being
+        // requested that the subscriber does not support.
+        // @see \Drupal\Core\EventSubscriber\AcceptNegotiation406::onViewDetect406()
+        'request_format',
+      ],
+      'bin' => 'dynamic_page_cache',
+    ],
+  ];
+
+  /**
+   * Internal cache of request policy results.
+   *
+   * @var \SplObjectStorage
+   */
+  protected $requestPolicyResults;
+
+  /**
+   * Constructs a new DynamicPageCacheSubscriber object.
+   *
+   * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
+   *   A policy rule determining the cacheability of a request.
+   * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
+   *   A policy rule determining the cacheability of the response.
+   * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
+   *   The render cache.
+   * @param array $renderer_config
+   *   The renderer configuration array.
+   */
+  public function __construct(RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, RenderCacheInterface $render_cache, array $renderer_config) {
+    $this->requestPolicy = $request_policy;
+    $this->responsePolicy = $response_policy;
+    $this->renderCache = $render_cache;
+    $this->rendererConfig = $renderer_config;
+    $this->requestPolicyResults = new \SplObjectStorage();
+  }
+
+  /**
+   * Sets a response in case of a Dynamic Page Cache hit.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
+   *   The event to process.
+   */
+  public function onRouteMatch(GetResponseEvent $event) {
+    // Don't cache the response if the Dynamic Page Cache request policies are
+    // not met. Store the result in a static keyed by current request, so that
+    // onResponse() does not have to redo the request policy check.
+    $request = $event->getRequest();
+    $request_policy_result = $this->requestPolicy->check($request);
+    $this->requestPolicyResults[$request] = $request_policy_result;
+    if ($request_policy_result === RequestPolicyInterface::DENY) {
+      return;
+    }
+
+    // Sets the response for the current route, if cached.
+    $cached = $this->renderCache->get($this->dynamicPageCacheRedirectRenderArray);
+    if ($cached) {
+      $response = $this->renderArrayToResponse($cached);
+      $response->headers->set(self::HEADER, 'HIT');
+      $event->setResponse($response);
+    }
+  }
+
+  /**
+   * Stores a response in case of a Dynamic Page Cache miss, if cacheable.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+
+    // Dynamic Page Cache only works with cacheable responses. It does not work
+    // with plain Response objects. (Dynamic Page Cache needs to be able to
+    // access and modify the cacheability metadata associated with the
+    // response.)
+    if (!$response instanceof CacheableResponseInterface) {
+      return;
+    }
+
+    // There's no work left to be done if this is a Dynamic Page Cache hit.
+    if ($response->headers->get(self::HEADER) === 'HIT') {
+      return;
+    }
+
+    // There's no work left to be done if this is an uncacheable response.
+    if (!$this->shouldCacheResponse($response)) {
+      // The response is uncacheable, mark it as such.
+      $response->headers->set(self::HEADER, 'UNCACHEABLE');
+      return;
+    }
+
+    // Don't cache the response if Dynamic Page Cache's request subscriber did
+    // not fire, because that means it is impossible to have a Dynamic Page
+    // Cache hit. This can happen when the master request is for example a 403
+    // or 404, in which case a subrequest is performed by the router. In that
+    // case, it is the subrequest's response that is cached by Dynamic Page
+    // Cache, because the routing happens in a request subscriber earlier than
+    // Dynamic Page Cache's and immediately sets a response, i.e. the one
+    // returned by the subrequest, and thus causes Dynamic Page Cache's request
+    // subscriber to not fire for the master request.
+    // @see \Drupal\Core\Routing\AccessAwareRouter::checkAccess()
+    // @see \Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber::on403()
+    $request = $event->getRequest();
+    if (!isset($this->requestPolicyResults[$request])) {
+      return;
+    }
+
+    // Don't cache the response if the Dynamic Page Cache request & response
+    // policies are not met.
+    // @see onRouteMatch()
+    if ($this->requestPolicyResults[$request] === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) {
+      return;
+    }
+
+    // Embed the response object in a render array so that RenderCache is able
+    // to cache it, handling cache redirection for us.
+    $response_as_render_array = $this->responseToRenderArray($response);
+    $this->renderCache->set($response_as_render_array, $this->dynamicPageCacheRedirectRenderArray);
+
+    // The response was generated, mark the response as a cache miss. The next
+    // time, it will be a cache hit.
+    $response->headers->set(self::HEADER, 'MISS');
+  }
+
+  /**
+   * Whether the given response should be cached by Dynamic Page Cache.
+   *
+   * We consider any response that has cacheability metadata meeting the auto-
+   * placeholdering conditions to be uncacheable. Because those conditions
+   * indicate poor cacheability, and if it doesn't make sense to cache parts of
+   * a page, then neither does it make sense to cache an entire page.
+   *
+   * But note that auto-placeholdering avoids such cacheability metadata ever
+   * bubbling to the response level: while rendering, the Renderer checks every
+   * subtree to see if meets the auto-placeholdering conditions. If it does, it
+   * is automatically placeholdered, and consequently the cacheability metadata
+   * of the placeholdered content does not bubble up to the response level.
+   *
+   * @param \Drupal\Core\Cache\CacheableResponseInterface $response
+   *   The response whose cacheability to analyze.
+   *
+   * @return bool
+   *   Whether the given response should be cached.
+   *
+   * @see \Drupal\Core\Render\Renderer::shouldAutomaticallyPlaceholder()
+   */
+  protected function shouldCacheResponse(CacheableResponseInterface $response) {
+    $conditions = $this->rendererConfig['auto_placeholder_conditions'];
+
+    $cacheability = $response->getCacheableMetadata();
+
+    // Response's max-age is at or below the configured threshold.
+    if ($cacheability->getCacheMaxAge() !== Cache::PERMANENT && $cacheability->getCacheMaxAge() <= $conditions['max-age']) {
+      return FALSE;
+    }
+
+    // Response has a high-cardinality cache context.
+    if (array_intersect($cacheability->getCacheContexts(), $conditions['contexts'])) {
+      return FALSE;
+    }
+
+    // Response has a high-invalidation frequency cache tag.
+    if (array_intersect($cacheability->getCacheTags(), $conditions['tags'])) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Embeds a Response object in a render array so that RenderCache can cache it.
+   *
+   * @param \Drupal\Core\Cache\CacheableResponseInterface $response
+   *   A cacheable response.
+   *
+   * @return array
+   *   A render array that embeds the given cacheable response object, with the
+   *   cacheability metadata of the response object present in the #cache
+   *   property of the render array.
+   *
+   * @see renderArrayToResponse()
+   *
+   * @todo Refactor/remove once https://www.drupal.org/node/2551419 lands.
+   */
+  protected function responseToRenderArray(CacheableResponseInterface $response) {
+    $response_as_render_array = $this->dynamicPageCacheRedirectRenderArray + [
+      // The data we actually care about.
+      '#response' => $response,
+      // Tell RenderCache to cache the #response property: the data we actually
+      // care about.
+      '#cache_properties' => ['#response'],
+      // These exist only to fulfill the requirements of the RenderCache, which
+      // is designed to work with render arrays only. We don't care about these.
+      '#markup' => '',
+      '#attached' => '',
+    ];
+
+    // Merge the response's cacheability metadata, so that RenderCache can take
+    // care of cache redirects for us.
+    CacheableMetadata::createFromObject($response->getCacheableMetadata())
+      ->merge(CacheableMetadata::createFromRenderArray($response_as_render_array))
+      ->applyTo($response_as_render_array);
+
+    return $response_as_render_array;
+  }
+
+  /**
+   * Gets the embedded Response object in a render array.
+   *
+   * @param array $render_array
+   *   A render array with a #response property.
+   *
+   * @return \Drupal\Core\Cache\CacheableResponseInterface
+   *   The cacheable response object.
+   *
+   * @see responseToRenderArray()
+   */
+  protected function renderArrayToResponse(array $render_array) {
+    return $render_array['#response'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events = [];
+
+    // Run after AuthenticationSubscriber (necessary for the 'user' cache
+    // context; priority 300) and MaintenanceModeSubscriber (Dynamic Page Cache
+    // should not be polluted by maintenance mode-specific behavior; priority
+    // 30), but before ContentControllerSubscriber (updates _controller, but
+    // that is a no-op when Dynamic Page Cache runs; priority 25).
+    $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27];
+
+    // Run before HtmlResponseSubscriber::onRespond(), which has priority 0.
+    $events[KernelEvents::RESPONSE][] = ['onResponse', 100];
+
+    return $events;
+  }
+
+}