core/core.services.yml | 15 +--- .../Core/EventSubscriber/SmartCacheSubscriber.php | 100 ++++++++++----------- .../Render/MainContent/SmartCacheHtmlRenderer.php | 96 +++++++++----------- core/lib/Drupal/Core/Render/RenderCache.php | 3 +- 4 files changed, 96 insertions(+), 118 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index c76b1f3..62ded44 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -862,7 +862,7 @@ services: - { name: event_subscriber } main_content_renderer.html: class: Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer - arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@element_info', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager', '@current_route_match', '@cache.smart_cache_contexts', '@cache.smart_cache_html', '@request_stack'] + arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@element_info', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager', '@current_route_match'] tags: - { name: render.main_content_renderer, format: html } main_content_renderer.ajax: @@ -882,23 +882,16 @@ services: tags: - { name: render.main_content_renderer, format: drupal_modal } - cache.smart_cache_contexts: + cache.smart_cache: class: Drupal\Core\Cache\CacheBackendInterface tags: - { name: cache.bin } factory_method: get factory_service: cache_factory - arguments: [smart_cache_contexts] - cache.smart_cache_html: - class: Drupal\Core\Cache\CacheBackendInterface - tags: - - { name: cache.bin } - factory_method: get - factory_service: cache_factory - arguments: [smart_cache_html] + arguments: [smart_cache] 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', '@render_cache'] tags: - { name: event_subscriber } diff --git a/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php index 51fde1b..acafa99 100644 --- a/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php @@ -7,8 +7,10 @@ namespace Drupal\Core\EventSubscriber; +use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheContextsManager; +use Drupal\Core\Render\RenderCacheInterface; use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; @@ -29,43 +31,23 @@ class SmartCacheSubscriber implements EventSubscriberInterface { protected $routeMatch; /** - * The cache contexts manager. + * The render cache service. * - * @var \Drupal\Core\Cache\CacheContextsManager + * @var \Drupal\Core\Render\RenderCacheInterface */ - protected $cacheContextsManager; - - /** - * The Smart Cache contexts cache bin. - * - * @var \Drupal\Core\Cache\CacheBackendInterface - */ - protected $smartContextsCache; - - /** - * The Smart Cache #type => html render array cache bin. - * - * @var \Drupal\Core\Cache\CacheBackendInterface - */ - protected $smartHtmlCache; + protected $renderCache; /** * Constructs a new SmartCacheSubscriber object. * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The current route match. - * @param \Drupal\Core\Cache\CacheContextsManager $cache_contexts_manager - * The cache contexts service. - * @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 \Drupal\Core\Render\RenderCacheInterface $render_cache + * The render cache service. */ - public function __construct(RouteMatchInterface $route_match, CacheContextsManager $cache_contexts_manager, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache) { + public function __construct(RouteMatchInterface $route_match, RenderCacheInterface $render_cache) { $this->routeMatch = $route_match; - $this->cacheContextsManager = $cache_contexts_manager; - $this->smartContextsCache = $contexts_cache; - $this->smartHtmlCache = $html_cache; + $this->renderCache = $render_cache; } /** @@ -87,31 +69,49 @@ public function onRouteMatch(GetResponseEvent $event) { return; } - $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'])[0]); + // If we have a cache hit, return early (avoiding the controller from being + // executed at all). + $cached_html = $this->cacheGet(); + if ($cached_html !== FALSE) { + $event->getRequest() + ->attributes + ->set('_controller', function() use ($cached_html) { + // Mark the render array, to skip as much in SmartCacheHtmlRenderer. + $cached_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 $cached_html; + }); + $event->stopPropagation(); + } + } - // 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)); - $cached_html = $this->smartHtmlCache->get($cid); - if ($cached_html !== FALSE) { - $html = $cached_html->data; - $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(); - } + /** + * @todo write comment + * + * @return array|FALSE + * + * @see \Drupal\Core\Render\MainContent\SmartCacheHtmlRenderer::cacheSet + */ + protected function cacheGet() { + $build = [ + '#cache' => [ + 'keys' => ['smartcache'], + 'contexts' => ['route'], + 'bin' => 'smart_cache', + ] + ]; + $cached_html = $this->renderCache->get($build); + if ($cached_html !== FALSE) { + unset($cached_html['#markup']); + unset($cached_html['#cache']); + unset($cached_html['#cache_properties']); + // Restore #type and its uncached defaults. + $cached_html['#type'] = 'html'; + $cached_html['#theme'] = 'html'; + $cached_html['#defaults_loaded'] = TRUE; } + return $cached_html; } /** diff --git a/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php index 715461a..d653006 100644 --- a/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php @@ -9,7 +9,6 @@ use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheContextsManager; use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -20,7 +19,6 @@ use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\HttpFoundation\RequestStack; /** * SmartCache main content renderer for HTML requests. @@ -28,25 +26,11 @@ class SmartCacheHtmlRenderer extends HtmlRenderer { /** - * The Smart Cache contexts cache bin. + * The current route match. * - * @var \Drupal\Core\Cache\CacheBackendInterface + * @var \Drupal\Core\Routing\RouteMatchInterface */ - protected $smartContextsCache; - - /** - * The Smart Cache #type => html render array cache bin. - * - * @var \Drupal\Core\Cache\CacheBackendInterface - */ - protected $smartHtmlCache; - - /** - * The request stack. - * - * @var \Symfony\Component\HttpFoundation\RequestStack - */ - protected $requestStack; + protected $routeMatch; /** * Constructs a new SmartCacheHtmlRenderer. @@ -69,19 +53,10 @@ class SmartCacheHtmlRenderer extends HtmlRenderer { * The cache contexts service. * @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. */ - public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ElementInfoManagerInterface $element_info_manager, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, CacheContextsManager $cache_contexts_manager, RouteMatchInterface $route_match, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache, RequestStack $request_stack) { + public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ElementInfoManagerInterface $element_info_manager, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, CacheContextsManager $cache_contexts_manager, RouteMatchInterface $route_match) { parent::__construct($title_resolver, $display_variant_manager, $event_dispatcher, $element_info_manager, $module_handler, $renderer, $render_cache, $cache_contexts_manager); $this->routeMatch = $route_match; - $this->smartContextsCache = $contexts_cache; - $this->smartHtmlCache = $html_cache; - $this->requestStack = $request_stack; } /** @@ -105,13 +80,6 @@ protected function finish(array $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'])[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 execute #post_render_cache yet, // since we must cache the placeholders, not the replaced placeholders). foreach (Element::children($cacheable_html) as $child) { @@ -154,32 +122,48 @@ protected function finish(array $html) { $cacheable_html['page'][$page_region] = ['#preprocess_functions_messing_with_cacheability' => TRUE]; } - // SmartCache only caches cacheable pages. - if ($html_cache_max_age !== 0) { - $html_cache_contexts = $this->cacheContextsManager->optimizeTokens($html_cache_contexts); - // 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_cache_contexts !== $stored_cache_contexts) { - if (is_array($stored_cache_contexts)) { - $html_cache_contexts = $this->cacheContextsManager->optimizeTokens(Cache::mergeContexts($html_cache_contexts, $stored_cache_contexts)); - } - $this->smartContextsCache->set($contexts_cid, $html_cache_contexts); - } - - // Finally, cache the #type => html render array by those contexts. - $cid = 'smartcache:html_render_array:' . implode(':', $this->cacheContextsManager->convertTokensToKeys($html_cache_contexts)); - $expire = ($html_cache_max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $html_cache_max_age; - $this->smartHtmlCache->set($cid, $cacheable_html, $expire, $html_cache_tags); + $this->cacheSet($cacheable_html, $html_cache_contexts, $html_cache_tags, $html_cache_max_age); + return parent::finish($cacheable_html); + } + + /** + * Caches the given #type => html render array by the regions' cache contexts. + * + * Reuses the RenderCacheInterface service to do its caching. This requires + * some counter-intuitive fiddling, but it means we get many things for free: + * - automatically doesn't cache when max-age = 0 + * - lots of test coverage + * + * @param array $cacheable_html + * @param array $html_cache_contexts + * @param array $html_cache_tags + * @param $html_cache_max_age + */ + protected function cacheSet(array &$cacheable_html, array $html_cache_contexts, array $html_cache_tags, $html_cache_max_age) { + $keys = ['smartcache']; + $cacheable_html['#markup'] = ''; + $cacheable_html['#post_render_cache'] = []; + $cacheable_html['#cache'] = [ + 'keys' => $keys, + 'contexts' => Cache::mergeContexts($html_cache_contexts, ['route']), + 'tags' => $html_cache_tags, + 'max-age' => $html_cache_max_age, + 'bin' => 'smart_cache', + ]; + $cacheable_html['#cache_properties'] = Element::children($cacheable_html); + $result = $this->renderCache->set($cacheable_html, ['#cache' => ['keys' => $keys, 'contexts' => ['route']]]); + if ($result === FALSE) { // Now that the cacheable HTML is cached, mark the response as a cache miss. - $cacheable_html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'MISS']; + $cacheable_html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'UNCACHEABLE']; } else { // Now that the cacheable HTML is cached, mark the response as a cache miss. - $cacheable_html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'UNCACHEABLE']; + $cacheable_html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'MISS']; } - - return parent::finish($cacheable_html); + unset($cacheable_html['#markup']); + unset($cacheable_html['#cache']); + unset($cacheable_html['#cache_properties']); } } diff --git a/core/lib/Drupal/Core/Render/RenderCache.php b/core/lib/Drupal/Core/Render/RenderCache.php index aa071cc..6e60656 100644 --- a/core/lib/Drupal/Core/Render/RenderCache.php +++ b/core/lib/Drupal/Core/Render/RenderCache.php @@ -237,6 +237,7 @@ public function set(array &$elements, array $pre_bubbling_elements) { 'contexts' => $merged_cache_contexts, // The union of the current element's and stored cache tags. 'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']), + 'bin' => $bin, ], ]; $cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered'])); @@ -320,7 +321,7 @@ public function getCacheableRenderArray(array $elements) { $data['#markup'] = ''; // Cache only cacheable children's markup. foreach ($cacheable_children as $key) { - $cacheable_items[$key] = ['#markup' => $cacheable_items[$key]['#markup']]; + $cacheable_items[$key] = $this->getCacheableRenderArray($cacheable_items[$key]); } } $data += $cacheable_items;