core/composer.json | 1 + core/core.services.yml | 6 +- core/includes/theme.inc | 2 +- .../ContentControllerSubscriber.php | 2 +- core/lib/Drupal/Core/Form/FormBuilder.php | 6 + core/lib/Drupal/Core/Render/HtmlResponse.php | 5 +- .../Render/HtmlResponseAttachmentsProcessor.php | 48 +++- .../Core/Render/MainContent/HtmlRenderer.php | 39 ++- .../src/Plugin/Block/TestAccessBlock.php | 2 +- core/modules/page_cache/page_cache.info.yml | 2 +- core/modules/page_cache/page_cache.module | 3 +- .../modules/page_cache/src/Tests/PageCacheTest.php | 2 +- core/modules/path/src/Tests/PathAliasTest.php | 4 + core/modules/smart_cache/smart_cache.info.yml | 6 + core/modules/smart_cache/smart_cache.module | 31 +++ core/modules/smart_cache/smart_cache.services.yml | 43 ++++ .../src/EventSubscriber/SmartCacheSubscriber.php | 275 +++++++++++++++++++++ .../RequestPolicy/DefaultRequestPolicy.php | 29 +++ .../RequestPolicy/DenyNonHtmlRequests.php | 33 +++ .../PageCache/ResponsePolicy/DenyAdminRoutes.php | 48 ++++ .../src/Tests/SmartCacheIntegrationTest.php | 149 +++++++++++ .../smart_cache_test/smart_cache_test.info.yml | 6 + .../smart_cache_test/smart_cache_test.routing.yml | 45 ++++ .../src/SmartCacheTestController.php | 82 ++++++ .../system/src/EventSubscriber/ConfigCacheTag.php | 4 +- core/modules/system/system.routing.yml | 2 + .../paramconverter_test/src/TestControllers.php | 4 +- core/modules/tracker/src/Tests/TrackerTest.php | 8 +- core/modules/tracker/tracker.pages.inc | 6 +- core/profiles/minimal/minimal.info.yml | 1 + core/profiles/standard/standard.info.yml | 1 + core/profiles/testing/testing.info.yml | 5 +- sites/example.settings.local.php | 15 +- 33 files changed, 883 insertions(+), 32 deletions(-) diff --git a/core/composer.json b/core/composer.json index 26938e3..02fea68 100644 --- a/core/composer.json +++ b/core/composer.json @@ -105,6 +105,7 @@ "drupal/seven": "self.version", "drupal/shortcut": "self.version", "drupal/simpletest": "self.version", + "drupal/smart_cache": "self.version", "drupal/standard": "self.version", "drupal/stark": "self.version", "drupal/statistics": "self.version", diff --git a/core/core.services.yml b/core/core.services.yml index e2939e9..2fef4a9 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -211,17 +211,20 @@ services: class: Drupal\Core\PageCache\ResponsePolicy\KillSwitch tags: - { name: page_cache_response_policy } + - { name: smart_cache_response_policy } page_cache_no_cache_routes: class: Drupal\Core\PageCache\ResponsePolicy\DenyNoCacheRoutes arguments: ['@current_route_match'] public: false tags: - { name: page_cache_response_policy } + - { name: smart_cache_response_policy } page_cache_no_server_error: class: Drupal\Core\PageCache\ResponsePolicy\NoServerError public: false tags: - { name: page_cache_response_policy } + - { name: smart_cache_response_policy } config.manager: class: Drupal\Core\Config\ConfigManager arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher'] @@ -903,7 +906,7 @@ services: - { name: event_subscriber } main_content_renderer.html: class: Drupal\Core\Render\MainContent\HtmlRenderer - arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache'] + arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache', '%renderer.config%'] tags: - { name: render.main_content_renderer, format: html } main_content_renderer.ajax: @@ -922,6 +925,7 @@ services: arguments: ['@title_resolver'] tags: - { name: render.main_content_renderer, format: drupal_modal } + controller.form: class: Drupal\Core\Controller\HtmlFormController arguments: ['@controller_resolver', '@form_builder', '@class_resolver'] 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/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/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index a24330d..b803be0 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -621,6 +621,12 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) { $form['#method'] = 'get'; } + // Mark every non-GET form as uncacheable. + // @todo Refine in https://www.drupal.org/node/2526472. + 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/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..742ea57 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,31 @@ public function processAttachments(AttachmentsInterface $response) { } /** + * Renders placeholders (#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 +205,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 +216,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 7e06aab..a5fcb1b 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -8,9 +8,11 @@ namespace Drupal\Core\Render\MainContent; use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Core\Cache\Cache; 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; @@ -75,6 +77,13 @@ class HtmlRenderer implements MainContentRendererInterface { protected $renderCache; /** + * The renderer configuration array. + * + * @var array + */ + protected $rendererConfig; + + /** * Constructs a new HtmlRenderer. * * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver @@ -89,14 +98,17 @@ class HtmlRenderer implements MainContentRendererInterface { * The renderer service. * @param \Drupal\Core\Render\RenderCacheInterface $render_cache * The render cache service. + * @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) { + public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, array $renderer_config) { $this->titleResolver = $title_resolver; $this->displayVariantManager = $display_variant_manager; $this->eventDispatcher = $event_dispatcher; $this->moduleHandler = $module_handler; $this->renderer = $renderer; $this->renderCache = $render_cache; + $this->rendererConfig = $renderer_config; } /** @@ -125,11 +137,30 @@ 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); - // @todo https://www.drupal.org/node/2495001 Make renderRoot return a - // cacheable render array directly. - $this->renderer->renderRoot($html); + // Render, but don't replace placeholders yet, because that happens later in + // the render pipeline. + // @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor. + $render_context = new RenderContext(); + $this->renderer->executeInRenderContext($render_context, function() use (&$html) { + // RendererInterface::render() renders the $html render array, it updates + // $html by reference. We don't care about the return value (which is just + // $html['#markup']), but about the resulting render array. + // @todo Simplify this when https://www.drupal.org/node/2495001 lands. + $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 required cache contexts. + // (Because we use ::render() above and not ::renderRoot(), we manually must + // ensure the HTML response varies by the required cache contexts.) + $content['#cache']['contexts'] = Cache::mergeContexts($content['#cache']['contexts'], $this->rendererConfig['required_cache_contexts']); + // Also associate the "rendered" cache tag. This allows us to invalidate the // entire render cache, regardless of the cache bin. $content['#cache']['tags'][] = 'rendered'; diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php index 873a77b..eb9ee56 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php @@ -63,7 +63,7 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ protected function blockAccess(AccountInterface $account) { - return $this->state->get('test_block_access', FALSE) ? AccessResult::allowed() : AccessResult::forbidden(); + return $this->state->get('test_block_access', FALSE) ? AccessResult::allowed()->setCacheMaxAge(0) : AccessResult::forbidden()->setCacheMaxAge(0); } /** diff --git a/core/modules/page_cache/page_cache.info.yml b/core/modules/page_cache/page_cache.info.yml index 4affed7..6999544 100644 --- a/core/modules/page_cache/page_cache.info.yml +++ b/core/modules/page_cache/page_cache.info.yml @@ -1,6 +1,6 @@ name: Internal Page Cache type: module -description: 'Caches pages for anonymous users. Works well for small to medium-sized websites.' +description: 'Caches entire pages for anonymous users. Works well for small to medium-sized websites.' package: Core version: VERSION core: 8.x diff --git a/core/modules/page_cache/page_cache.module b/core/modules/page_cache/page_cache.module index f4eb71b..cc00f29 100644 --- a/core/modules/page_cache/page_cache.module +++ b/core/modules/page_cache/page_cache.module @@ -21,7 +21,8 @@ function page_cache_help($route_name, RouteMatchInterface $route_match) { $output .= '
'; $output .= '
' . t('Speeding up your site') . '
'; $output .= '
' . t('Pages requested by anonymous users are stored the first time they are requested and then are reused. Depending on your site configuration and the amount of your web traffic tied to anonymous visitors, the caching system may significantly increase the speed of your site.') . '
'; - $output .= '
' . t('Pages are usually identical for all anonymous users, while they can be customized for each authenticated user. This is why pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '
'; + $output .= '
' . t('Pages are usually identical for all anonymous users, while they can be personalized for each authenticated user. This is why pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '
'; + $output .= '
' . t('For speeding up your site for authenticated users, see the online documentation for the Smart Cache module.', ['!smartcache-documentation' => 'https://www.drupal.org/documentation/modules/smart_cache']) . '

