core/core.services.yml | 6 +- core/includes/theme.inc | 2 +- .../Core/EventSubscriber/SmartCacheSubscriber.php | 151 +++++++++++-- core/lib/Drupal/Core/Render/HtmlResponse.php | 5 +- .../Render/HtmlResponseAttachmentsProcessor.php | 50 ++++- .../Core/Render/MainContent/HtmlRenderer.php | 35 ++- .../Render/MainContent/SmartCacheHtmlRenderer.php | 238 --------------------- .../src/Tests/Cache/SmartCacheIntegrationTest.php | 6 +- 8 files changed, 194 insertions(+), 299 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index d6c3e17..9ba5318 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -895,8 +895,8 @@ services: tags: - { name: event_subscriber } main_content_renderer.html: - class: Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer - arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager', '@smart_cache_request_policy', '@smart_cache_response_policy', '@current_route_match', '@cache.smart_cache_contexts', '@cache.smart_cache_html', '@request_stack', %renderer.config%] + class: Drupal\Core\Render\MainContent\HtmlRenderer + arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache'] tags: - { name: render.main_content_renderer, format: html } main_content_renderer.ajax: @@ -940,7 +940,7 @@ services: lazy: true smart_cache_subscriber: class: Drupal\Core\EventSubscriber\SmartCacheSubscriber - arguments: ['@current_route_match', '@cache_contexts_manager', '@cache.smart_cache_contexts', '@cache.smart_cache_html'] + arguments: ['@current_route_match', '@cache_contexts_manager', '@cache.smart_cache_contexts', '@cache.smart_cache_html', '@smart_cache_request_policy', '@smart_cache_response_policy', '%renderer.config%'] tags: - { name: event_subscriber } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 3b156a5..f61cc67 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1319,7 +1319,7 @@ function template_preprocess_html(&$variables) { '@token' => $token, ]); $variables[$type]['#markup'] = $placeholder; - $variables[$type]['#attached']['html_response_placeholders'][$type] = $placeholder; + $variables[$type]['#attached']['html_response_attachment_placeholders'][$type] = $placeholder; } } diff --git a/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php index c332572..7905f09 100644 --- a/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php @@ -7,12 +7,17 @@ namespace Drupal\Core\EventSubscriber; -use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\Context\CacheContextsManager; +use Drupal\Core\PageCache\RequestPolicyInterface; +use Drupal\Core\PageCache\ResponsePolicyInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -45,13 +50,34 @@ class SmartCacheSubscriber implements EventSubscriberInterface { protected $smartContextsCache; /** - * The Smart Cache #type => html render array cache bin. + * The Smart Cache HTML response cache bin. * * @var \Drupal\Core\Cache\CacheBackendInterface */ protected $smartHtmlCache; /** + * A policy rule determining the cacheability of a request. + * + * @var \Drupal\Core\PageCache\RequestPolicyInterface + */ + protected $requestPolicy; + + /** + * A policy rule determining the cacheability of the response. + * + * @var \Drupal\Core\PageCache\ResponsePolicyInterface + */ + protected $responsePolicy; + + /** + * The renderer configuration array. + * + * @var array + */ + protected $rendererConfig; + + /** * Constructs a new SmartCacheSubscriber object. * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match @@ -61,13 +87,22 @@ class SmartCacheSubscriber implements EventSubscriberInterface { * @param \Drupal\Core\Cache\CacheBackendInterface $contexts_cache * The Smart Cache contexts cache bin. * @param \Drupal\Core\Cache\CacheBackendInterface $html_cache - * The Smart Cache #type => html render array cache bin. + * The Smart Cache HTML response cache bin. + * @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 array $renderer_config + * The renderer configuration array. */ - public function __construct(RouteMatchInterface $route_match, CacheContextsManager $cache_contexts_manager, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache) { + public function __construct(RouteMatchInterface $route_match, CacheContextsManager $cache_contexts_manager, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, array $renderer_config) { $this->routeMatch = $route_match; $this->cacheContextsManager = $cache_contexts_manager; $this->smartContextsCache = $contexts_cache; $this->smartHtmlCache = $html_cache; + $this->requestPolicy = \Drupal::service('smart_cache_request_policy'); + $this->responsePolicy = \Drupal::service('smart_cache_response_policy'); + $this->rendererConfig = $renderer_config; } /** @@ -93,42 +128,114 @@ public function onRouteMatch(GetResponseEvent $event) { $this->routeMatch->getRouteName(); // Get the contexts by which the current route's response must be varied. - $cache_contexts = $this->smartContextsCache->get('smartcache:contexts:' . $this->cacheContextsManager->convertTokensToKeys(['route'])->getKeys()[0]); + $cache_contexts = $this->smartContextsCache->get($this->cacheContextsManager->convertTokensToKeys(['route'])->getKeys()[0]); // If we already know the contexts by which the current route's response // must be varied, check if a response already is cached for the current // request's values for those contexts, and if so, return early. if ($cache_contexts !== FALSE) { - $cid = 'smartcache:html_render_array:' . implode(':', $this->cacheContextsManager->convertTokensToKeys($cache_contexts->data)->getKeys()); + $cid = implode(':', $this->cacheContextsManager->convertTokensToKeys($cache_contexts->data)->getKeys()); $cached_html = $this->smartHtmlCache->get($cid); if ($cached_html !== FALSE) { - $html = $cached_html->data; - // Since https://www.drupal.org/node/2273925, the Renderer filters any - // markup that is given, to ensure it is safe. But, in the case of - // SmartCache, the markup is known to be safe, since it was originally - // generated by the Renderer. - $html['#markup'] = SafeMarkup::set($html['#markup']); - $event->getRequest() - ->attributes - ->set('_controller', function() use ($html) { - // Mark the render array, to skip as much in SmartCacheHtmlRenderer. - $html['#smartcache'] = TRUE; - // Return the #type => html render array. Let Symfony's HttpKernel - // handle the conversion to a Response object via its VIEW event. - return $html; - }); - $event->stopPropagation(); + $response = $cached_html->data; + $response->headers->set('X-Drupal-SmartCache', 'HIT'); + $event->setResponse($response); } } } /** + * Stores a response in case of a SmartCache cache miss, if cacheable. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onResponse(FilterResponseEvent $event) { + $response = $event->getResponse(); + + // SmartCache only cares about HTML responses. + if (!$response instanceof HtmlResponse) { + return; + } + + // There's no work left to be done if this is a SmartCache cache hit. + if ($response->headers->get('X-Drupal-SmartCache') === 'HIT') { + return; + } + + // Don't cache the HTML response if the SmartCache request & response + // policies are not met. + $request = $event->getRequest(); + if ($this->requestPolicy->check($request) === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { + return; + } + + // Get the contexts by which the current route's response must be varied. + $contexts_cid = $this->cacheContextsManager->convertTokensToKeys(['route'])->getKeys()[0]; + $stored_cache_contexts = $this->smartContextsCache->get($contexts_cid); + if ($stored_cache_contexts !== FALSE) { + $stored_cache_contexts = $stored_cache_contexts->data; + } + + // Get the cacheability metadata. + $html_cacheability = CacheableMetadata::createFromObject($response->getCacheableMetadata()) + // SmartCache caches per route. + ->addCacheContexts(['route']) + // SmartCache also respects the Renderer's required cache contexts. + ->addCacheContexts($this->rendererConfig['required_cache_contexts']) + ->addCacheTags(['rendered']); + + // @todo DEBUG DEBUG DEBUG PROFILING PROFILING PROFILING — Until only the + // truly uncacheable things set max-age = 0 (such as the search block and + // the breadcrumbs block, which currently set max-age = 0, even though it + // is perfectly possible to cache them), to see the performance boost this + // will bring, uncomment this line. +//$html_cacheability->setCacheMaxAge(Cache::PERMANENT); + + // SmartCache only caches cacheable HTML responses. + if ($html_cacheability->getCacheMaxAge() !== 0) { + // Anonymous function to optimize the cache contexts of CacheableMetadata. + $optimize_cache_contexts = function (CacheableMetadata $cacheability) { + $cacheability->setCacheContexts($this->cacheContextsManager->optimizeTokens($cacheability->getCacheContexts())); + }; + + $optimize_cache_contexts($html_cacheability); + // If the set of cache contexts is different, store the union of the already + // stored cache contexts and the contexts for this request. + if ($html_cacheability->getCacheContexts() !== $stored_cache_contexts) { + if (is_array($stored_cache_contexts)) { + $html_cacheability->addCacheContexts($stored_cache_contexts); + $optimize_cache_contexts($html_cacheability); + } + $this->smartContextsCache->set($contexts_cid, $html_cacheability->getCacheContexts()); + } + + // Finally, cache the HTML response by those contexts. + $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($html_cacheability->getCacheContexts()); + $cid = implode(':', $context_cache_keys->getKeys()); + $html_cacheability = $html_cacheability->merge($context_cache_keys); + $expire = ($html_cacheability->getCacheMaxAge() === Cache::PERMANENT) ? Cache::PERMANENT : (int) $request->server->get('REQUEST_TIME') + $html_cacheability->getCacheMaxAge(); + $this->smartHtmlCache->set($cid, $response, $expire, $html_cacheability->getCacheTags()); + + // Now that the HTML response is cached, mark the response as a cache miss. + $response->headers->set('X-Drupal-SmartCache', 'MISS'); + } + else { + // The HTML response is uncacheable, mark it as such. + $response->headers->set('X-Drupal-SmartCache', 'UNCACHEABLE'); + } + } + + /** * {@inheritdoc} */ public static function getSubscribedEvents() { $events = []; $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27]; + // Run before HtmlResponseSubscriber::onRespond(), which has priority 0. + $events[KernelEvents::RESPONSE][] = ['onResponse', 100]; + return $events; } diff --git a/core/lib/Drupal/Core/Render/HtmlResponse.php b/core/lib/Drupal/Core/Render/HtmlResponse.php index c5339d6..4aea1e0 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponse.php +++ b/core/lib/Drupal/Core/Render/HtmlResponse.php @@ -36,12 +36,15 @@ public function setContent($content) { // A render array can automatically be converted to a string and set the // necessary metadata. if (is_array($content) && (isset($content['#markup']))) { - $content += ['#attached' => ['html_response_placeholders' => []]]; + $content += ['#attached' => ['html_response_attachment_placeholders' => [], 'placeholders' => []]]; $this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content)); $this->setAttachments($content['#attached']); $content = $content['#markup']; } parent::setContent($content); + + return $this; } + } diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php index 50ff584..ab8db4d 100644 --- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php +++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php @@ -99,13 +99,19 @@ public function processAttachments(AttachmentsInterface $response) { throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.'); } + // First, render the actual placeholders; this may cause additional + // attachments to be added to the response, which the attachment + // placeholders rendered by renderHtmlResponseAttachmentPlaceholders() will + // need to include. + $this->renderPlaceholders($response); + $attached = $response->getAttachments(); // Get the placeholders from attached and then remove them. - $placeholders = $attached['html_response_placeholders']; - unset($attached['html_response_placeholders']); + $attachment_placeholders = $attached['html_response_attachment_placeholders']; + unset($attached['html_response_attachment_placeholders']); - $variables = $this->processAssetLibraries($attached, $placeholders); + $variables = $this->processAssetLibraries($attached, $attachment_placeholders); // Handle all non-asset attachments. This populates drupal_get_html_head() // and drupal_get_http_header(). @@ -113,12 +119,12 @@ public function processAttachments(AttachmentsInterface $response) { drupal_process_attached($all_attached); // Get HTML head elements - if present. - if (isset($placeholders['head'])) { + if (isset($attachment_placeholders['head'])) { $variables['head'] = drupal_get_html_head(FALSE); } - // Now replace the placeholders in the response content with the real data. - $this->renderPlaceholders($response, $placeholders, $variables); + // Now replace the attachment placeholders. + $this->renderHtmlResponseAttachmentPlaceholders($response, $attachment_placeholders, $variables); // Finally set the headers on the response. $headers = drupal_get_http_header(); @@ -128,6 +134,33 @@ public function processAttachments(AttachmentsInterface $response) { } /** + * Renders placeholders. + * + * Renders #attached[placeholders]. + * + * @param \Drupal\Core\Render\HtmlResponse $response + * The HTML response whose placeholders to replace. + * + * @see \Drupal\Core\Render\Renderer::replacePlaceholders() + * @see \Drupal\Core\Render\Renderer::renderPlaceholder() + */ + protected function renderPlaceholders(HtmlResponse $response) { + // Render the placeholders in the HTML Response object. + $build = [ + '#markup' => SafeString::create($response->getContent()), + '#attached' => $response->getAttachments(), + ]; + $this->renderer->renderRoot($build); + + // Update the Response object now that the placeholders have been rendered. + $placeholders_bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build); + $response + ->setContent($build['#markup']) + ->addCacheableDependency($placeholders_bubbleable_metadata) + ->setAttachments($placeholders_bubbleable_metadata->getAttachments()); + } + + /** * Processes asset libraries into render arrays. * * @param array $attached @@ -174,8 +207,7 @@ protected function processAssetLibraries(array $attached, array $placeholders) { } /** - * Renders variables into HTML markup and replaces placeholders in the - * response content. + * Renders HTML response attachment placeholders. * * @param \Drupal\Core\Render\HtmlResponse $response * The HTML response to update. @@ -186,7 +218,7 @@ protected function processAssetLibraries(array $attached, array $placeholders) { * The variables to render and replace, keyed by type with renderable * arrays as values. */ - protected function renderPlaceholders(HtmlResponse $response, array $placeholders, array $variables) { + protected function renderHtmlResponseAttachmentPlaceholders(HtmlResponse $response, array $placeholders, array $variables) { $content = $response->getContent(); foreach ($placeholders as $type => $placeholder) { if (isset($variables[$type])) { diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index e0a2ec0..542d337 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -11,6 +11,7 @@ use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Display\PageVariantInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Render\PageDisplayVariantSelectionEvent; use Drupal\Core\Render\RenderCacheInterface; @@ -105,12 +106,6 @@ public function __construct(TitleResolverInterface $title_resolver, PluginManage * The entire HTML: takes a #type 'page' and wraps it in a #type 'html'. */ public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { - // If the _controller result already is #type => html, we can skip - // immediately to the final rendering (only html.html.twig). - if (isset($main_content['#type']) && $main_content['#type'] === 'html') { - return $this->finish($main_content); - } - list($page, $title) = $this->prepare($main_content, $request, $route_match); if (!isset($page['#type']) || $page['#type'] !== 'page') { @@ -131,22 +126,18 @@ public function renderResponse(array $main_content, Request $request, RouteMatch // page.html.twig, hence add them here, just before rendering html.html.twig. $this->buildPageTopAndBottom($html); - return $this->finish($html); - } - - /** - * Receives the render array for the html.twig.twig template and renders it. - * - * @param array $html - * The #type => html render array that represents the entire page. - * - * @return \Symfony\Component\HttpFoundation\Response - * The response. - */ - protected function finish(array $html) { - // @todo https://www.drupal.org/node/2495001 Make renderRoot return a - // cacheable render array directly. - $this->renderer->renderRoot($html); + // "Soft-render" the HTML regions: don't replace placeholders yet, because + // that happens in \Drupal\Core\Render\HtmlResponseAttachmentsProcessor. + $render_context = new RenderContext(); + $this->renderer->executeInRenderContext($render_context, function() use (&$html) { + $this->renderer->render($html); + }); + if (!$render_context->isEmpty()) { + $bubbleable_metadata = $render_context->pop(); + BubbleableMetadata::createFromRenderArray($html) + ->merge($bubbleable_metadata) + ->applyTo($html); + } $content = $this->renderCache->getCacheableRenderArray($html); // Also associate the "rendered" cache tag. This allows us to invalidate the diff --git a/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php deleted file mode 100644 index 985e9bd..0000000 --- a/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php +++ /dev/null @@ -1,238 +0,0 @@ - html render array cache bin. - * - * @var \Drupal\Core\Cache\CacheBackendInterface - */ - protected $smartHtmlCache; - - /** - * The request stack. - * - * @var \Symfony\Component\HttpFoundation\RequestStack - */ - protected $requestStack; - - /** - * The renderer configuration array. - * - * @var array - */ - protected $rendererConfig; - - /** - * Constructs a new SmartCacheHtmlRenderer. - * - * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver - * The title resolver. - * @param \Drupal\Component\Plugin\PluginManagerInterface $display_variant_manager - * The display variant manager. - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher - * The event dispatcher. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - * @param \Drupal\Core\Render\RendererInterface $renderer - * The renderer service. - * @param \Drupal\Core\Render\RenderCacheInterface $render_cache - * The render cache service. - * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager - * The cache contexts service. - * @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\Routing\RouteMatchInterface $route_match - * The current route match. - * @param \Drupal\Core\Cache\CacheBackendInterface $contexts_cache - * The Smart Cache contexts cache bin. - * @param \Drupal\Core\Cache\CacheBackendInterface $html_cache - * The Smart Cache #type => html render array cache bin. - * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack - * The request stack. - * @param array $renderer_config - * The renderer configuration array. - */ - public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, CacheContextsManager $cache_contexts_manager, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, RouteMatchInterface $route_match, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache, RequestStack $request_stack, array $renderer_config) { - parent::__construct($title_resolver, $display_variant_manager, $event_dispatcher, $module_handler, $renderer, $render_cache); - $this->cacheContextsManager = $cache_contexts_manager; - $this->requestPolicy = $request_policy; - $this->responsePolicy = $response_policy; - $this->routeMatch = $route_match; - $this->smartContextsCache = $contexts_cache; - $this->smartHtmlCache = $html_cache; - $this->requestStack = $request_stack; - $this->rendererConfig = $renderer_config; - } - - /** - * {@inheritdoc} - */ - public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { - // If this is a #type => html render array that comes from SmartCache - // already, then we can return early: no need to redo all the work. - if (isset($main_content['#smartcache'])) { - $html = $main_content; - // Mark the response as a cache hit. - $html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'HIT']; - return parent::finish($html); - } - else { - return parent::renderResponse($main_content, $request, $route_match); - } - } - - /** - * {@inheritdoc} - */ - protected function finish(array $html) { - // Don't cache the render array if the associated response will not meet the - // SmartCache request & response policies. - $response = new Response(); - $request = $this->requestStack->getCurrentRequest(); - if ($this->requestPolicy->check($request) === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { - return parent::finish($html); - } - - $cacheable_html = $html; - - // Get the contexts by which the current route's response must be varied. - $contexts_cid = 'smartcache:contexts:' . $this->cacheContextsManager->convertTokensToKeys(['route'])->getKeys()[0]; - $stored_cache_contexts = $this->smartContextsCache->get($contexts_cid); - if ($stored_cache_contexts !== FALSE) { - $stored_cache_contexts = $stored_cache_contexts->data; - } - - // "Soft-render" the HTML regions (don't replace placeholders yet, since we - // mustcache the placeholders, not the replaced placeholders). - $render_context = new RenderContext(); - $this->renderer->executeInRenderContext($render_context, function() use (&$cacheable_html) { - $this->renderer->render($cacheable_html); - }); - if (!$render_context->isEmpty()) { - $bubbleable_metadata = $render_context->pop(); - BubbleableMetadata::createFromRenderArray($cacheable_html) - ->merge($bubbleable_metadata) - ->applyTo($cacheable_html); - } - - // Only retain the cacheable render array, to be stored in SmartCache. - $cacheable_html = $this->renderCache->getCacheableRenderArray($cacheable_html); - - // Get the cacheability metadata. - $html_cacheability = CacheableMetadata::createFromRenderArray($cacheable_html) - // SmartCache caches per route. - ->addCacheContexts(['route']) - // SmartCache also respects the Renderer's required cache contexts. - ->addCacheContexts($this->rendererConfig['required_cache_contexts']) - ->addCacheTags(['rendered']); - - // Retain page titles defined in the main content render array. - if (isset($html['page']['#title'])) { - $cacheable_html['page']['#title'] = $html['page']['#title']; - } - - // @todo DEBUG DEBUG DEBUG PROFILING PROFILING PROFILING — Until only the - // truly uncacheable things set max-age = 0 (such as the search block and - // the breadcrumbs block, which currently set max-age = 0, even though it - // is perfectly possible to cache them), to see the performance boost this - // will bring, uncomment this line. -//$html_cacheability->setCacheMaxAge(Cache::PERMANENT); - - // SmartCache only caches cacheable pages. - if ($html_cacheability->getCacheMaxAge() !== 0) { - // Anonymous function to optimize the cache contexts of CacheableMetadata. - $optimize_cache_contexts = function (CacheableMetadata $cacheability) { - $cacheability->setCacheContexts($this->cacheContextsManager->optimizeTokens($cacheability->getCacheContexts())); - }; - - $optimize_cache_contexts($html_cacheability); - // If the set of cache contexts is different, store the union of the already - // stored cache contexts and the contexts for this request. - if ($html_cacheability->getCacheContexts() !== $stored_cache_contexts) { - if (is_array($stored_cache_contexts)) { - $html_cacheability->addCacheContexts($stored_cache_contexts); - $optimize_cache_contexts($html_cacheability); - } - $this->smartContextsCache->set($contexts_cid, $html_cacheability->getCacheContexts()); - } - - // Finally, cache the #type => html render array by those contexts. - $context_cache_keys = $this->cacheContextsManager->convertTokensToKeys($html_cacheability->getCacheContexts()); - $cid = 'smartcache:html_render_array:' . implode(':', $context_cache_keys->getKeys()); - $html_cacheability = $html_cacheability->merge($context_cache_keys); - $expire = ($html_cacheability->getCacheMaxAge() === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $html_cacheability->getCacheMaxAge(); - $this->smartHtmlCache->set($cid, $cacheable_html, $expire, $html_cacheability->getCacheTags()); - - // Now that the cacheable HTML is cached, mark the response as a cache miss. - $cacheable_html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'MISS']; - } - else { - // Now that the cacheable HTML is cached, mark the response as a cache miss. - $cacheable_html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'UNCACHEABLE']; - } - - return parent::finish($cacheable_html); - } - -} diff --git a/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php b/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php index ded4167..3d5b3de 100644 --- a/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php +++ b/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; +use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Session\AnonymousUserSession; use Drupal\Core\Url; use Drupal\simpletest\WebTestBase; @@ -20,7 +21,6 @@ * @group Cache * * @see \Drupal\Core\EventSubscriber\SmartCacheSubscriber - * @see \Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer */ class SmartCacheIntegrationTest extends WebTestBase { @@ -134,14 +134,14 @@ protected function assertSmartCache(Url $url, array $final_cache_contexts) { $cache_item = \Drupal::cache('smart_cache_contexts')->get($cid); $this->assertEqual($final_cache_contexts, array_values($cache_item->data)); - // Assert SmartCache html render array item. + // Assert SmartCache HTML response. $cid_parts = ['smartcache', 'html_render_array']; foreach ($final_cache_contexts as $cache_context_token) { $cid_parts[] = $cache_context_to_key_mapping[$cache_context_token]; } $cid = implode(':', $cid_parts); $cache_item = \Drupal::cache('smart_cache_html')->get($cid); - $this->assertEqual(['#markup', '#attached', '#cache'], array_keys($cache_item->data)); + $this->assertTrue($cache_item->data instanceof HtmlResponse); } }