core/MAINTAINERS.txt | 4 + core/composer.json | 1 + core/core.services.yml | 3 + .../ContentControllerSubscriber.php | 2 +- .../dynamic_page_cache/dynamic_page_cache.info.yml | 6 + .../dynamic_page_cache/dynamic_page_cache.module | 27 ++ .../dynamic_page_cache.services.yml | 29 ++ .../EventSubscriber/DynamicPageCacheSubscriber.php | 322 +++++++++++++++++++++ .../RequestPolicy/DefaultRequestPolicy.php | 29 ++ .../PageCache/ResponsePolicy/DenyAdminRoutes.php | 51 ++++ .../src/Tests/DynamicPageCacheIntegrationTest.php | 132 +++++++++ .../dynamic_page_cache_test.info.yml | 6 + .../dynamic_page_cache_test.routing.yml | 75 +++++ .../src/DynamicPageCacheTestController.php | 139 +++++++++ core/modules/page_cache/page_cache.info.yml | 2 +- core/modules/page_cache/page_cache.module | 6 +- .../system/src/EventSubscriber/ConfigCacheTag.php | 2 +- .../system/src/Tests/Session/SessionTest.php | 5 + core/modules/system/system.module | 2 +- core/profiles/minimal/minimal.info.yml | 1 + core/profiles/standard/src/Tests/StandardTest.php | 25 ++ core/profiles/standard/standard.info.yml | 1 + core/profiles/testing/testing.info.yml | 5 +- sites/example.settings.local.php | 15 +- 24 files changed, 880 insertions(+), 10 deletions(-) diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 2e9b1bf..6a94a6b 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -312,6 +312,10 @@ Database Logging module DateTime module - Matthew Donadio 'mpdonadio' https://www.drupal.org/u/mpdonadio +Dynamic Page Cache module +- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx +- Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers + Email module - Nils Destoop 'zuuperman' https://www.drupal.org/u/zuuperman diff --git a/core/composer.json b/core/composer.json index 75b45b9..7ec0df3 100644 --- a/core/composer.json +++ b/core/composer.json @@ -73,6 +73,7 @@ "drupal/core-uuid": "self.version", "drupal/datetime": "self.version", "drupal/dblog": "self.version", + "drupal/dynamic_page_cache": "self.version", "drupal/editor": "self.version", "drupal/entity_reference": "self.version", "drupal/field": "self.version", diff --git a/core/core.services.yml b/core/core.services.yml index 2d044d1..090d8f3 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -250,17 +250,20 @@ services: class: Drupal\Core\PageCache\ResponsePolicy\KillSwitch tags: - { name: page_cache_response_policy } + - { name: dynamic_page_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: dynamic_page_cache_response_policy } page_cache_no_server_error: class: Drupal\Core\PageCache\ResponsePolicy\NoServerError public: false tags: - { name: page_cache_response_policy } + - { name: dynamic_page_cache_response_policy } config.manager: class: Drupal\Core\Config\ConfigManager arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher'] 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/modules/dynamic_page_cache/dynamic_page_cache.info.yml b/core/modules/dynamic_page_cache/dynamic_page_cache.info.yml new file mode 100644 index 0000000..b269036 --- /dev/null +++ b/core/modules/dynamic_page_cache/dynamic_page_cache.info.yml @@ -0,0 +1,6 @@ +name: Internal Dynamic Page Cache +type: module +description: 'Caches pages, minus the personalized parts. Works well for websites of all sizes.' +package: Core +version: VERSION +core: 8.x diff --git a/core/modules/dynamic_page_cache/dynamic_page_cache.module b/core/modules/dynamic_page_cache/dynamic_page_cache.module new file mode 100644 index 0000000..dcd224f --- /dev/null +++ b/core/modules/dynamic_page_cache/dynamic_page_cache.module @@ -0,0 +1,27 @@ +' . t('About') . ''; + $output .= '

