22a1f2c2ced019b03d196175bfec8eeac20b9082
[yaffs-website] / web / core / modules / dynamic_page_cache / src / EventSubscriber / DynamicPageCacheSubscriber.php
1 <?php
2
3 namespace Drupal\dynamic_page_cache\EventSubscriber;
4
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;
15
16 /**
17  * Returns cached responses as early and avoiding as much work as possible.
18  *
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.
24  *
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.
31  *
32  * @see \Drupal\Core\Render\MainContent\HtmlRenderer
33  * @see \Drupal\Core\Cache\CacheableResponseInterface
34  */
35 class DynamicPageCacheSubscriber implements EventSubscriberInterface {
36
37   /**
38    * Name of Dynamic Page Cache's response header.
39    */
40   const HEADER = 'X-Drupal-Dynamic-Cache';
41
42   /**
43    * A request policy rule determining the cacheability of a response.
44    *
45    * @var \Drupal\Core\PageCache\RequestPolicyInterface
46    */
47   protected $requestPolicy;
48
49   /**
50    * A response policy rule determining the cacheability of the response.
51    *
52    * @var \Drupal\Core\PageCache\ResponsePolicyInterface
53    */
54   protected $responsePolicy;
55
56   /**
57    * The render cache.
58    *
59    * @var \Drupal\Core\Render\RenderCacheInterface
60    */
61   protected $renderCache;
62
63   /**
64    * The renderer configuration array.
65    *
66    * @var array
67    */
68   protected $rendererConfig;
69
70   /**
71    * Dynamic Page Cache's redirect render array.
72    *
73    * @var array
74    */
75   protected $dynamicPageCacheRedirectRenderArray = [
76     '#cache' => [
77       'keys' => ['response'],
78       'contexts' => [
79         'route',
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()
86         'request_format',
87       ],
88       'bin' => 'dynamic_page_cache',
89     ],
90   ];
91
92   /**
93    * Internal cache of request policy results.
94    *
95    * @var \SplObjectStorage
96    */
97   protected $requestPolicyResults;
98
99   /**
100    * Constructs a new DynamicPageCacheSubscriber object.
101    *
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
107    *   The render cache.
108    * @param array $renderer_config
109    *   The renderer configuration array.
110    */
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();
117   }
118
119   /**
120    * Sets a response in case of a Dynamic Page Cache hit.
121    *
122    * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
123    *   The event to process.
124    */
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) {
133       return;
134     }
135
136     // Sets the response for the current route, if cached.
137     $cached = $this->renderCache->get($this->dynamicPageCacheRedirectRenderArray);
138     if ($cached) {
139       $response = $this->renderArrayToResponse($cached);
140       $response->headers->set(self::HEADER, 'HIT');
141       $event->setResponse($response);
142     }
143   }
144
145   /**
146    * Stores a response in case of a Dynamic Page Cache miss, if cacheable.
147    *
148    * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
149    *   The event to process.
150    */
151   public function onResponse(FilterResponseEvent $event) {
152     $response = $event->getResponse();
153
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
157     // response.)
158     if (!$response instanceof CacheableResponseInterface) {
159       return;
160     }
161
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') {
164       return;
165     }
166
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');
171       return;
172     }
173
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])) {
187       return;
188     }
189
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) {
194       return;
195     }
196
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);
201
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');
205   }
206
207   /**
208    * Whether the given response should be cached by Dynamic Page Cache.
209    *
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.
214    *
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.
220    *
221    * @param \Drupal\Core\Cache\CacheableResponseInterface $response
222    *   The response whose cacheability to analyze.
223    *
224    * @return bool
225    *   Whether the given response should be cached.
226    *
227    * @see \Drupal\Core\Render\Renderer::shouldAutomaticallyPlaceholder()
228    */
229   protected function shouldCacheResponse(CacheableResponseInterface $response) {
230     $conditions = $this->rendererConfig['auto_placeholder_conditions'];
231
232     $cacheability = $response->getCacheableMetadata();
233
234     // Response's max-age is at or below the configured threshold.
235     if ($cacheability->getCacheMaxAge() !== Cache::PERMANENT && $cacheability->getCacheMaxAge() <= $conditions['max-age']) {
236       return FALSE;
237     }
238
239     // Response has a high-cardinality cache context.
240     if (array_intersect($cacheability->getCacheContexts(), $conditions['contexts'])) {
241       return FALSE;
242     }
243
244     // Response has a high-invalidation frequency cache tag.
245     if (array_intersect($cacheability->getCacheTags(), $conditions['tags'])) {
246       return FALSE;
247     }
248
249     return TRUE;
250   }
251
252   /**
253    * Embeds a Response object in a render array so that RenderCache can cache it.
254    *
255    * @param \Drupal\Core\Cache\CacheableResponseInterface $response
256    *   A cacheable response.
257    *
258    * @return array
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.
262    *
263    * @see renderArrayToResponse()
264    *
265    * @todo Refactor/remove once https://www.drupal.org/node/2551419 lands.
266    */
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
272       // care about.
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.
276       '#markup' => '',
277       '#attached' => '',
278     ];
279
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);
285
286     return $response_as_render_array;
287   }
288
289   /**
290    * Gets the embedded Response object in a render array.
291    *
292    * @param array $render_array
293    *   A render array with a #response property.
294    *
295    * @return \Drupal\Core\Cache\CacheableResponseInterface
296    *   The cacheable response object.
297    *
298    * @see responseToRenderArray()
299    */
300   protected function renderArrayToResponse(array $render_array) {
301     return $render_array['#response'];
302   }
303
304   /**
305    * {@inheritdoc}
306    */
307   public static function getSubscribedEvents() {
308     $events = [];
309
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];
316
317     // Run before HtmlResponseSubscriber::onRespond(), which has priority 0.
318     $events[KernelEvents::RESPONSE][] = ['onResponse', 100];
319
320     return $events;
321   }
322
323 }