'; $output .= '
' . t('Configuring the internal page cache') . '
'; $output .= '
' . t('On the Performance page, you can configure how long browsers and proxies may cache pages; that setting is also respected by the Internal Page Cache module. There is no other configuration.', array('!cache-settings' => \Drupal::url('system.performance_settings'))) . '
'; $output .= '
'; diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index e97a1b4..18a6dfd 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -399,7 +399,7 @@ public function testFormImmutability() { // that implementation. \Drupal::state()->set('page_cache_bypass_form_immutability', TRUE); \Drupal::moduleHandler()->resetImplementations(); - \Drupal::cache('render')->deleteAll(); + Cache::invalidateTags(['rendered']); $this->drupalGet('page_cache_form_test_immutability'); diff --git a/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php index e4d07e2..651c11f 100644 --- a/core/modules/path/src/Tests/PathAliasTest.php +++ b/core/modules/path/src/Tests/PathAliasTest.php @@ -7,6 +7,8 @@ namespace Drupal\path\Tests; +use Drupal\Core\Cache\Cache; + /** * Add, edit, delete, and change alias and verify its consistency in the * database. @@ -57,6 +59,8 @@ function testPathCache() { // Visit the alias for the node and confirm a cache entry is created. \Drupal::cache('data')->deleteAll(); + // @todo Remove this once https://www.drupal.org/node/2480077 lands. + Cache::invalidateTags(['rendered']); $this->drupalGet(trim($edit['alias'], '/')); $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.'); } diff --git a/core/modules/smart_cache/smart_cache.info.yml b/core/modules/smart_cache/smart_cache.info.yml new file mode 100644 index 0000000..71778e1 --- /dev/null +++ b/core/modules/smart_cache/smart_cache.info.yml @@ -0,0 +1,6 @@ +name: Smart Cache +type: module +description: 'Caches the non-personalized parts of pages. Works well for websites of all sizes.' +package: Core +version: VERSION +core: 8.x diff --git a/core/modules/smart_cache/smart_cache.module b/core/modules/smart_cache/smart_cache.module new file mode 100644 index 0000000..73341cd --- /dev/null +++ b/core/modules/smart_cache/smart_cache.module @@ -0,0 +1,31 @@ +' . t('About') . ''; + $output .= '