' . t('The Dynamic Page Cache module caches pages for authenticated users in the database, minus the personalized parts. For more information, see the online documentation for the Dynamic Page Cache module.', ['!dynamic_page_cache-documentation' => 'https://www.drupal.org/documentation/modules/dynamic_page_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. Personalized parts are excluded automatically. Depending on your site configuration and the complexity of particular pages, Dynamic Page Cache may significantly increase the speed of your site, even for authenticated users.') . '
'; + $output .= '
' . t('The module requires no configuration. Every part of the page contains metadata that allows Dynamic Page Cache to figure this out on its own.') . '
'; + $output .= '
'; + + return $output; + } +} diff --git a/core/modules/dynamic_page_cache/dynamic_page_cache.services.yml b/core/modules/dynamic_page_cache/dynamic_page_cache.services.yml new file mode 100644 index 0000000..2419992 --- /dev/null +++ b/core/modules/dynamic_page_cache/dynamic_page_cache.services.yml @@ -0,0 +1,29 @@ +services: + cache.dynamic_page_cache: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory: cache_factory:get + arguments: [dynamic_page_cache] + dynamic_page_cache_subscriber: + class: Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber + arguments: ['@dynamic_page_cache_request_policy', '@dynamic_page_cache_response_policy', '@render_cache', '%renderer.config%'] + tags: + - { name: event_subscriber } + + # Request & response policies. + dynamic_page_cache_request_policy: + class: Drupal\dynamic_page_cache\PageCache\RequestPolicy\DefaultRequestPolicy + tags: + - { name: service_collector, tag: dynamic_page_cache_request_policy, call: addPolicy} + dynamic_page_cache_response_policy: + class: Drupal\Core\PageCache\ChainResponsePolicy + tags: + - { name: service_collector, tag: dynamic_page_cache_response_policy, call: addPolicy} + lazy: true + dynamic_page_cache_deny_admin_routes: + class: Drupal\dynamic_page_cache\PageCache\ResponsePolicy\DenyAdminRoutes + arguments: ['@current_route_match'] + public: false + tags: + - { name: dynamic_page_cache_response_policy } diff --git a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php new file mode 100644 index 0000000..d5f0e3a --- /dev/null +++ b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php @@ -0,0 +1,322 @@ + [ + 'keys' => ['response'], + 'contexts' => [ + 'route', + // Some routes' controllers rely on the request format (they don't have + // a separate route for each request format). Additionally, a controller + // may be returning a domain object that a KernelEvents::VIEW subscriber + // must turn into an actual response, but perhaps a format is being + // requested that the subscriber does not support. + // @see \Drupal\Core\EventSubscriber\AcceptNegotiation406::onViewDetect406() + 'request_format', + ], + 'bin' => 'dynamic_page_cache', + ], + ]; + + /** + * Constructs a new DynamicPageCacheSubscriber object. + * + * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy + * A policy rule determining the cacheability of a request. + * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy + * A policy rule determining the cacheability of the response. + * @param \Drupal\Core\Render\RenderCacheInterface $render_cache + * The render cache. + * @param array $renderer_config + * The renderer configuration array. + */ + public function __construct(RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, RenderCacheInterface $render_cache, array $renderer_config) { + $this->requestPolicy = $request_policy; + $this->responsePolicy = $response_policy; + $this->renderCache = $render_cache; + $this->rendererConfig = $renderer_config; + } + + /** + * Sets a response in case of a Dynamic Page Cache hit. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The event to process. + */ + public function onRouteMatch(GetResponseEvent $event) { + // Don't cache the response if the Dynamic Page Cache 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; + } + + // Sets the response for the current route, if cached. + $cached = $this->renderCache->get($this->dynamicPageCacheRedirectRenderArray); + if ($cached) { + $response = $this->renderArrayToResponse($cached); + $response->headers->set(self::HEADER, 'HIT'); + $event->setResponse($response); + } + } + + /** + * Stores a response in case of a Dynamic Page Cache miss, if cacheable. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onResponse(FilterResponseEvent $event) { + $response = $event->getResponse(); + + // Dynamic Page Cache only works with cacheable responses. It does not work + // with plain Response objects. (Dynamic Page Cache needs to be able to + // access and modify the cacheability metadata associated with the response.) + if (!$response instanceof CacheableResponseInterface) { + return; + } + + // There's no work left to be done if this is a Dynamic Page Cache hit. + if ($response->headers->get(self::HEADER) === 'HIT') { + return; + } + + // There's no work left to be done if this is an uncacheable response. + if (!$this->shouldCacheResponse($response)) { + // The response is uncacheable, mark it as such. + $response->headers->set(self::HEADER, 'UNCACHEABLE'); + return; + } + + // Don't cache the response if Dynamic Page Cache's request subscriber did + // not fire, because that means it is impossible to have a Dynamic Page + // 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 Dynamic Page + // Cache, because the routing happens in a request subscriber earlier than + // Dynamic Page Cache's and immediately sets a response, i.e. the one + // returned by the subrequest, and thus causes Dynamic Page Cache'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 response if the Dynamic Page CAche 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; + } + + // Embed the response object in a render array so that RenderCache is able + // to cache it, handling cache redirection for us. + $response_as_render_array = $this->responseToRenderArray($response); + $this->renderCache->set($response_as_render_array, $this->dynamicPageCacheRedirectRenderArray); + + // The response was generated, mark the response as a cache miss. The next + // time, it will be a cache hit. + $response->headers->set(self::HEADER, 'MISS'); + } + + /** + * Whether the given response should be cached by Dynamic Page Cache. + * + * We consider any response that has cacheability metadata meeting the auto- + * placeholdering conditions to be uncacheable. Because those conditions + * indicate poor cacheability, and if it doesn't make sense to cache parts of + * a page, then neither does it make sense to cache an entire page. + * + * @param \Drupal\Core\Cache\CacheableResponseInterface + * The response whose cacheability to analyze. + * + * @return bool + * Whether the given response should be cached. + * + * @see \Drupal\Core\Render\Renderer::shouldAutomaticallyPlaceholder() + */ + protected function shouldCacheResponse(CacheableResponseInterface $response) { + $conditions = $this->rendererConfig['auto_placeholder_conditions']; + + $cacheability = $response->getCacheableMetadata(); + + // Response's max-age is at or below the configured threshold. + if ($cacheability->getCacheMaxAge() !== Cache::PERMANENT && $cacheability->getCacheMaxAge() <= $conditions['max-age']) { + return FALSE; + } + + // Response has a high-cardinality cache context. + if (array_intersect($cacheability->getCacheContexts(), $conditions['contexts'])) { + return FALSE; + } + + // Response has a high-invalidation frequency cache tag. + if (array_intersect($cacheability->getCacheTags(), $conditions['tags'])) { + return FALSE; + } + + return TRUE; + } + + /** + * Embeds a Response object in a render array so that RenderCache can cache it. + * + * @param \Drupal\Core\Cache\CacheableResponseInterface $response + * A cacheable response. + * + * @return array + * A render array that embeds the given cacheable response object, with the + * cacheability metadata of the response object present in the #cache + * property of the render array. + * + * @see renderArrayToResponse() + * + * @todo Refactor/remove once https://www.drupal.org/node/2551419 lands. + */ + protected function responseToRenderArray(CacheableResponseInterface $response) { + $response_as_render_array = $this->dynamicPageCacheRedirectRenderArray + [ + // The data we actually care about. + '#response' => $response, + // Tell RenderCache to cache the #response property: the data we actually + // care about. + '#cache_properties' => ['#response'], + // These exist only to fulfill the requirements of the RenderCache, which + // is designed to work with render arrays only. We don't care about these. + '#markup' => '', + '#attached' => '', + ]; + + // Merge the response's cacheability metadata, so that RenderCache can take + // care of cache redirects for us. + CacheableMetadata::createFromObject($response->getCacheableMetadata()) + ->merge(CacheableMetadata::createFromRenderArray($response_as_render_array)) + ->applyTo($response_as_render_array); + + return $response_as_render_array; + } + + /** + * Gets the embedded Response object in a render array. + * + * @param array $render_array + * A render array with a #response property. + * + * @return \Drupal\Core\Cache\CacheableResponseInterface + * The cacheable response object. + * + * @see responseToRenderArray() + */ + protected function renderArrayToResponse(array $render_array) { + return $render_array['#response']; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = []; + + // Run after AuthenticationSubscriber (necessary for the 'user' cache + // context) and MaintenanceModeSubscriber (Dynamic Page Cache should not be + // polluted by maintenance mode-specific behavior), but before + // ContentControllerSubscriber (updates _controller, but that is pointless + // when Dynamic Page Cache 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/dynamic_page_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php b/core/modules/dynamic_page_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php new file mode 100644 index 0000000..4016221 --- /dev/null +++ b/core/modules/dynamic_page_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php @@ -0,0 +1,29 @@ +addPolicy(new CommandLineOrUnsafeMethod()); + } + +} diff --git a/core/modules/dynamic_page_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php b/core/modules/dynamic_page_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php new file mode 100644 index 0000000..d79b659 --- /dev/null +++ b/core/modules/dynamic_page_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php @@ -0,0 +1,51 @@ +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/dynamic_page_cache/src/Tests/DynamicPageCacheIntegrationTest.php b/core/modules/dynamic_page_cache/src/Tests/DynamicPageCacheIntegrationTest.php new file mode 100644 index 0000000..82854a0 --- /dev/null +++ b/core/modules/dynamic_page_cache/src/Tests/DynamicPageCacheIntegrationTest.php @@ -0,0 +1,132 @@ +uninstall(['page_cache']); + } + + /** + * Tests that Dynamic Page Cache works correctly, and verifies the edge cases. + */ + public function testDynamicPageCache() { + // Controllers returning plain response objects are ignored by Dynamic Page + // Cache. + $url = Url::fromUri('route:dynamic_page_cache_test.response'); + $this->drupalGet($url); + $this->assertFalse($this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Response object returned: Dynamic Page Cache is ignoring.'); + + // Controllers returning CacheableResponseInterface (cacheable response) + // objects are handled by Dynamic Page Cache. + $url = Url::fromUri('route:dynamic_page_cache_test.cacheable_response'); + $this->drupalGet($url); + $this->assertEqual('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Cacheable response object returned: Dynamic Page Cache is active, Dynamic Page Cache MISS.'); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Cacheable response object returned: Dynamic Page Cache is active, Dynamic Page Cache HIT.'); + + // Controllers returning render arrays, rendered as HTML responses, are + // handled by Dynamic Page Cache. + $url = Url::fromUri('route:dynamic_page_cache_test.html'); + $this->drupalGet($url); + $this->assertEqual('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as HTML response: Dynamic Page Cache is active, Dynamic Page Cache MISS.'); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as HTML response: Dynamic Page Cache is active, Dynamic Page Cache 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:dynamic_page_cache_test.html.with_cache_contexts', ['query' => ['animal' => $animal]]); + $this->drupalGet($url); + $this->assertRaw($animal); + $this->assertEqual('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as HTML response: Dynamic Page Cache is active, Dynamic Page Cache MISS.'); + $this->drupalGet($url); + $this->assertRaw($animal); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as HTML response: Dynamic Page Cache is active, Dynamic Page Cache HIT.'); + + // Finally, let's also verify that the 'dynamic_page_cache_test.html' + // route continued to see cache hits if we specify a query argument, + // because it *should* ignore it and continue to provide Dynamic Page + // Cache hits. + $url = Url::fromUri('route:dynamic_page_cache_test.html', ['query' => ['animal' => 'piglet']]); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as HTML response: Dynamic Page Cache is active, Dynamic Page Cache HIT.'); + } + + // Controllers returning render arrays, rendered as anything except a HTML + // response, are ignored by Dynamic Page Cache (but only because those + // wrapper formats' responses do not implement CacheableResponseInterface). + $this->drupalGet('dynamic-page-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax'))); + $this->assertFalse($this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as AJAX response: Dynamic Page Cache is ignoring.'); + $this->drupalGet('dynamic-page-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog'))); + $this->assertFalse($this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as dialog response: Dynamic Page Cache is ignoring.'); + $this->drupalGet('dynamic-page-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal'))); + $this->assertFalse($this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as modal response: Dynamic Page Cache is ignoring.'); + + // Admin routes are ignored by Dynamic Page Cache. + $this->drupalGet('dynamic-page-cache-test/html/admin'); + $this->assertFalse($this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Response returned, rendered as HTML response, admin route: Dynamic Page Cache is ignoring'); + $this->drupalGet('dynamic-page-cache-test/response/admin'); + $this->assertFalse($this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Response returned, plain response, admin route: Dynamic Page Cache is ignoring'); + $this->drupalGet('dynamic-page-cache-test/cacheable-response/admin'); + $this->assertFalse($this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Response returned, cacheable response, admin route: Dynamic Page Cache is ignoring'); + + // Max-age = 0 responses are ignored by SmDynamic Page Cache artCache. + $this->drupalGet('dynamic-page-cache-test/html/uncacheable/max-age'); + $this->assertEqual('UNCACHEABLE', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as HTML response, but uncacheable: Dynamic Page Cache is running, but not caching.'); + + // 'user' cache context responses are ignored by Dynamic Page Cache. + $this->drupalGet('dynamic-page-cache-test/html/uncacheable/contexts'); + $this->assertEqual('UNCACHEABLE', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Render array returned, rendered as HTML response, but uncacheable: Dynamic Page Cache is running, but not caching.'); + + // 'current-temperature' cache tag responses are ignored by Dynamic Page + // Cache. + $this->drupalGet('dynamic-page-cache-test/html/uncacheable/tags'); + $this->assertEqual('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'By default, Drupal has no auto-placeholdering cache tags.'); + } + +} diff --git a/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.info.yml b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.info.yml new file mode 100644 index 0000000..7a57fd4 --- /dev/null +++ b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.info.yml @@ -0,0 +1,6 @@ +name: 'Test Dynamic Page Cache' +type: module +description: 'Provides test routes/responses for Dynamic Page Cache.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.routing.yml b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.routing.yml new file mode 100644 index 0000000..77125b6 --- /dev/null +++ b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/dynamic_page_cache_test.routing.yml @@ -0,0 +1,75 @@ +dynamic_page_cache_test.response: + path: '/dynamic-page-cache-test/response' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::response' + requirements: + _access: 'TRUE' + +dynamic_page_cache_test.response.admin: + path: '/dynamic-page-cache-test/response/admin' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::response' + requirements: + _access: 'TRUE' + options: + _admin_route: TRUE + +dynamic_page_cache_test.cacheable_response: + path: '/dynamic-page-cache-test/cacheable-response' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::cacheableResponse' + requirements: + _access: 'TRUE' + +dynamic_page_cache_test.cacheable_response.admin: + path: '/dynamic-page-cache-test/cacheable-response/admin' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::cacheableResponse' + requirements: + _access: 'TRUE' + options: + _admin_route: TRUE + +dynamic_page_cache_test.html: + path: '/dynamic-page-cache-test/html' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::html' + requirements: + _access: 'TRUE' + +dynamic_page_cache_test.html.admin: + path: '/dynamic-page-cache-test/html/admin' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::html' + requirements: + _access: 'TRUE' + options: + _admin_route: TRUE + +dynamic_page_cache_test.html.with_cache_contexts: + path: '/dynamic-page-cache-test/html/with-cache-contexts' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::htmlWithCacheContexts' + requirements: + _access: 'TRUE' + +dynamic_page_cache_test.html.uncacheable.max_age: + path: '/dynamic-page-cache-test/html/uncacheable/max-age' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::htmlUncacheableMaxAge' + requirements: + _access: 'TRUE' + +dynamic_page_cache_test.html.uncacheable.contexts: + path: '/dynamic-page-cache-test/html/uncacheable/contexts' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::htmlUncacheableContexts' + requirements: + _access: 'TRUE' + +dynamic_page_cache_test.html.uncacheable.tags: + path: '/dynamic-page-cache-test/html/uncacheable/tags' + defaults: + _controller: '\Drupal\dynamic_page_cache_test\DynamicPageCacheTestController::htmlUncacheableTags' + requirements: + _access: 'TRUE' diff --git a/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/src/DynamicPageCacheTestController.php b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/src/DynamicPageCacheTestController.php new file mode 100644 index 0000000..4371a29 --- /dev/null +++ b/core/modules/dynamic_page_cache/tests/dynamic_page_cache_test/src/DynamicPageCacheTestController.php @@ -0,0 +1,139 @@ +label()); + $response->addCacheableDependency($user); + return $response; + } + + /** + * A route returning a render array (without cache contexts, so cacheable). + * + * @return array + * A render array. + */ + public function html() { + return [ + 'content' => [ + '#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 htmlUncacheableMaxAge() { + $build = $this->html(); + $build['very_dynamic_part'] = [ + '#markup' => 'Drupal cannot handle the awesomeness of llamas.', + '#cache' => [ + 'max-age' => 0, + ], + ]; + return $build; + } + + /** + * A route returning a render array (with 'user' context, so uncacheable) + * + * @return array + * A render array. + * + * @see html() + */ + public function htmlUncacheableContexts() { + $build = $this->html(); + $build['very_dynamic_part'] = [ + '#markup' => 'Drupal cannot handle the awesomeness of llamas.', + '#cache' => [ + 'contexts' => [ + 'user', + ], + ], + ]; + return $build; + } + + /** + * A route returning a render array (with max-age=0, so uncacheable) + * + * @return array + * A render array. + * + * @see html() + */ + public function htmlUncacheableTags() { + $build = $this->html(); + $build['very_dynamic_part'] = [ + '#markup' => 'Drupal cannot handle the awesomeness of llamas.', + '#cache' => [ + 'tags' => [ + 'current-temperature', + ], + ], + ]; + return $build; + } + +} 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..be051cf 100644 --- a/core/modules/page_cache/page_cache.module +++ b/core/modules/page_cache/page_cache.module @@ -5,9 +5,8 @@ * Caches responses for anonymous users, request and response policies allowing. */ -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\PageCache\RequestPolicyInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; /** * Implements hook_help(). @@ -21,7 +20,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 entire pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '
'; + $output .= '
' . t('To speed up your site for authenticated users, see the Dynamic Page Cache module.', ['!dynamic_page_cache-help' => (\Drupal::moduleHandler()->moduleExists('dynamic_page_cache')) ? Url::fromRoute('help.page', ['name' => 'dynamic_page_cache'])->toString() : '#']) . '

