3 namespace Drupal\dynamic_page_cache\EventSubscriber;
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Cache\CacheableMetadata;
7 use Drupal\Core\Cache\CacheableResponseInterface;
8 use Drupal\Core\PageCache\RequestPolicyInterface;
9 use Drupal\Core\PageCache\ResponsePolicyInterface;
10 use Drupal\Core\Render\RenderCacheInterface;
11 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
12 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
13 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
14 use Symfony\Component\HttpKernel\KernelEvents;
17 * Returns cached responses as early and avoiding as much work as possible.
19 * Dynamic Page Cache is able to cache so much because it utilizes cache
20 * contexts: the cache contexts that are present capture the variations of every
21 * component of the page. That, combined with the fact that cacheability
22 * metadata is bubbled, means that the cache contexts at the page level
23 * represent the complete set of contexts that the page varies by.
25 * The reason Dynamic Page Cache is implemented as two event subscribers (a late
26 * REQUEST subscriber immediately after routing for cache hits, and an early
27 * RESPONSE subscriber for cache misses) is because many cache contexts can only
28 * be evaluated after routing. (Examples: 'user', 'user.permissions', 'route' …)
29 * Consequently, it is impossible to implement Dynamic Page Cache as a kernel
30 * middleware that simply caches per URL.
32 * @see \Drupal\Core\Render\MainContent\HtmlRenderer
33 * @see \Drupal\Core\Cache\CacheableResponseInterface
35 class DynamicPageCacheSubscriber implements EventSubscriberInterface {
38 * Name of Dynamic Page Cache's response header.
40 const HEADER = 'X-Drupal-Dynamic-Cache';
43 * A request policy rule determining the cacheability of a response.
45 * @var \Drupal\Core\PageCache\RequestPolicyInterface
47 protected $requestPolicy;
50 * A response policy rule determining the cacheability of the response.
52 * @var \Drupal\Core\PageCache\ResponsePolicyInterface
54 protected $responsePolicy;
59 * @var \Drupal\Core\Render\RenderCacheInterface
61 protected $renderCache;
64 * The renderer configuration array.
68 protected $rendererConfig;
71 * Dynamic Page Cache's redirect render array.
75 protected $dynamicPageCacheRedirectRenderArray = [
77 'keys' => ['response'],
80 // Some routes' controllers rely on the request format (they don't have
81 // a separate route for each request format). Additionally, a controller
82 // may be returning a domain object that a KernelEvents::VIEW subscriber
83 // must turn into an actual response, but perhaps a format is being
84 // requested that the subscriber does not support.
85 // @see \Drupal\Core\EventSubscriber\AcceptNegotiation406::onViewDetect406()
88 'bin' => 'dynamic_page_cache',
93 * Internal cache of request policy results.
95 * @var \SplObjectStorage
97 protected $requestPolicyResults;
100 * Constructs a new DynamicPageCacheSubscriber object.
102 * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
103 * A policy rule determining the cacheability of a request.
104 * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
105 * A policy rule determining the cacheability of the response.
106 * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
108 * @param array $renderer_config
109 * The renderer configuration array.
111 public function __construct(RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, RenderCacheInterface $render_cache, array $renderer_config) {
112 $this->requestPolicy = $request_policy;
113 $this->responsePolicy = $response_policy;
114 $this->renderCache = $render_cache;
115 $this->rendererConfig = $renderer_config;
116 $this->requestPolicyResults = new \SplObjectStorage();
120 * Sets a response in case of a Dynamic Page Cache hit.
122 * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
123 * The event to process.
125 public function onRouteMatch(GetResponseEvent $event) {
126 // Don't cache the response if the Dynamic Page Cache request policies are
127 // not met. Store the result in a static keyed by current request, so that
128 // onResponse() does not have to redo the request policy check.
129 $request = $event->getRequest();
130 $request_policy_result = $this->requestPolicy->check($request);
131 $this->requestPolicyResults[$request] = $request_policy_result;
132 if ($request_policy_result === RequestPolicyInterface::DENY) {
136 // Sets the response for the current route, if cached.
137 $cached = $this->renderCache->get($this->dynamicPageCacheRedirectRenderArray);
139 $response = $this->renderArrayToResponse($cached);
140 $response->headers->set(self::HEADER, 'HIT');
141 $event->setResponse($response);
146 * Stores a response in case of a Dynamic Page Cache miss, if cacheable.
148 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
149 * The event to process.
151 public function onResponse(FilterResponseEvent $event) {
152 $response = $event->getResponse();
154 // Dynamic Page Cache only works with cacheable responses. It does not work
155 // with plain Response objects. (Dynamic Page Cache needs to be able to
156 // access and modify the cacheability metadata associated with the
158 if (!$response instanceof CacheableResponseInterface) {
162 // There's no work left to be done if this is a Dynamic Page Cache hit.
163 if ($response->headers->get(self::HEADER) === 'HIT') {
167 // There's no work left to be done if this is an uncacheable response.
168 if (!$this->shouldCacheResponse($response)) {
169 // The response is uncacheable, mark it as such.
170 $response->headers->set(self::HEADER, 'UNCACHEABLE');
174 // Don't cache the response if Dynamic Page Cache's request subscriber did
175 // not fire, because that means it is impossible to have a Dynamic Page
176 // Cache hit. This can happen when the master request is for example a 403
177 // or 404, in which case a subrequest is performed by the router. In that
178 // case, it is the subrequest's response that is cached by Dynamic Page
179 // Cache, because the routing happens in a request subscriber earlier than
180 // Dynamic Page Cache's and immediately sets a response, i.e. the one
181 // returned by the subrequest, and thus causes Dynamic Page Cache's request
182 // subscriber to not fire for the master request.
183 // @see \Drupal\Core\Routing\AccessAwareRouter::checkAccess()
184 // @see \Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber::on403()
185 $request = $event->getRequest();
186 if (!isset($this->requestPolicyResults[$request])) {
190 // Don't cache the response if the Dynamic Page Cache request & response
191 // policies are not met.
192 // @see onRouteMatch()
193 if ($this->requestPolicyResults[$request] === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) {
197 // Embed the response object in a render array so that RenderCache is able
198 // to cache it, handling cache redirection for us.
199 $response_as_render_array = $this->responseToRenderArray($response);
200 $this->renderCache->set($response_as_render_array, $this->dynamicPageCacheRedirectRenderArray);
202 // The response was generated, mark the response as a cache miss. The next
203 // time, it will be a cache hit.
204 $response->headers->set(self::HEADER, 'MISS');
208 * Whether the given response should be cached by Dynamic Page Cache.
210 * We consider any response that has cacheability metadata meeting the auto-
211 * placeholdering conditions to be uncacheable. Because those conditions
212 * indicate poor cacheability, and if it doesn't make sense to cache parts of
213 * a page, then neither does it make sense to cache an entire page.
215 * But note that auto-placeholdering avoids such cacheability metadata ever
216 * bubbling to the response level: while rendering, the Renderer checks every
217 * subtree to see if meets the auto-placeholdering conditions. If it does, it
218 * is automatically placeholdered, and consequently the cacheability metadata
219 * of the placeholdered content does not bubble up to the response level.
221 * @param \Drupal\Core\Cache\CacheableResponseInterface $response
222 * The response whose cacheability to analyze.
225 * Whether the given response should be cached.
227 * @see \Drupal\Core\Render\Renderer::shouldAutomaticallyPlaceholder()
229 protected function shouldCacheResponse(CacheableResponseInterface $response) {
230 $conditions = $this->rendererConfig['auto_placeholder_conditions'];
232 $cacheability = $response->getCacheableMetadata();
234 // Response's max-age is at or below the configured threshold.
235 if ($cacheability->getCacheMaxAge() !== Cache::PERMANENT && $cacheability->getCacheMaxAge() <= $conditions['max-age']) {
239 // Response has a high-cardinality cache context.
240 if (array_intersect($cacheability->getCacheContexts(), $conditions['contexts'])) {
244 // Response has a high-invalidation frequency cache tag.
245 if (array_intersect($cacheability->getCacheTags(), $conditions['tags'])) {
253 * Embeds a Response object in a render array so that RenderCache can cache it.
255 * @param \Drupal\Core\Cache\CacheableResponseInterface $response
256 * A cacheable response.
259 * A render array that embeds the given cacheable response object, with the
260 * cacheability metadata of the response object present in the #cache
261 * property of the render array.
263 * @see renderArrayToResponse()
265 * @todo Refactor/remove once https://www.drupal.org/node/2551419 lands.
267 protected function responseToRenderArray(CacheableResponseInterface $response) {
268 $response_as_render_array = $this->dynamicPageCacheRedirectRenderArray + [
269 // The data we actually care about.
270 '#response' => $response,
271 // Tell RenderCache to cache the #response property: the data we actually
273 '#cache_properties' => ['#response'],
274 // These exist only to fulfill the requirements of the RenderCache, which
275 // is designed to work with render arrays only. We don't care about these.
280 // Merge the response's cacheability metadata, so that RenderCache can take
281 // care of cache redirects for us.
282 CacheableMetadata::createFromObject($response->getCacheableMetadata())
283 ->merge(CacheableMetadata::createFromRenderArray($response_as_render_array))
284 ->applyTo($response_as_render_array);
286 return $response_as_render_array;
290 * Gets the embedded Response object in a render array.
292 * @param array $render_array
293 * A render array with a #response property.
295 * @return \Drupal\Core\Cache\CacheableResponseInterface
296 * The cacheable response object.
298 * @see responseToRenderArray()
300 protected function renderArrayToResponse(array $render_array) {
301 return $render_array['#response'];
307 public static function getSubscribedEvents() {
310 // Run after AuthenticationSubscriber (necessary for the 'user' cache
311 // context; priority 300) and MaintenanceModeSubscriber (Dynamic Page Cache
312 // should not be polluted by maintenance mode-specific behavior; priority
313 // 30), but before ContentControllerSubscriber (updates _controller, but
314 // that is a no-op when Dynamic Page Cache runs; priority 25).
315 $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27];
317 // Run before HtmlResponseSubscriber::onRespond(), which has priority 0.
318 $events[KernelEvents::RESPONSE][] = ['onResponse', 100];