core/core.services.yml | 25 ++- .../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/Cache/SmartCacheIntegrationTest.php | 128 ++++++++++++++ .../smart_cache_test/smart_cache_test.info.yml | 6 + .../smart_cache_test/smart_cache_test.routing.yml | 45 +++++ .../src/SmartCacheTestController.php | 51 ++++++ 10 files changed, 590 insertions(+), 3 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index d44bcb1..ac85799 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -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/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/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; + } + +}