'; $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/system/src/EventSubscriber/ConfigCacheTag.php b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php index 640a6f0..7705056 100644 --- a/core/modules/system/src/EventSubscriber/ConfigCacheTag.php +++ b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php @@ -61,7 +61,7 @@ public function onSave(ConfigCrudEvent $event) { } // Theme configuration and global theme settings. - if (in_array($event->getConfig()->getName(), ['system.theme', 'system.theme.global'])) { + if (in_array($event->getConfig()->getName(), ['system.theme', 'system.theme.global'], TRUE)) { $this->cacheTagsInvalidator->invalidateTags(['rendered']); } diff --git a/core/modules/system/src/Tests/Session/SessionTest.php b/core/modules/system/src/Tests/Session/SessionTest.php index c194c79..037054c 100644 --- a/core/modules/system/src/Tests/Session/SessionTest.php +++ b/core/modules/system/src/Tests/Session/SessionTest.php @@ -153,6 +153,11 @@ public function testSessionPersistenceOnLogin() { * Test that empty anonymous sessions are destroyed. */ function testEmptyAnonymousSession() { + // Disable the dynamic_page_cache module; it'd cause session_test's debug + // output (that is added in + // SessionTestSubscriber::onKernelResponseSessionTest()) to not be added. + $this->container->get('module_installer')->uninstall(['dynamic_page_cache']); + // Verify that no session is automatically created for anonymous user when // page caching is disabled. $this->container->get('module_installer')->uninstall(['page_cache']); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index c12a2d4..27ff1b3 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -90,7 +90,7 @@ function system_help($route_name, RouteMatchInterface $route_match) { $output .= '
' . t('Using maintenance mode') . '
'; $output .= '
' . t('When you are performing site maintenance, you can prevent non-administrative users (including anonymous visitors) from viewing your site by putting it in Maintenance mode. This will prevent unauthorized users from making changes to the site while you are performing maintenance, or from seeing a broken site while updates are in progress.', array('!maintenance-mode' => \Drupal::url('system.site_maintenance_mode'))) . '
'; $output .= '
' . t('Configuring for performance') . '
'; - $output .= '
' . t('On the Performance page, the site can be configured to aggregate CSS and JavaScript files, making the total request size smaller. Note that, for small- to medium-sized websites, the Internal Page Cache module should be installed so that pages are efficiently cached and reused.', array('!performance-page' => \Drupal::url('system.performance_settings'), '!page-cache' => (\Drupal::moduleHandler()->moduleExists('page_cache')) ? \Drupal::url('help.page', array('name' => 'page_cache')) : '#')) . '
'; + $output .= '
' . t('On the Performance page, the site can be configured to aggregate CSS and JavaScript files, making the total request size smaller. Note that, for small- to medium-sized websites, the Internal Page Cache module should be installed so that pages are efficiently cached and reused for anonymous users. Finally, for websites of all sizes, the Dynamic Page Cache module should also be installed so that the non-personalized parts of pages are efficiently cached (for all users).', array('!performance-page' => \Drupal::url('system.performance_settings'), '!page-cache' => (\Drupal::moduleHandler()->moduleExists('page_cache')) ? \Drupal::url('help.page', array('name' => 'page_cache')) : '#', '!dynamic-page-cache' => (\Drupal::moduleHandler()->moduleExists('dynamic_page_cache')) ? \Drupal::url('help.page', array('name' => 'dynamic_page_cache')) : '#')) . '
'; $output .= '
' . t('Configuring cron') . '
'; $output .= '
' . t('In order for the site and its modules to continue to operate well, a set of routine administrative operations must run on a regular basis; these operations are known as cron tasks. On the Cron page, you can configure cron to run periodically as part of normal page requests, or you can turn this off and trigger cron from an outside process on your web server. You can verify the status of cron tasks by visiting the Status report page. For more information, see the online documentation for configuring cron jobs.', array('!status' => \Drupal::url('system.status'), '!handbook' => 'https://www.drupal.org/cron', '!cron' => \Drupal::url('system.cron_settings'))) . '
'; $output .= '
' . t('Configuring the file system') . '
'; diff --git a/core/profiles/minimal/minimal.info.yml b/core/profiles/minimal/minimal.info.yml index 206b8e7..2617270 100644 --- a/core/profiles/minimal/minimal.info.yml +++ b/core/profiles/minimal/minimal.info.yml @@ -8,5 +8,6 @@ dependencies: - block - dblog - page_cache + - dynamic_page_cache themes: - stark diff --git a/core/profiles/standard/src/Tests/StandardTest.php b/core/profiles/standard/src/Tests/StandardTest.php index 2a7f4ef..f12b2a6 100644 --- a/core/profiles/standard/src/Tests/StandardTest.php +++ b/core/profiles/standard/src/Tests/StandardTest.php @@ -9,6 +9,8 @@ use Drupal\config\Tests\SchemaCheckTestTrait; use Drupal\contact\Entity\ContactForm; +use Drupal\Core\Url; +use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber; use Drupal\filter\Entity\FilterFormat; use Drupal\simpletest\WebTestBase; use Drupal\user\Entity\Role; @@ -177,5 +179,28 @@ function testStandard() { $this->assertText('Max 650x650'); $this->assertText('Max 1300x1300'); $this->assertText('Max 2600x2600'); + + // Verify certain routes' responses are cacheable by Dynamic Page Cache, to + // ensure these responses are very fast for authenticated users. + $this->dumpHeaders = TRUE; + $this->drupalLogin($this->adminUser); + $url = Url::fromRoute('contact.site_page'); + $this->drupalGet($url); + $this->assertEqual('UNCACHEABLE', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Site-wide contact page cannot be cached by Dynamic Page Cache.'); + + $url = Url::fromRoute(''); + $this->drupalGet($url); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Frontpage is cached by Dynamic Page Cache.'); + + $url = Url::fromRoute('entity.node.canonical', ['node' => 1]); + $this->drupalGet($url); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'Full node page is cached by Dynamic Page Cache.'); + + $url = Url::fromRoute('entity.user.canonical', ['user' => 1]); + $this->drupalGet($url); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER), 'User profile page is cached by Dynamic Page Cache.'); } } diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml index a356ae8..68fcba2 100644 --- a/core/profiles/standard/standard.info.yml +++ b/core/profiles/standard/standard.info.yml @@ -26,6 +26,7 @@ dependencies: - options - path - page_cache + - dynamic_page_cache - taxonomy - dblog - search diff --git a/core/profiles/testing/testing.info.yml b/core/profiles/testing/testing.info.yml index 5ded376..28fc4e8 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 dynamic_page_cache in testing, to ensure that as many + # tests as possible run with them enabled. - page_cache + - dynamic_page_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 23090a4..a5d50f2 100644 --- a/sites/example.settings.local.php +++ b/sites/example.settings.local.php @@ -52,12 +52,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. 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 Dynamic Page Cache. + * + * Note: you should test with Dynamic Page 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. + */ +# $settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.null'; /** * Allow test modules and themes to be installed.