core/core.services.yml | 27 ++- .../ContentControllerSubscriber.php | 2 +- .../Core/EventSubscriber/SmartCacheSubscriber.php | 126 ++++++++++++++ core/lib/Drupal/Core/Form/FormBuilder.php | 5 + .../Core/Render/MainContent/HtmlRenderer.php | 20 +++ .../Render/MainContent/SmartCacheHtmlRenderer.php | 185 +++++++++++++++++++++ .../src/Tests/BlockContentCacheTagsTest.php | 2 +- .../comment/src/Tests/CommentCacheTagsTest.php | 6 +- core/modules/filter/src/Tests/FilterAPITest.php | 1 + .../menu_ui/src/Tests/MenuCacheTagsTest.php | 3 +- .../modules/node/src/Tests/NodeListBuilderTest.php | 2 +- .../src/Tests/Cache/SmartCacheIntegrationTest.php | 128 ++++++++++++++ .../src/Tests/Entity/EntityCacheTagsTestBase.php | 17 +- .../src/Tests/Entity/EntityListBuilderTest.php | 2 +- .../src/Tests/Entity/EntityViewBuilderTest.php | 6 +- .../Entity/EntityWithUriCacheTagsTestBase.php | 2 +- .../smart_cache_test/smart_cache_test.info.yml | 6 + .../smart_cache_test/smart_cache_test.routing.yml | 45 +++++ .../src/SmartCacheTestController.php | 51 ++++++ core/modules/tour/src/Tests/TourCacheTagsTest.php | 2 + sites/default/default.services.yml | 4 +- 21 files changed, 619 insertions(+), 23 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index d44bcb1..c76b1f3 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -2,7 +2,7 @@ parameters: session.storage.options: {} twig.config: {} renderer.config: - required_cache_contexts: ['languages:language_interface', 'theme'] + required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions'] factory.keyvalue: default: keyvalue.database factory.keyvalue.expirable: @@ -861,8 +861,8 @@ services: tags: - { name: event_subscriber } main_content_renderer.html: - class: Drupal\Core\Render\MainContent\HtmlRenderer - arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@element_info', '@module_handler', '@renderer', '@render_cache', '@cache_contexts_manager'] + 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'] tags: - { name: render.main_content_renderer, format: html } main_content_renderer.ajax: @@ -881,6 +881,27 @@ services: arguments: ['@title_resolver'] tags: - { name: render.main_content_renderer, format: drupal_modal } + + cache.smart_cache_contexts: + 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] + smart_cache_subscriber: + class: Drupal\Core\EventSubscriber\SmartCacheSubscriber + arguments: ['@current_route_match', '@cache_contexts_manager', '@cache.smart_cache_contexts', '@cache.smart_cache_html'] + tags: + - { name: event_subscriber } + controller.form: class: Drupal\Core\Controller\HtmlFormController arguments: ['@controller_resolver', '@form_builder', '@class_resolver'] diff --git a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php index 16e9613..f6f30fe 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php @@ -40,7 +40,7 @@ public function onRequestDeriveFormWrapper(GetResponseEvent $event) { * An array of event listener definitions. */ static function getSubscribedEvents() { - $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 29); + $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 25); return $events; } diff --git a/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php new file mode 100644 index 0000000..51fde1b --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/SmartCacheSubscriber.php @@ -0,0 +1,126 @@ + html render array cache bin. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $smartHtmlCache; + + /** + * 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. + */ + public function __construct(RouteMatchInterface $route_match, CacheContextsManager $cache_contexts_manager, CacheBackendInterface $contexts_cache, CacheBackendInterface $html_cache) { + $this->routeMatch = $route_match; + $this->cacheContextsManager = $cache_contexts_manager; + $this->smartContextsCache = $contexts_cache; + $this->smartHtmlCache = $html_cache; + } + + /** + * Sets a response in case of a SmartCache cache hit. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The event to process. + */ + public function onRouteMatch(GetResponseEvent $event) { + // SmartCache only supports master requests that are safe and ask for HTML. + if (!$event->isMasterRequest() || !$event->getRequest()->isMethodSafe() || $event->getRequest()->getRequestFormat() !== 'html') { + return; + } + + // @todo For now, SmartCache doesn't handle admin routes. It may be too much + // work to add the necessary cacheability metadata to all admin routes + // before 8.0.0, but that can happen in 8.1.0 without a BC break. + if ($this->routeMatch->getRouteObject()->getOption('_admin_route')) { + 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 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(); + } + } + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27]; + + return $events; + } + +} diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index 4b995c1..fca740e 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -567,6 +567,11 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) { $form['#method'] = 'get'; } + // Mark every non-GET form as uncacheable. + if (!$form_state->isMethodType('get')) { + $form['#cache']['max-age'] = 0; + } + // Generate a new #build_id for this form, if none has been set already. // The form_build_id is used as key to cache a particular build of the form. // For multi-step forms, this allows the user to go back to an earlier diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index e5cdd87..0a561ad 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -16,6 +16,7 @@ use Drupal\Core\Display\PageVariantInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Render\Element; use Drupal\Core\Render\PageDisplayVariantSelectionEvent; use Drupal\Core\Render\RenderCacheInterface; use Drupal\Core\Render\RendererInterface; @@ -123,6 +124,12 @@ 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') { @@ -144,6 +151,19 @@ 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) { // The three parts of rendered markup in html.html.twig (page_top, page and // page_bottom) must be rendered with drupal_render_root(), so that their // #post_render_cache callbacks are executed (which may attach additional diff --git a/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php new file mode 100644 index 0000000..715461a --- /dev/null +++ b/core/lib/Drupal/Core/Render/MainContent/SmartCacheHtmlRenderer.php @@ -0,0 +1,185 @@ + html render array cache bin. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $smartHtmlCache; + + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + /** + * 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\Render\ElementInfoManagerInterface + * The element info manager. + * @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\CacheContextsManager $cache_contexts_manager + * 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) { + 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; + } + + /** + * {@inheritdoc} + */ + protected function finish(array $html) { + // 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($html['#smartcache'])) { + // Mark the response as a cache hit. + $html['#attached']['http_header'][] = ['X-Drupal-SmartCache', 'HIT']; + return parent::finish($html); + } + + // @todo For now, SmartCache doesn't handle admin routes. It may be too much + // work to add the necessary cacheability metadata to all admin routes + // before 8.0.0, but that can happen in 8.1.0 without a BC break. + if ($this->routeMatch->getRouteObject()->getOption('_admin_route')) { + 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'])[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) { + $this->renderer->render($cacheable_html[$child]); + } + + // Iterate over all the html template regions (page, page_top, page_bottom) + // and replace them with the equivalent cacheable render array. At the same + // time, collect the total set of cache contexts (to update the stored cache + // contexts, if any), and the total set of cache tags (to associate with the + // smart_cache_html cache item). + $html_cache_max_age = Cache::PERMANENT; + // SmartCache always caches per route, so always include that cache context. + $html_cache_contexts = ['route']; + $html_cache_tags = []; + foreach (Element::children($cacheable_html) as $child) { + $cacheable_html[$child] = $this->renderCache->getCacheableRenderArray($cacheable_html[$child]); + $html_cache_contexts = Cache::mergeContexts($html_cache_contexts, $cacheable_html[$child]['#cache']['contexts']); + $html_cache_tags = Cache::mergeTags($html_cache_tags, $cacheable_html[$child]['#cache']['tags']); + $html_cache_max_age = Cache::mergeMaxAges($html_cache_max_age, $cacheable_html[$child]['#cache']['max-age']); + } + + // 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_cache_max_age = Cache::PERMANENT; + + // @todo Remove this. Work-around to support the deep-render-array-scanning- + // dependent logic bartik_preprocess_html() has: it needs to know about + // the presence or absence of certain regions. That is similar (but less + /// bad) to the evil things one could do with hook_page_alter() in D7. + foreach (Element::children($html['page']) as $page_region) { + $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); + + // 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/block_content/src/Tests/BlockContentCacheTagsTest.php b/core/modules/block_content/src/Tests/BlockContentCacheTagsTest.php index 582d832..c8044dc 100644 --- a/core/modules/block_content/src/Tests/BlockContentCacheTagsTest.php +++ b/core/modules/block_content/src/Tests/BlockContentCacheTagsTest.php @@ -90,7 +90,7 @@ public function testBlock() { // Expected keys, contexts, and tags for the block. // @see \Drupal\block\BlockViewBuilder::viewMultiple() $expected_block_cache_keys = ['entity_view', 'block', $block->id()]; - $expected_block_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme']; + $expected_block_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; $expected_block_cache_tags = Cache::mergeTags(['block_view', 'rendered'], $block->getCacheTags(), $block->getPlugin()->getCacheTags()); // Expected contexts and tags for the BlockContent entity. diff --git a/core/modules/comment/src/Tests/CommentCacheTagsTest.php b/core/modules/comment/src/Tests/CommentCacheTagsTest.php index 5226434..2393d96 100644 --- a/core/modules/comment/src/Tests/CommentCacheTagsTest.php +++ b/core/modules/comment/src/Tests/CommentCacheTagsTest.php @@ -86,11 +86,7 @@ protected function createEntity() { * {@inheritdoc} */ protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) { - return [ - // Field access for the user picture rendered as part of the node that - // this comment is created on. - 'user.permissions', - ]; + return []; } /** diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php index 483bc22..4923962 100644 --- a/core/modules/filter/src/Tests/FilterAPITest.php +++ b/core/modules/filter/src/Tests/FilterAPITest.php @@ -264,6 +264,7 @@ function testProcessedTextElement() { // The default cache contexts for Renderer. 'languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', + 'user.permissions', ]; $this->assertEqual($expected_cache_contexts, $build['#cache']['contexts'], 'Expected cache contexts present.'); $expected_markup = '
Hello, world!
This is a dynamic llama.
'; diff --git a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php index 45a9df7..e0c0fc7 100644 --- a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php +++ b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php @@ -54,6 +54,7 @@ public function testMenuBlock() { 'config:block_list', 'config:block.block.' . $block->id(), 'config:system.menu.llama', + 'config:user.role.anonymous', ); $this->verifyPageCache($url, 'HIT', $expected_tags); @@ -107,7 +108,7 @@ public function testMenuBlock() { $this->verifyPageCache($url, 'MISS'); // Verify a cache hit. - $this->verifyPageCache($url, 'HIT', ['config:block_list', 'rendered']); + $this->verifyPageCache($url, 'HIT', ['config:block_list', 'config:user.role.anonymous', 'rendered']); } } diff --git a/core/modules/node/src/Tests/NodeListBuilderTest.php b/core/modules/node/src/Tests/NodeListBuilderTest.php index 798a056..2d23604 100644 --- a/core/modules/node/src/Tests/NodeListBuilderTest.php +++ b/core/modules/node/src/Tests/NodeListBuilderTest.php @@ -39,7 +39,7 @@ public function testCacheContexts() { $build = $list_builder->render(); $this->container->get('renderer')->renderRoot($build); - $this->assertEqual(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url.query_args.pagers:0', 'user.node_grants:view'], $build['#cache']['contexts']); + $this->assertEqual(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions'], $build['#cache']['contexts']); } } diff --git a/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php b/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php new file mode 100644 index 0000000..5845969 --- /dev/null +++ b/core/modules/system/src/Tests/Cache/SmartCacheIntegrationTest.php @@ -0,0 +1,128 @@ +uninstall(['page_cache']); + } + + /** + * Tests that SmartCache works correctly, and verifies the edge cases. + */ + function testSmartCache() { + // Controllers returning response objects are ignored by SmartCache. + $url = Url::fromUri('route:smart_cache_test.response'); + $this->drupalGet($url); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response object returned: SmartCache is ignoring.'); + + // Controllers returning render arrays, rendered as HTML responses, are + // handled by SmartCache. + $url = Url::fromUri('route:smart_cache_test.html'); + $this->drupalGet($url); + $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache MISS.'); + $this->assertSmartCache($url, [], []); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.'); + + // The above is the simple case, where the render array returned by the + // response contains no cache contexts. So let's now test a route/controller + // that *does* vary by a cache context whose value we can easily control: it + // varies by the 'animal' query argument. + foreach (['llama', 'piggy', 'unicorn', 'kitten'] as $animal) { + $url = Url::fromUri('route:smart_cache_test.html.with_cache_contexts', ['query' => ['animal' => $animal]]); + $this->drupalGet($url); + $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache MISS.'); + $this->assertSmartCache($url, ['url.query_args:animal'], [$animal]); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.'); + + // Finally, let's also verify that the 'smart_cache_test.html' route + // continued to see cache hits if we specify a query argument, because it + // *should* ignore it and continue to provide SmartCache hits. + $url = Url::fromUri('route:smart_cache_test.html', ['query' => ['animal' => 'piglet']]); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.'); + } + + // Controllers returning render arrays, rendered as anything except a HTML + // response, are ignored by SmartCache. + $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax'))); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as AJAX response: SmartCache is ignoring.'); + $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog'))); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as dialog response: SmartCache is ignoring.'); + $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal'))); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as modal response: SmartCache is ignoring.'); + + // Admin routes are ignored by SmartCache. + $this->drupalGet('smart-cache-test/html/admin'); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response returned, rendered as HTML response, admin route: SmartCache is ignoring'); + $this->drupalGet('smart-cache-test/response/admin'); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response returned, admin route: SmartCache is ignoring'); + + // Max-age = 0 responses are ignored by SmartCache. + $this->drupalGet('smart-cache-test/html/uncacheable'); + $this->assertEqual('UNCACHEABLE', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response, but uncacheable: SmartCache is running, but not caching.'); + } + + /** + * Asserts SmartCache cache items. + * + * @param \Drupal\Core\Url $url + * The URL to test. + * @param string[] $expected_cache_contexts + * The expected cache contexts for the given URL. + * @param string[] $cid_parts_for_cache_contexts + * The CID parts corresponding to the values in $expected_cache_contexts. + */ + protected function assertSmartCache(Url $url, array $expected_cache_contexts, array $cid_parts_for_cache_contexts) { + // Assert SmartCache contexts item. + $cid_parts = ['smartcache', 'contexts', $url->getRouteName() . hash('sha256', serialize($url->getRouteParameters()))]; + $cid = implode(':', $cid_parts); + $cache_item = \Drupal::cache('smart_cache_contexts')->get($cid); + $this->assertEqual($expected_cache_contexts, array_values(array_diff($cache_item->data, ['route']))); + + // Assert SmartCache html render array item. + $cid_parts = ['smartcache', 'html_render_array', $url->getRouteName() . hash('sha256', serialize($url->getRouteParameters()))]; + $cid_parts = array_merge($cid_parts, $cid_parts_for_cache_contexts); + $cid = implode(':', $cid_parts); + $cache_item = \Drupal::cache('smart_cache_html')->get($cid); + $this->assertTrue($cache_item->data['#type'] === 'html'); + } + +} diff --git a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php index 0b365cb..df593d8 100644 --- a/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityCacheTagsTestBase.php @@ -132,6 +132,8 @@ protected static function generateStandardizedInfo($entity_type_label, $group) { /** * Returns the access cache contexts for the tested entity. * + * Only list cache contexts that aren't part of the required cache contexts. + * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be tested, as created by createEntity(). * @@ -141,12 +143,14 @@ protected static function generateStandardizedInfo($entity_type_label, $group) { * @see \Drupal\Core\Entity\EntityAccessControlHandlerInterface */ protected function getAccessCacheContextsForEntity(EntityInterface $entity) { - return ['user.permissions']; + return []; } /** * Returns the additional (non-standard) cache contexts for the tested entity. * + * Only list cache contexts that aren't part of the required cache contexts. + * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be tested, as created by createEntity(). * @@ -331,12 +335,16 @@ public function testReferencedEntity() { $nonempty_entity_listing_url = Url::fromRoute('entity.entity_test.collection_labels_alphabetically', ['entity_type_id' => $entity_type]); // The default cache contexts for rendered entities. - $default_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme']; + $default_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; $entity_cache_contexts = $default_cache_contexts; // Cache tags present on every rendered page. $page_cache_tags = Cache::mergeTags( ['rendered'], + // 'user.permissions' is a required cache context, and responses that vary + // by this cache context when requested by anonymous users automatically + // also get this cache tag, to ensure correct invalidation. + ['config:user.role.anonymous'], // If the block module is used, the Block page display variant is used, // which adds the block config entity type's list cache tags. \Drupal::moduleHandler()->moduleExists('block') ? ['config:block_list']: [] @@ -387,9 +395,10 @@ public function testReferencedEntity() { $cache_keys = ['entity_view', 'entity_test', $this->referencingEntity->id(), 'full']; $cid = $this->createCacheId($cache_keys, $entity_cache_contexts); $access_cache_contexts = $this->getAccessCacheContextsForEntity($this->entity); + $additional_cache_contexts = $this->getAdditionalCacheContextsForEntity($this->referencingEntity); $redirected_cid = NULL; - if (count($access_cache_contexts)) { - $redirected_cid = $this->createCacheId($cache_keys, Cache::mergeContexts($entity_cache_contexts, $this->getAdditionalCacheContextsForEntity($this->referencingEntity), $access_cache_contexts)); + if (count($access_cache_contexts) || count($additional_cache_contexts)) { + $redirected_cid = $this->createCacheId($cache_keys, Cache::mergeContexts($entity_cache_contexts, $additional_cache_contexts, $access_cache_contexts)); } $this->verifyRenderCache($cid, $referencing_entity_cache_tags, $redirected_cid); diff --git a/core/modules/system/src/Tests/Entity/EntityListBuilderTest.php b/core/modules/system/src/Tests/Entity/EntityListBuilderTest.php index 485e72c..e32b689 100644 --- a/core/modules/system/src/Tests/Entity/EntityListBuilderTest.php +++ b/core/modules/system/src/Tests/Entity/EntityListBuilderTest.php @@ -67,7 +67,7 @@ public function testCacheContexts() { $build = $list_builder->render(); $this->container->get('renderer')->renderRoot($build); - $this->assertEqual(['entity_test_view_grants', 'languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url.query_args.pagers:0'], $build['#cache']['contexts']); + $this->assertEqual(['entity_test_view_grants', 'languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'url.query_args.pagers:0', 'user.permissions'], $build['#cache']['contexts']); } } diff --git a/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php b/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php index 333dcda..bf0512d 100644 --- a/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php +++ b/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php @@ -63,7 +63,7 @@ public function testEntityViewBuilderCache() { // Get a fully built entity view render array. $entity_test->save(); $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'full'); - $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme'])); + $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])); $cid = implode(':', $cid_parts); $bin = $build['#cache']['bin']; @@ -113,7 +113,7 @@ public function testEntityViewBuilderCacheWithReferences() { // Get a fully built entity view render array for the referenced entity. $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test_reference, 'full'); - $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme'])); + $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])); $cid_reference = implode(':', $cid_parts); $bin_reference = $build['#cache']['bin']; @@ -132,7 +132,7 @@ public function testEntityViewBuilderCacheWithReferences() { // Get a fully built entity view render array. $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'full'); - $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme'])); + $cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])); $cid = implode(':', $cid_parts); $bin = $build['#cache']['bin']; diff --git a/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php b/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php index 0b6f430..47b551a 100644 --- a/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php +++ b/core/modules/system/src/Tests/Entity/EntityWithUriCacheTagsTestBase.php @@ -32,7 +32,7 @@ public function testEntityUri() { $view_mode = $this->selectViewMode($entity_type); // The default cache contexts for rendered entities. - $entity_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme']; + $entity_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions']; // Generate the standardized entity cache tags. $cache_tag = $this->entity->getCacheTags(); diff --git a/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.info.yml b/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.info.yml new file mode 100644 index 0000000..cfa52e2 --- /dev/null +++ b/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.info.yml @@ -0,0 +1,6 @@ +name: 'Test SmartCache' +type: module +description: 'Provides test routes/responses for SmartCache.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.routing.yml b/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.routing.yml new file mode 100644 index 0000000..c7164b2 --- /dev/null +++ b/core/modules/system/tests/modules/smart_cache_test/smart_cache_test.routing.yml @@ -0,0 +1,45 @@ +smart_cache_test.response: + path: '/smart-cache-test/response' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::response' + requirements: + _access: 'TRUE' + +smart_cache_test.response.admin: + path: '/smart-cache-test/response/admin' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::response' + requirements: + _access: 'TRUE' + options: + _admin_route: TRUE + +smart_cache_test.html: + path: '/smart-cache-test/html' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::html' + requirements: + _access: 'TRUE' + +smart_cache_test.html.admin: + path: '/smart-cache-test/html/admin' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::html' + requirements: + _access: 'TRUE' + options: + _admin_route: TRUE + +smart_cache_test.html.with_cache_contexts: + path: '/smart-cache-test/html/with-cache-contexts' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::htmlWithCacheContexts' + requirements: + _access: 'TRUE' + +smart_cache_test.html.uncacheable: + path: '/smart-cache-test/html/uncacheable' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::htmlUncacheable' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/smart_cache_test/src/SmartCacheTestController.php b/core/modules/system/tests/modules/smart_cache_test/src/SmartCacheTestController.php new file mode 100644 index 0000000..28bb937 --- /dev/null +++ b/core/modules/system/tests/modules/smart_cache_test/src/SmartCacheTestController.php @@ -0,0 +1,51 @@ + [ + '#markup' => 'Hello world.', + ], + ]; + } + + public function htmlWithCacheContexts() { + $build = $this->html(); + $build['dynamic_part'] = [ + '#markup' => SafeMarkup::format('Hello there, %animal.', ['%animal' => \Drupal::requestStack()->getCurrentRequest()->query->get('animal')]), + '#cache' => [ + 'contexts' => [ + 'url.query_args:animal', + ], + ], + ]; + return $build; + } + + public function htmlUncacheable() { + $build = $this->html(); + $build['very_dynamic_part'] = [ + '#markup' => 'Drupal cannot handle the awesomeness of llamas.', + '#cache' => [ + 'max-age' => 0, + ], + ]; + return $build; + } + +} diff --git a/core/modules/tour/src/Tests/TourCacheTagsTest.php b/core/modules/tour/src/Tests/TourCacheTagsTest.php index 1c4c323..8d2814f 100644 --- a/core/modules/tour/src/Tests/TourCacheTagsTest.php +++ b/core/modules/tour/src/Tests/TourCacheTagsTest.php @@ -52,6 +52,7 @@ public function testRenderedTour() { // Verify a cache hit, but also the presence of the correct cache tags. $expected_tags = [ 'config:tour.tour.tour-test', + 'config:user.role.anonymous', 'rendered', ]; $this->verifyPageCache($url, 'HIT', $expected_tags); @@ -71,6 +72,7 @@ public function testRenderedTour() { // Verify a cache hit. $expected_tags = [ + 'config:user.role.anonymous', 'rendered', ]; $this->verifyPageCache($url, 'HIT', $expected_tags); diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml index c44c73b..3e813fa 100644 --- a/sites/default/default.services.yml +++ b/sites/default/default.services.yml @@ -82,8 +82,8 @@ parameters: # The Renderer will automatically associate these cache contexts with every # render array, hence varying every render array by these cache contexts. # - # @default ['languages:language_interface', 'theme'] - required_cache_contexts: ['languages:language_interface', 'theme'] + # @default ['languages:language_interface', 'theme', 'user.permissions'] + required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions'] factory.keyvalue: {} # Default key/value storage service to use.