' . t('The Smart Cache module caches the non-personalized parts of pages in the database. For more information, see the online documentation for the Smart Cache module.', ['!smartcache-documentation' => 'https://www.drupal.org/documentation/modules/smart_cache']) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Speeding up your site') . '
'; + $output .= '
' . t('Pages are stored the first time they are requested if they are safe to cache, and then are reused. Depending on your site configuration and the complexity of particular pages, Smart Cache may significantly increase the speed of your site, even for authenticated users.') . '
'; + $output .= '
' . t('Configuring the Smart Cache') . '
'; + $output .= '
' . t('Nothing needs to be configured — that is why it is smart!') . '
'; + $output .= '
' . t('(Every part of the page contains metadata that allows Smart Cache to figure this out on its own.)') . '
'; + $output .= '
'; + + return $output; + } +} diff --git a/core/modules/smart_cache/smart_cache.services.yml b/core/modules/smart_cache/smart_cache.services.yml new file mode 100644 index 0000000..a8c5ab5 --- /dev/null +++ b/core/modules/smart_cache/smart_cache.services.yml @@ -0,0 +1,43 @@ +services: + # Cache bins. + cache.smart_cache_contexts: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory: cache_factory:get + arguments: [smart_cache_contexts] + cache.smart_cache_html: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory: cache_factory:get + arguments: [smart_cache_html] + + # Event subscriber. + smart_cache_subscriber: + class: Drupal\smart_cache\EventSubscriber\SmartCacheSubscriber + arguments: ['@cache_contexts_manager', '@cache.smart_cache_contexts', '@cache.smart_cache_html', '@smart_cache_request_policy', '@smart_cache_response_policy'] + tags: + - { name: event_subscriber } + + # Request & response policies. + smart_cache_request_policy: + class: Drupal\smart_cache\PageCache\RequestPolicy\DefaultRequestPolicy + tags: + - { name: service_collector, tag: smart_cache_request_policy, call: addPolicy} + smart_cache_response_policy: + class: Drupal\Core\PageCache\ChainResponsePolicy + tags: + - { name: service_collector, tag: smart_cache_response_policy, call: addPolicy} + lazy: true + smart_cache_deny_admin_routes: + class: Drupal\smart_cache\PageCache\ResponsePolicy\DenyAdminRoutes + arguments: ['@current_route_match'] + public: false + tags: + - { name: smart_cache_response_policy } + smart_cache_deny_non_html_requests: + class: Drupal\smart_cache\PageCache\RequestPolicy\DenyNonHtmlRequests + public: false + tags: + - { name: smart_cache_request_policy } diff --git a/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php b/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php new file mode 100644 index 0000000..056ea88 --- /dev/null +++ b/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php @@ -0,0 +1,275 @@ +cacheContextsManager = $cache_contexts_manager; + $this->smartContextsCache = $contexts_cache; + $this->smartHtmlCache = $html_cache; + $this->requestPolicy = $request_policy; + $this->responsePolicy = $response_policy; + } + + /** + * 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) { + // Don't cache the HTML response if the SmartCache request policies are not + // met. Store the result in a request attribute, so that onResponse() does + // not have to redo the request policy check. + $request = $event->getRequest(); + $request_policy_result = $this->requestPolicy->check($request); + $request->attributes->set(self::ATTRIBUTE_REQUEST_POLICY_RESULT, $request_policy_result); + if ($request_policy_result === RequestPolicyInterface::DENY) { + return; + } + + // Get the contexts by which the current route's response must be varied. + $cid = implode(':', $this->cacheContextsManager->convertTokensToKeys(['route'])->getKeys()); + $cached = $this->smartContextsCache->get($cid); + $cache_contexts_for_route = ($cached !== FALSE) ? $cached->data : NULL; + $request->attributes->set(self::ATTRIBUTE_CACHE_CONTEXTS_FOR_ROUTE_CID, $cid); + $request->attributes->set(self::ATTRIBUTE_CACHE_CONTEXTS_FOR_ROUTE, $cache_contexts_for_route); + + // Get & set the cached HTML response for the current route, if any. + if (is_array($cache_contexts_for_route)) { + $cid = implode(':', $this->cacheContextsManager->convertTokensToKeys($cache_contexts_for_route)->getKeys()); + $cached_html = $this->smartHtmlCache->get($cid); + if ($cached_html !== FALSE) { + $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 works with HTML responses that are actual HTMLResponse + // objects, it does not work with plain Response objects that happen to + // return HTML. (SmartCache needs to be able to access and modify the + // cacheability metadata associated with the HTML response.) + 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 SmartCache's request subscriber did not + // fire, because that means it's impossible to have a SmartCache cache hit. + // (This can happen when the master request is for example a 403 or 404, in + // which case a subrequest is performed by the router. In that case, it is + // the subrequest's response that is cached by SmartCache, because the + // routing happens in a request subscriber earlier than SmartCache's and + // immediately sets a response, i.e. the one returned by the subrequest, and + // thus causes SmartCache's request subscriber to not fire for the master + // request.) + // @see \Drupal\Core\Routing\AccessAwareRouter::checkAccess() + // @see \Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber::on403() + $request = $event->getRequest(); + if (!$request->attributes->has(self::ATTRIBUTE_REQUEST_POLICY_RESULT)) { + return; + } + + // Don't cache the HTML response if the SmartCache request & response + // policies are not met. + // @see onRouteMatch() + if ($request->attributes->get(self::ATTRIBUTE_REQUEST_POLICY_RESULT) === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { + return; + } + + // Get the cacheability metadata. + $html_cacheability = CacheableMetadata::createFromObject($response->getCacheableMetadata()) + ->addCacheContexts(['route']) + ->addCacheTags(['rendered']); + + // SmartCache only caches cacheable HTML responses, i.e. with a max-age > 0. + if ($html_cacheability->getCacheMaxAge() !== 0) { + $this->normalizeCacheability($html_cacheability); + + // Get the contexts by which the current route's response must be varied. + $stored_cache_contexts = $request->attributes->get(self::ATTRIBUTE_CACHE_CONTEXTS_FOR_ROUTE); + + // 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); + $this->normalizeCacheability($html_cacheability); + } + $this->smartContextsCache->set($request->attributes->get(self::ATTRIBUTE_CACHE_CONTEXTS_FOR_ROUTE_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()); + + // The HTML response was generated, mark the response as a cache miss. The + // next time, it will be a cache hit. + $response->headers->set('X-Drupal-SmartCache', 'MISS'); + } + else { + // The HTML response is uncacheable, mark it as such. + $response->headers->set('X-Drupal-SmartCache', 'UNCACHEABLE'); + } + } + + /** + * Normalizes cacheability metadata, so SmartCache can store & compare it. + * + * Optimizes the cache contexts in the given cacheability metadata. + * + * @param \Drupal\Core\Cache\CacheableMetadata $cacheability + * The cacheability metadata to normalize. + */ + protected function normalizeCacheability(CacheableMetadata $cacheability) { + $cacheability->setCacheContexts($this->cacheContextsManager->optimizeTokens($cacheability->getCacheContexts())); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = []; + + // Run after AuthenticationSubscriber (necessary for the 'user' cache + // context) and MaintenanceModeSubscriber (SmartCache should not be polluted + // by maintenance mode-specific behavior), but before + // ContentControllerSubscriber (updates _controller, but that's pointless + // when SmartCache runs). + $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27]; + + // Run before HtmlResponseSubscriber::onRespond(), which has priority 0. + $events[KernelEvents::RESPONSE][] = ['onResponse', 100]; + + return $events; + } + +} diff --git a/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php b/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php new file mode 100644 index 0000000..0651b8f --- /dev/null +++ b/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php @@ -0,0 +1,29 @@ +addPolicy(new CommandLineOrUnsafeMethod()); + } + +} diff --git a/core/modules/smart_cache/src/PageCache/RequestPolicy/DenyNonHtmlRequests.php b/core/modules/smart_cache/src/PageCache/RequestPolicy/DenyNonHtmlRequests.php new file mode 100644 index 0000000..dfc5445 --- /dev/null +++ b/core/modules/smart_cache/src/PageCache/RequestPolicy/DenyNonHtmlRequests.php @@ -0,0 +1,33 @@ +getRequestFormat() !== 'html' || $request->query->has(MainContentViewSubscriber::WRAPPER_FORMAT)) { + return static::DENY; + } + } + +} diff --git a/core/modules/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php b/core/modules/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php new file mode 100644 index 0000000..45485f5 --- /dev/null +++ b/core/modules/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php @@ -0,0 +1,48 @@ +routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public function check(Response $response, Request $request) { + if (($route = $this->routeMatch->getRouteObject()) && $route->getOption('_admin_route')) { + return static::DENY; + } + } + +} diff --git a/core/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php b/core/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php new file mode 100644 index 0000000..ace479f --- /dev/null +++ b/core/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php @@ -0,0 +1,149 @@ +uninstall(['page_cache']); + } + + /** + * Tests that SmartCache works correctly, and verifies the edge cases. + */ + public 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->assertRaw($animal); + $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->assertRaw($animal); + $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[] $final_cache_contexts + * Assocative array, with the keys being the expected cache contexts for the + * given URL and the values being the corresponding cache keys. + * (Excluding the required cache contexts (%renderer.config%) and the + * 'route' cache context, all of which are added by SmartCache automatically + * and are asserted here too.) + */ + protected function assertSmartCache(Url $url, array $final_cache_contexts) { + // The complete cache context to key mapping consists of: + // - the expected cache contexts as passed in; + $cache_context_to_key_mapping = $final_cache_contexts; + // - the required cache contexts; + $cache_context_to_key_mapping['languages:language_interface'] = 'en'; + $cache_context_to_key_mapping['theme'] = 'classy'; + $cache_context_to_key_mapping['user.permissions'] = \Drupal::service('user_permissions_hash_generator')->generate(new AnonymousUserSession()); + // - the 'route' cache context added by SmartCache. + $cache_context_to_key_mapping['route'] = $url->getRouteName() . hash('sha256', serialize($url->getRouteParameters())); + + // The final list of cache contexts that should be present. + $final_cache_contexts = Cache::mergeContexts(array_keys($cache_context_to_key_mapping)); + + // Assert SmartCache contexts item. + $cid_parts = ['[route]=' . $url->getRouteName() . hash('sha256', serialize($url->getRouteParameters()))]; + $cid = implode(':', $cid_parts); + $cache_item = \Drupal::cache('smart_cache_contexts')->get($cid); + $this->assertEqual($final_cache_contexts, array_values($cache_item->data)); + + // Assert SmartCache HTML response. + $cid_parts = []; + foreach ($final_cache_contexts as $cache_context_token) { + $cid_parts[] = '[' . $cache_context_token . ']=' . $cache_context_to_key_mapping[$cache_context_token]; + } + $cid = implode(':', $cid_parts); + $cache_item = \Drupal::cache('smart_cache_html')->get($cid); + $this->assertTrue($cache_item->data instanceof HtmlResponse); + } + +} diff --git a/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.info.yml b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.info.yml new file mode 100644 index 0000000..cfa52e2 --- /dev/null +++ b/core/modules/smart_cache/tests/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/smart_cache/tests/smart_cache_test/smart_cache_test.routing.yml b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.routing.yml new file mode 100644 index 0000000..c7164b2 --- /dev/null +++ b/core/modules/smart_cache/tests/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/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php b/core/modules/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php new file mode 100644 index 0000000..177d5f5 --- /dev/null +++ b/core/modules/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php @@ -0,0 +1,82 @@ + [ + '#markup' => 'Hello world.', + ], + ]; + } + + /** + * A route returning a render array (with cache contexts, so cacheable). + * + * @return array + * A render array. + * + * @see html() + */ + 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; + } + + /** + * A route returning a render array (with max-age=0, so uncacheable) + * + * @return array + * A render array. + * + * @see html() + */ + 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/system/src/EventSubscriber/ConfigCacheTag.php b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php index 0482a74..640a6f0 100644 --- a/core/modules/system/src/EventSubscriber/ConfigCacheTag.php +++ b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php @@ -60,8 +60,8 @@ public function onSave(ConfigCrudEvent $event) { $this->cacheTagsInvalidator->invalidateTags(['route_match', 'rendered']); } - // Global theme settings. - if ($event->getConfig()->getName() === 'system.theme.global') { + // Theme configuration and global theme settings. + if (in_array($event->getConfig()->getName(), ['system.theme', 'system.theme.global'])) { $this->cacheTagsInvalidator->invalidateTags(['rendered']); } diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 9d806bb..2bc24a0 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -454,6 +454,8 @@ system.db_update: _title: 'Drupal database update' _controller: '\Drupal\system\Controller\DbUpdateController::handle' op: 'info' + options: + _admin_route: TRUE requirements: _access_system_update: 'TRUE' diff --git a/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php b/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php index 1e8e2f4..327a8d5 100644 --- a/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php +++ b/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php @@ -25,6 +25,8 @@ public function testNodeSetParent(NodeInterface $node, NodeInterface $parent) { } public function testEntityLanguage(NodeInterface $node) { - return ['#markup' => $node->label()]; + $build = ['#markup' => $node->label()]; + \Drupal::service('renderer')->addCacheableDependency($build, $node); + return $build; } } diff --git a/core/modules/tracker/src/Tests/TrackerTest.php b/core/modules/tracker/src/Tests/TrackerTest.php index c57f1c9..aaf7bad 100644 --- a/core/modules/tracker/src/Tests/TrackerTest.php +++ b/core/modules/tracker/src/Tests/TrackerTest.php @@ -83,10 +83,10 @@ function testTrackerAll() { $this->assertLink(t('My recent content'), 0, 'User tab shows up on the global tracker page.'); // Assert cache contexts, specifically the pager and node access contexts. - $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions']); - // Assert cache tags for the visible node and node list cache tag. + $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args.pagers:0', 'user.node_grants:view', 'user.permissions', 'user.roles:authenticated']); + // Assert cache tags for the visible node, node lists and comment lists. $expected_tags = Cache::mergeTags($published->getCacheTags(), $published->getOwner()->getCacheTags()); - $expected_tags = Cache::mergeTags($expected_tags, ['node_list', 'rendered']); + $expected_tags = Cache::mergeTags($expected_tags, ['node_list', 'comment_list', 'rendered']); $this->assertCacheTags($expected_tags); // Delete a node and ensure it no longer appears on the tracker. @@ -155,7 +155,7 @@ function testTrackerUser() { $expected_tags = Cache::mergeTags($my_published->getCacheTags(), $my_published->getOwner()->getCacheTags()); $expected_tags = Cache::mergeTags($expected_tags, $other_published_my_comment->getCacheTags()); $expected_tags = Cache::mergeTags($expected_tags, $other_published_my_comment->getOwner()->getCacheTags()); - $expected_tags = Cache::mergeTags($expected_tags, ['node_list', 'rendered']); + $expected_tags = Cache::mergeTags($expected_tags, ['node_list', 'comment_list', 'rendered']); $this->assertCacheTags($expected_tags); $this->assertCacheContexts(['languages:language_interface', 'theme', 'url.query_args.pagers:0', 'user', 'user.node_grants:view']); diff --git a/core/modules/tracker/tracker.pages.inc b/core/modules/tracker/tracker.pages.inc index 3be3966..78293b7 100644 --- a/core/modules/tracker/tracker.pages.inc +++ b/core/modules/tracker/tracker.pages.inc @@ -123,8 +123,9 @@ function tracker_page($account = NULL) { } } - // Add the list cache tag for nodes. + // Add the list cache tag for nodes and comments. $cache_tags = Cache::mergeTags($cache_tags, \Drupal::entityManager()->getDefinition('node')->getListCacheTags()); + $cache_tags = Cache::mergeTags($cache_tags, \Drupal::entityManager()->getDefinition('comment')->getListCacheTags()); $page['tracker'] = array( '#rows' => $rows, @@ -140,6 +141,9 @@ function tracker_page($account = NULL) { $page['#cache']['tags'] = $cache_tags; $page['#cache']['contexts'][] = 'user.node_grants:view'; + // Cacheable per "authenticated or not", because we can only track (and show) + // reading history for authenticated users, not for anonymous users. + $page['#cache']['contexts'][] = 'user.roles:authenticated'; if (Drupal::moduleHandler()->moduleExists('history') && \Drupal::currentUser()->isAuthenticated()) { $page['#attached']['library'][] = 'tracker/history'; } diff --git a/core/profiles/minimal/minimal.info.yml b/core/profiles/minimal/minimal.info.yml index 206b8e7..9c1d809 100644 --- a/core/profiles/minimal/minimal.info.yml +++ b/core/profiles/minimal/minimal.info.yml @@ -8,5 +8,6 @@ dependencies: - block - dblog - page_cache + - smart_cache themes: - stark diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml index a356ae8..b487f6d 100644 --- a/core/profiles/standard/standard.info.yml +++ b/core/profiles/standard/standard.info.yml @@ -26,6 +26,7 @@ dependencies: - options - path - page_cache + - smart_cache - taxonomy - dblog - search diff --git a/core/profiles/testing/testing.info.yml b/core/profiles/testing/testing.info.yml index 5ded376..8b04df4 100644 --- a/core/profiles/testing/testing.info.yml +++ b/core/profiles/testing/testing.info.yml @@ -5,9 +5,10 @@ version: VERSION core: 8.x hidden: true dependencies: - # Enable page_cache in testing, to ensure that as many tests as possible run - # with page caching enabled. + # Enable page_cache and smart_cache in testing, to ensure that as many tests + # as possible run with anonymous page caching and SmartCache enabled. - page_cache + - smart_cache # @todo: Remove this in https://www.drupal.org/node/2352949 themes: - classy diff --git a/sites/example.settings.local.php b/sites/example.settings.local.php index 34b4e19..c9933e2 100644 --- a/sites/example.settings.local.php +++ b/sites/example.settings.local.php @@ -51,12 +51,25 @@ /** * Disable the render cache (this includes the page cache). * + * Note: you should test with the render cache enabled, to ensure the correct + * cacheability metadata is present (and hence the expected behavior). However, + * in the early stages of development, you may want to disable it. + * * This setting disables the render cache by using the Null cache back-end * defined by the development.services.yml file above. * * Do not use this setting until after the site is installed. */ -$settings['cache']['bins']['render'] = 'cache.backend.null'; +# $settings['cache']['bins']['render'] = 'cache.backend.null'; + +/** + * Disable SmartCache. + * + * Note: you should test with SmartCache enabled, to ensure the correct + * cacheability metadata is present (and hence the expected behavior). However, + * in the early stages of development, you may want to disable it. + */ +# $settings['cache']['bins']['smart_cache_html'] = 'cache.backend.null'; /** * Allow test modules and themes to be installed.