diff --git a/core/includes/common.inc b/core/includes/common.inc index 580ef214ad5dc64bedb12cc3298c77e43931ea26..b97cc5e509450963c985b335d0c6062cd6be3ca1 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -3147,7 +3147,7 @@ function drupal_page_set_cache(Response $response, Request $request) { // because by the time it is read, the configuration might change. 'page_compressed' => $page_compressed, ), - 'tags' => array('content' => TRUE) + drupal_cache_tags_page_get(), + 'tags' => array('content' => TRUE) + drupal_cache_tags_page_get($response), 'expire' => Cache::PERMANENT, 'created' => REQUEST_TIME, ); @@ -4358,36 +4358,21 @@ function drupal_render_collect_cache_tags($element, $tags = array()) { return $tags; } -/** - * A #post_render callback at the top level of the $page array. Collects the - * tags for use in page cache. - * - * @param string $children - * An HTML string of rendered output. - * @param array $elements - * A render array. - * - * @return string - * The same $children that was passed in - no modifications. - */ -function drupal_post_render_cache_tags_page_set($children, array $elements) { - if (drupal_page_is_cacheable()) { - $tags = &drupal_static('system_cache_tags_page', array()); - $tags = drupal_render_collect_cache_tags($elements); - } - return $children; -} - /** * Return the cache tags that were stored during drupal_render_page(). * + * @param \Symfony\Component\HttpFoundation\Response $response + * The response object. * @return array * An array of cache tags. * - * @see drupal_post_render_cache_tags_page_set() + * @see \Drupal\Core\EventSubscriber\HtmlViewSubscriber::onHtmlPage() */ -function drupal_cache_tags_page_get() { - return drupal_static('system_cache_tags_page', array()); +function drupal_cache_tags_page_get(Response $response) { + if (($tags = $response->headers->get('cache_tags')) && $tags = unserialize($tags)) { + return $tags; + } + return array(); } /** diff --git a/core/lib/Drupal/Core/Cache/CacheableInterface.php b/core/lib/Drupal/Core/Cache/CacheableInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..da4f04c2acdc38520adfed242449edc325a48c40 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheableInterface.php @@ -0,0 +1,54 @@ +<?php +/** + * @file + * Contains \Drupal\Core\CacheableInterface + */ + +namespace Drupal\Core\Cache; + +/** + * Defines an interface for objects which are potentially cacheable. + */ +interface CacheableInterface { + + /** + * The cache keys associated with this potentially cacheable object. + * + * @return array + * An array of strings or cache constants, used to generate a cache ID. + */ + public function getCacheKeys(); + + /** + * The cache tags associated with this potentially cacheable object. + * + * @return array + * An array of cache tags. + */ + public function getCacheTags(); + + /** + * The bin to use for this potentially cacheable object. + * + * @return string + * The name of the cache bin to use. + */ + public function getCacheBin(); + + /** + * The maximum age for which this object may be cached. + * + * @return int + * The maximum time in seconds that this object may be cached. + */ + public function getCacheMaxAge(); + + /** + * Indicates whether this object is cacheable. + * + * @return bool + * Returns TRUE if the object is cacheable, FALSE otherwise. + */ + public function isCacheable(); + +} diff --git a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php index bf7be8df3c203f88387d469209b14aa903c093d8..0204bc06e1e9975f4bb410901a2c2735d930b855 100644 --- a/core/lib/Drupal/Core/Controller/HtmlControllerBase.php +++ b/core/lib/Drupal/Core/Controller/HtmlControllerBase.php @@ -71,7 +71,9 @@ protected function createHtmlFragment($page_content, Request $request) { ); } - $fragment = new HtmlFragment(drupal_render($page_content)); + $cache_tags = $this->drupalRenderCollectCacheTags($page_content); + $cache = !empty($cache_tags) ? array('tags' => $cache_tags) : array(); + $fragment = new HtmlFragment($this->drupalRender($page_content), $cache); // A title defined in the return always wins. if (isset($page_content['#title'])) { @@ -84,4 +86,22 @@ protected function createHtmlFragment($page_content, Request $request) { return $fragment; } + /** + * Wraps drupal_render(). + * + * @todo: Remove as part of https://drupal.org/node/2182149 + */ + protected function drupalRender(&$elements, $is_recursive_call = FALSE) { + return drupal_render($elements, $is_recursive_call); + } + + /** + * Wraps drupal_render_collect_cache_tags() + * + * @todo: Remove as part of https://drupal.org/node/2182149 + */ + protected function drupalRenderCollectCacheTags($element, $tags = array()) { + return drupal_render_collect_cache_tags($element, $tags); + } + } diff --git a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php index f66b6040f0cb37095412cfea5b62e041868f0e82..ae7c9a1b7465abc448a67a2cdfc1e5874bf62623 100644 --- a/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/HtmlViewSubscriber.php @@ -65,6 +65,18 @@ public function onHtmlPage(GetResponseForControllerResultEvent $event) { // to return an object implementing __toString(), but that is not // recommended. $response = new Response((string) $this->renderer->renderPage($page), $page->getStatusCode()); + if ($tags = $page->getCacheTags()) { + $response->headers->set('cache_tags', serialize($tags)); + } + if ($keys = $page->getCacheKeys()) { + $response->headers->set('cache_keys', serialize($keys)); + } + if ($bin = $page->getCacheBin()) { + $response->headers->set('cache_bin', $bin); + } + if ($max_age = $page->getCacheMaxAge()) { + $response->headers->set('cache_max_age', $max_age); + } $event->setResponse($response); } } diff --git a/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php b/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php index ad29a5b08403de5923a49500c5147f06f0f43771..0d3b6d50d69628baad039017107d43ed6ecd552a 100644 --- a/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php +++ b/core/lib/Drupal/Core/Page/DefaultHtmlPageRenderer.php @@ -36,21 +36,27 @@ public function __construct(LanguageManager $language_manager) { * {@inheritdoc} */ public function render(HtmlFragment $fragment, $status_code = 200) { - $page = new HtmlPage('', $fragment->getTitle()); - + // Converts the given HTML fragment which represents the main content region + // of the page into a render array. $page_content['main'] = array( '#markup' => $fragment->getContent(), + '#cache' => array('tags' => $fragment->getCacheTags()), ); - $page_content['#title'] = $page->getTitle(); + $page_content['#title'] = $fragment->getTitle(); + // Build the full page array by calling drupal_prepare_page(), which invokes + // hook_page_build(). This adds the other regions to the page. $page_array = drupal_prepare_page($page_content); - $page = $this->preparePage($page, $page_array); + // Collect cache tags for all the content in all the regions on the page. + $tags = drupal_render_collect_cache_tags($page_array); + // Build the HtmlPage object. + $page = new HtmlPage('', array('tags' => $tags), $fragment->getTitle()); + $page = $this->preparePage($page, $page_array); $page->setBodyTop(drupal_render($page_array['page_top'])); $page->setBodyBottom(drupal_render($page_array['page_bottom'])); $page->setContent(drupal_render($page_array)); - $page->setStatusCode($status_code); return $page; diff --git a/core/lib/Drupal/Core/Page/HtmlFragment.php b/core/lib/Drupal/Core/Page/HtmlFragment.php index 3a50d5e9707f70976255001f90b2dfb82ea7ded8..cc5cac5a6c4ebb0da58336a382f96f5499f4715b 100644 --- a/core/lib/Drupal/Core/Page/HtmlFragment.php +++ b/core/lib/Drupal/Core/Page/HtmlFragment.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\Xss; +use Drupal\Core\Cache\CacheableInterface; use Drupal\Core\Utility\Title; /** @@ -18,7 +19,7 @@ * https://drupal.org/node/1871596#comment-7134686 * @todo Add method replacements for *all* data sourced by html.tpl.php. */ -class HtmlFragment { +class HtmlFragment implements CacheableInterface { /** * HTML content string. @@ -34,14 +35,30 @@ class HtmlFragment { */ protected $title = ''; + /** + * The cache metadata of this HtmlFragment. + * + * @var array + */ + protected $cache = array(); + /** * Constructs a new HtmlFragment. * * @param string $content * The content for this fragment. + * @param array $cache_info + * The cache information. */ - public function __construct($content = '') { + public function __construct($content = '', array $cache_info = array()) { $this->content = $content; + $this->cache = $cache_info + array( + 'keys' => array(), + 'tags' => array(), + 'bin' => NULL, + 'max_age' => 0, + 'is_cacheable' => TRUE, + ); } /** @@ -123,4 +140,41 @@ public function getTitle() { return $this->title; } + /** + * {@inheritdoc} + * + * @TODO Use a trait once we require php 5.4 for all the cache methods. + */ + public function getCacheKeys() { + return $this->cache['keys']; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return $this->cache['tags']; + } + + /** + * {@inheritdoc} + */ + public function getCacheBin() { + return $this->cache['bin']; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return $this->cache['max_age']; + } + + /** + * {@inheritdoc} + */ + public function isCacheable() { + return $this->cache['is_cacheable']; + } + } diff --git a/core/lib/Drupal/Core/Page/HtmlPage.php b/core/lib/Drupal/Core/Page/HtmlPage.php index 6996d7c1e1711d971412679f8e4f7aef62c9aeba..c2f09edc5a4b4c2ce1391caf42916ce7766778ec 100644 --- a/core/lib/Drupal/Core/Page/HtmlPage.php +++ b/core/lib/Drupal/Core/Page/HtmlPage.php @@ -54,11 +54,13 @@ class HtmlPage extends HtmlFragment { * * @param string $content * (optional) The body content of the page. + * @param array $cache_info + * The cache information. * @param string $title * (optional) The title of the page. */ - public function __construct($content = '', $title = '') { - parent::__construct($content); + public function __construct($content = '', array $cache_info = array(), $title = '') { + parent::__construct($content, $cache_info); $this->title = $title; diff --git a/core/modules/aggregator/aggregator.routing.yml b/core/modules/aggregator/aggregator.routing.yml index 33ac34dd035a30a33027a1b55d435cb29cbd1cd7..1f3464deebc10950aa26a3c8df984671543bec5d 100644 --- a/core/modules/aggregator/aggregator.routing.yml +++ b/core/modules/aggregator/aggregator.routing.yml @@ -57,7 +57,7 @@ aggregator.feed_edit: aggregator.feed_refresh: path: '/admin/config/services/aggregator/update/{aggregator_feed}' defaults: - _controller: '\Drupal\aggregator\Controller\AggregatorController::feedRefresh' + _content: '\Drupal\aggregator\Controller\AggregatorController::feedRefresh' _title: 'Update items' requirements: _permission: 'administer news feeds' diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php index 2df738732e32b1a5bc326a048e65135e70263466..7ba5b2a2162ab95939a00c1859027f9074816540 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php @@ -176,7 +176,7 @@ public function getOverviewRoute() { return new Route( $this->getBaseRoute()->getPath() . '/translate', array( - '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage', + '_content' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage', 'plugin_id' => $this->getPluginId(), ), array('_config_translation_overview_access' => 'TRUE') diff --git a/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php index 3df6ac024500627f4b5b1dd3540a743bbaaf6134..ac8a13a30bba6409407b464e1deb74490fddac54 100644 --- a/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php +++ b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php @@ -176,7 +176,7 @@ public function testGetOverviewRouteParameters() { public function testGetOverviewRoute() { $expected = new Route('/admin/config/system/site-information/translate', array( - '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage', + '_content' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage', 'plugin_id' => 'system.site_information_settings', ), array( diff --git a/core/modules/node/lib/Drupal/node/Tests/NodePageCacheTest.php b/core/modules/node/lib/Drupal/node/Tests/NodePageCacheTest.php index 4a2385cd0119392df3caa4c71b69fba3bbadff26..49bc190faed8a4bdcff2172ceb906741eeb122bd 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodePageCacheTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodePageCacheTest.php @@ -48,11 +48,18 @@ function setUp() { * Tests deleting nodes clears page cache. */ public function testNodeDelete() { - $node_path = 'node/' . $this->drupalCreateNode()->id(); + $author = $this->drupalCreateUser(); + $node_path = 'node/' . $this->drupalCreateNode(array('uid' => $author->id()))->id(); // Populate page cache. $this->drupalGet($node_path); + // Verify the presence of the correct cache tags. + $cid_parts = array(url($node_path, array('absolute' => TRUE)), 'html'); + $cid = sha1(implode(':', $cid_parts)); + $cache_entry = \Drupal::cache('page')->get($cid); + $this->assertIdentical($cache_entry->tags, array('content:1', 'user:' . $author->id(), 'filter_format:plain_text')); + // Login and delete the node. $this->drupalLogin($this->adminUser); $this->drupalGet($node_path . '/delete'); diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 6efecef525132e2c704d22c129b6b37cfe259433..c6b53bf0c4055c406c8c22750aa5b9f4d1417a0a 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -1,6 +1,7 @@ <?php use Drupal\Core\Database\Database; +use Drupal\Core\Page\HtmlPage; use Drupal\simpletest\TestBase; use Symfony\Component\Process\PhpExecutableFinder; diff --git a/core/modules/system/lib/Drupal/system/Controller/BatchController.php b/core/modules/system/lib/Drupal/system/Controller/BatchController.php index 2e7480d6af84c182dd94bb249e0b6065d292a2c2..340cd75a059ac21b945b4155545e77dfb9cd8727 100644 --- a/core/modules/system/lib/Drupal/system/Controller/BatchController.php +++ b/core/modules/system/lib/Drupal/system/Controller/BatchController.php @@ -102,7 +102,7 @@ public function render(array $output, $status_code = 200) { $request = \Drupal::request(); $output['#title'] = $this->titleResolver->getTitle($request, $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)); } - $page = new HtmlPage('', $output['#title']); + $page = new HtmlPage('', isset($output['#cache']) ? $output['#cache'] : array(), $output['#title']); $page_array = drupal_prepare_page($output); diff --git a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php index aec3355fca7148ff9fb17759a173d081e245ae54..3ca1f19f34d3f209f0468bd07c41a90ca539d25e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php @@ -58,8 +58,15 @@ function testPageCacheTags() { $tags = array('system_test_cache_tags_page' => TRUE); $this->drupalGet($path); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + + // Verify a cache hit, but also the presence of the correct cache tags. $this->drupalGet($path); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); + $cid_parts = array(url($path, array('absolute' => TRUE)), 'html'); + $cid = sha1(implode(':', $cid_parts)); + $cache_entry = \Drupal::cache('page')->get($cid); + $this->assertIdentical($cache_entry->tags, array('content:1', 'system_test_cache_tags_page:1')); + Cache::invalidateTags($tags); $this->drupalGet($path); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 4f92d7352cde73e42bdaeed10365c1a5cfa417f8..cc647a23c9e55592b41f55a85d055c68ae9ed725 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -269,7 +269,6 @@ function system_element_info() { '#theme_wrappers' => array('form'), ); $types['page'] = array( - '#post_render' => array('drupal_post_render_cache_tags_page_set'), '#show_messages' => TRUE, '#theme' => 'page', ); diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml index ecd03ed59c59318583a533b4db03c8fcba21048b..0a6ee6f555ad375edfd440b640bb32054c078e80 100644 --- a/core/modules/system/tests/modules/system_test/system_test.routing.yml +++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml @@ -48,7 +48,7 @@ system_test.lock_exit: system_test.cache_tags_page: path: '/system-test/cache_tags_page' defaults: - _controller: '\Drupal\system_test\Controller\SystemTestController::system_test_cache_tags_page' + _content: '\Drupal\system_test\Controller\SystemTestController::system_test_cache_tags_page' requirements: _access: 'TRUE'