core/includes/common.inc | 13 +++ core/includes/menu.inc | 41 ++++---- .../menu/lib/Drupal/menu/Tests/MenuTest.php | 60 +++++++++++ .../lib/Drupal/menu_link/Entity/MenuLink.php | 13 ++- .../system/lib/Drupal/system/Entity/Menu.php | 19 ++++ .../Tests/Cache/PageCacheTagsIntegrationTest.php | 105 ++++++++++++++++++++ 6 files changed, 228 insertions(+), 23 deletions(-) diff --git a/core/includes/common.inc b/core/includes/common.inc index b97cc5e..ff13861 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -3562,6 +3562,19 @@ function drupal_prepare_page($page) { // 'sidebar_first', 'footer', etc. drupal_alter('page', $page); + // The "main" and "secondary" menus are never part of the page-level render + // array and therefor their cache tags will never bubble up into the page + // cache, even though they should be. This happens because they're rendered + // directly by the theme system. + if (theme_get_setting('features.main_menu') && count(menu_main_menu())) { + $main_links_source = _menu_get_links_source('main_links', 'main'); + $page['page_top']['#cache']['tags']['menu'][$main_links_source] = $main_links_source; + } + if (theme_get_setting('features.secondary_menu') && count(menu_secondary_menu())) { + $secondary_links_source = _menu_get_links_source('secondary_links', 'account'); + $page['page_top']['#cache']['tags']['menu'][$secondary_links_source] = $secondary_links_source; + } + // If no module has taken care of the main content, add it to the page now. // This allows the site to still be usable even if no modules that // control page regions (for example, the Block module) are enabled. diff --git a/core/includes/menu.inc b/core/includes/menu.inc index bbf5c43..814c9d9 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -1100,6 +1100,9 @@ function menu_tree_output($tree) { // Add the theme wrapper for outer markup. // Allow menu-specific theme overrides. $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_'); + // Set cache tag. + $menu_name = $data['link']['menu_name']; + $build['#cache']['tags']['menu'][$menu_name] = $menu_name; } return $build; @@ -1758,10 +1761,7 @@ function menu_list_system_menus() { * Returns an array of links to be rendered as the Main menu. */ function menu_main_menu() { - $config = \Drupal::config('menu.settings'); - $menu_enabled = module_exists('menu'); - // When menu module is not enabled, we need a hardcoded default value. - $main_links_source = $menu_enabled ? $config->get('main_links') : 'main'; + $main_links_source = _menu_get_links_source('main_links', 'main'); return menu_navigation_links($main_links_source); } @@ -1769,11 +1769,8 @@ function menu_main_menu() { * Returns an array of links to be rendered as the Secondary links. */ function menu_secondary_menu() { - $config = \Drupal::config('menu.settings'); - $menu_enabled = module_exists('menu'); - // When menu module is not enabled, we need a hardcoded default value. - $main_links_source = $menu_enabled ? $config->get('main_links') : 'main'; - $secondary_links_source = $menu_enabled ? $config->get('secondary_links') : 'account'; + $main_links_source = _menu_get_links_source('main_links', 'main'); + $secondary_links_source = _menu_get_links_source('secondary_links', 'account'); // If the secondary menu source is set as the primary menu, we display the // second level of the primary menu. @@ -1786,6 +1783,23 @@ function menu_secondary_menu() { } /** + * Returns the source of links of a menu. + * + * @param string $name + * A string configuration key of menu link source. + * @param string $default + * Default menu name. + * + * @return string + * Returns menu name, if exist + */ +function _menu_get_links_source($name, $default) { + $config = \Drupal::config('menu.settings'); + // When menu module is not enabled, we need a hardcoded default value. + return \Drupal::moduleHandler()->moduleExists('menu') ? $config->get($name) : $default; +} + +/** * Returns an array of links for a navigation menu. * * @param $menu_name @@ -2404,15 +2418,6 @@ function menu_get_active_trail() { } /** - * Clears the cached cached data for a single named menu. - */ -function menu_cache_clear($menu_name = 'tools') { - Cache::deleteTags(array('menu' => $menu_name)); - // Also clear the menu system static caches. - menu_reset_static_cache(); -} - -/** * Clears all cached menu data. * * This should be called any time broad changes diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php index 48f9361..d03012d 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php @@ -457,6 +457,66 @@ public function testBlockContextualLinks() { } /** + * Test that cache tags are properly set and bubbled up to the page cache. + * + * Ensures that invalidation of the "menu:" cache tags works. + */ + public function testMenuBlockPageCacheTags() { + // Enable page caching. + $config = \Drupal::config('system.performance'); + $config->set('cache.page.use_internal', 1); + $config->set('cache.page.max_age', 300); + $config->save(); + + // Create a Llama menu, add a link to it and place the corresponding block. + $menu = entity_create('menu', array( + 'id' => 'llama', + 'label' => 'Llama', + 'description' => 'Description text', + )); + $menu->save(); + $menu_link = entity_create('menu_link', array( + 'link_path' => '', + 'link_title' => 'Vicuña', + 'menu_name' => 'llama', + )); + $menu_link->save(); + $block = $this->drupalPlaceBlock('system_menu_block:llama', array('label' => 'Llama', 'module' => 'system', 'region' => 'footer')); + + // Prime the page cache. + $this->drupalGet('test-page'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + + // Verify a cache hit, but also the presence of the correct cache tags. + $this->drupalGet('test-page'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); + $cid_parts = array(url('test-page', array('absolute' => TRUE)), 'html'); + $cid = sha1(implode(':', $cid_parts)); + $cache_entry = \Drupal::cache('page')->get($cid); + $this->assertIdentical($cache_entry->tags, array('content:1', 'menu:llama')); + + // The "Llama" menu is modified. + $menu->label = 'Awesome llama'; + $menu->save(); + + // Verify that after the modified menu, there is a cache miss. + $this->drupalGet('test-page'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + + // Verify a cache hit. + $this->drupalGet('test-page'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT'); + + // A link in the "Llama" menu is modified. + $menu_link->link_title = 'Guanaco'; + $menu_link->save(); + + // Verify that after the modified menu link, there is a cache miss. + $this->drupalGet('test-page'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + } + + /** * Tests menu link bundles. */ public function testMenuBundles() { diff --git a/core/modules/menu_link/lib/Drupal/menu_link/Entity/MenuLink.php b/core/modules/menu_link/lib/Drupal/menu_link/Entity/MenuLink.php index 6d70014..be72585 100644 --- a/core/modules/menu_link/lib/Drupal/menu_link/Entity/MenuLink.php +++ b/core/modules/menu_link/lib/Drupal/menu_link/Entity/MenuLink.php @@ -7,6 +7,7 @@ namespace Drupal\menu_link\Entity; +use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\Entity; use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\menu_link\MenuLinkInterface; @@ -446,9 +447,9 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont } } - foreach ($affected_menus as $menu_name) { - menu_cache_clear($menu_name); - } + Cache::invalidateTags(array('menu' => array_keys($affected_menus))); + // Also clear the menu system static caches. + menu_reset_static_cache(); _menu_clear_page_cache(); } @@ -527,10 +528,12 @@ public function postSave(EntityStorageControllerInterface $storage_controller, $ // Check the has_children status of the parent. $storage_controller->updateParentalStatus($this); - menu_cache_clear($this->menu_name); + Cache::invalidateTags(array('menu' => $this->menu_name)); if (isset($this->original) && $this->menu_name != $this->original->menu_name) { - menu_cache_clear($this->original->menu_name); + Cache::invalidateTags(array('menu' => $this->original->menu_name)); } + // Also clear the menu system static caches. + menu_reset_static_cache(); // Now clear the cache. _menu_clear_page_cache(); diff --git a/core/modules/system/lib/Drupal/system/Entity/Menu.php b/core/modules/system/lib/Drupal/system/Entity/Menu.php index 644629a..6e8fce8 100644 --- a/core/modules/system/lib/Drupal/system/Entity/Menu.php +++ b/core/modules/system/lib/Drupal/system/Entity/Menu.php @@ -7,6 +7,7 @@ namespace Drupal\system\Entity; +use Drupal\Core\Cache\Cache; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageControllerInterface; use Drupal\system\MenuInterface; @@ -90,4 +91,22 @@ public function isLocked() { return (bool) $this->locked; } + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) { + parent::postSave($storage_controller, $update); + + Cache::invalidateTags(array('menu' => $this->id())); + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) { + parent::postDelete($storage_controller, $entities); + + Cache::invalidateTags(array('menu' => array_keys($entities))); + } + } diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php new file mode 100644 index 0000000..4b4c799 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Cache/PageCacheTagsIntegrationTest.php @@ -0,0 +1,105 @@ + 'Page cache tags integration test', + 'description' => 'Enable the page cache and test its cache tags in various scenarios.', + 'group' => 'Cache', + ); + } + + function setUp() { + parent::setUp(); + + $config = \Drupal::config('system.performance'); + $config->set('cache.page.use_internal', 1); + $config->set('cache.page.max_age', 300); + $config->save(); + } + + /** + * Test that cache tags are properly bubbled up to the page level. + */ + function testPageCacheTags() { + // Create two nodes. + $author_1 = $this->drupalCreateUser(); + $node_1_path = 'node/' . $this->drupalCreateNode(array( + 'uid' => $author_1->id(), + 'title' => 'Node 1', + 'body' => array( + 0 => array('value' => 'Body 1', 'format' => 'basic_html'), + ), + 'promote' => NODE_PROMOTED, + ))->id(); + $author_2 = $this->drupalCreateUser(); + $node_2_path = 'node/' . $this->drupalCreateNode(array( + 'uid' => $author_2->id(), + 'title' => 'Node 2', + 'body' => array( + 0 => array('value' => 'Body 2', 'format' => 'full_html'), + ), + 'promote' => NODE_PROMOTED, + ))->id(); + + // Full node page 1. + $this->verifyPageCacheTags($node_1_path, array( + 'content:1', + 'user:' . $author_1->id(), + 'filter_format:basic_html', + 'menu:footer', + 'menu:main', + )); + + // Full node page 2. + $this->verifyPageCacheTags($node_2_path, array( + 'content:1', + 'user:' . $author_2->id(), + 'filter_format:full_html', + 'menu:footer', + 'menu:main', + )); + } + + /** + * Fills page cache for the given path, verify cache tags on page cache hit. + * + * @param $path + * The Drupal page path to test. + * @param $expected_tags + * The expected cache tags for the page cache entry of the given $path. + */ + protected function verifyPageCacheTags($path, $expected_tags) { + $this->drupalGet($path); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); + $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, $expected_tags); + } + +}