diff --git a/core/core.services.yml b/core/core.services.yml index 640440b..6721015 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -174,7 +174,7 @@ services: arguments: ['@container.namespaces', '@controller_resolver', '@request', '@module_handler'] plugin.manager.menu.local_task: class: Drupal\Core\Menu\LocalTaskManager - arguments: ['@container.namespaces', '@controller_resolver', '@request', '@router.route_provider', '@module_handler'] + arguments: ['@container.namespaces', '@controller_resolver', '@request', '@router.route_provider', '@module_handler', '@cache.cache', '@language_manager'] request: class: Symfony\Component\HttpFoundation\Request # @TODO the synthetic setting must be uncommented whenever drupal_session_initialize() diff --git a/core/lib/Drupal/Core/Menu/LocalTaskManager.php b/core/lib/Drupal/Core/Menu/LocalTaskManager.php index 70db0b0..eba42ea 100644 --- a/core/lib/Drupal/Core/Menu/LocalTaskManager.php +++ b/core/lib/Drupal/Core/Menu/LocalTaskManager.php @@ -7,7 +7,10 @@ namespace Drupal\Core\Menu; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Core\Routing\RouteProviderInterface; use Symfony\Component\HttpFoundation\Request; @@ -50,6 +53,7 @@ class LocalTaskManager extends DefaultPluginManager { */ protected $routeProvider; + /** * Constructs a \Drupal\Core\Menu\LocalTaskManager object. * @@ -63,14 +67,19 @@ class LocalTaskManager extends DefaultPluginManager { * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider * The route provider to load routes by name. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler.u + * The module handler. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache backend. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. */ - public function __construct(\Traversable $namespaces, ControllerResolverInterface $controller_resolver, Request $request, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler) { + public function __construct(\Traversable $namespaces, ControllerResolverInterface $controller_resolver, Request $request, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManager $language_manager) { parent::__construct('Menu\LocalTask', $namespaces, array(), 'Drupal\Core\Annotation\Menu\LocalTask'); $this->controllerResolver = $controller_resolver; $this->request = $request; $this->routeProvider = $route_provider; $this->alterInfo($module_handler, 'local_tasks'); + $this->setCacheBackend($cache, $language_manager, 'local_task'); } /** @@ -118,15 +127,21 @@ public function getPath(LocalTaskInterface $local_task) { public function getLocalTasksForRoute($route_name) { if (!isset($this->instances[$route_name])) { $this->instances[$route_name] = array(); - // @todo - optimize this lookup by compiling or caching. + if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $route_name)) { + $tab_root_ids = $cache->data['tab_root_ids']; + $parents = $cache->data['parents']; + $children = $cache->data['children']; + } + else { $definitions = $this->getDefinitions(); // We build the hierarchy by finding all tabs that should // appear on the current route. $tab_root_ids = array(); $parents = array(); + $children = array(); foreach ($definitions as $plugin_id => $task_info) { if ($route_name == $task_info['route_name']) { - $tab_root_ids[$task_info['tab_root_id']] = TRUE; + $tab_root_ids[$task_info['tab_root_id']] = $task_info['tab_root_id']; // Tabs that link to the current route are viable parents // and their parent and children should be visible also. // @todo - this only works for 2 levels of tabs. @@ -140,7 +155,6 @@ public function getLocalTasksForRoute($route_name) { if ($tab_root_ids) { // Find all the plugins with the same root and that are at the top // level or that have a visible parent. - $children = array(); foreach ($definitions as $plugin_id => $task_info) { if (!empty($tab_root_ids[$task_info['tab_root_id']]) && (empty($task_info['tab_parent_id']) || !empty($parents[$task_info['tab_parent_id']]))) { // Concat '> ' with root ID for the parent of top-level tabs. @@ -148,7 +162,16 @@ public function getLocalTasksForRoute($route_name) { $children[$parent][$plugin_id] = $task_info; } } - foreach (array_keys($tab_root_ids) as $root_id) { + } + $data = array( + 'tab_root_ids' => $tab_root_ids, + 'parents' => $parents, + 'children' => $children, + ); + $this->cacheBackend->set($this->cacheKey . ':' . $route_name, $data, CacheBackendInterface::CACHE_PERMANENT, array('local_task')); + } + // Create a plugin instance for each element of the hierarchy. + foreach ($tab_root_ids as $root_id) { // Convert the tree keyed by plugin IDs into a simple one with // integer depth. Create instances for each plugin along the way. $level = 0; @@ -175,7 +198,7 @@ public function getLocalTasksForRoute($route_name) { $level++; } while ($next_parent); } - } + } return $this->instances[$route_name]; } diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalTaskManagerTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalTaskManagerTest.php index 5d61a9c..c4dd3d8 100644 --- a/core/tests/Drupal/Tests/Core/Menu/LocalTaskManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalTaskManagerTest.php @@ -8,6 +8,8 @@ namespace Drupal\Tests\Core\Menu; use Drupal\Component\Plugin\Factory\DefaultFactory; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Language\Language; use Drupal\system\Plugin\Type\MenuLocalTaskManager; use Drupal\Tests\UnitTestCase; use Symfony\Component\HttpFoundation\Request; @@ -23,7 +25,7 @@ class LocalTaskManagerTest extends UnitTestCase { /** * The tested manager. * - * @var \Drupal\system\Plugin\Type\MenuLocalTaskManager + * @var \Drupal\Core\Menu\LocalTaskManager */ protected $manager; @@ -62,6 +64,13 @@ class LocalTaskManagerTest extends UnitTestCase { */ protected $factory; + /** + * The cache backend used in the test. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $cacheBackend; + public static function getInfo() { return array( 'name' => 'Local tasks manager.', @@ -81,6 +90,7 @@ protected function setUp() { $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); $this->pluginDiscovery = $this->getMock('Drupal\Component\Plugin\Discovery\DiscoveryInterface'); $this->factory = $this->getMock('Drupal\Component\Plugin\Factory\FactoryInterface'); + $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); $this->setupLocalTaskManager(); } @@ -91,53 +101,84 @@ protected function setUp() { * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getLocalTasksForRoute() */ public function testGetLocalTasksForRouteSingleLevelTitle() { - $definitions = array(); - $definitions['menu_local_task_test_tasks_settings'] = array( - 'id' => 'menu_local_task_test_tasks_settings', - 'route_name' => 'menu_local_task_test_tasks_settings', - 'title' => 'Settings', - 'tab_root_id' => 'menu_local_task_test_tasks_view', - 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksSettings', - ); - $definitions['menu_local_task_test_tasks_edit'] = array( - 'id' => 'menu_local_task_test_tasks_edit', - 'route_name' => 'menu_local_task_test_tasks_edit', - 'title' => 'Settings', - 'tab_root_id' => 'menu_local_task_test_tasks_view', - 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksEdit', - 'weight' => 20, - ); - $definitions['menu_local_task_test_tasks_view'] = array( - 'id' => 'menu_local_task_test_tasks_view', - 'route_name' => 'menu_local_task_test_tasks_view', - 'title' => 'Settings', - 'tab_root_id' => 'menu_local_task_test_tasks_view', - 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksView', - ); + $definitions = $this->getLocalTaskFixtures(); - $this->pluginDiscovery->expects($this->any()) + $this->pluginDiscovery->expects($this->once()) ->method('getDefinitions') ->will($this->returnValue($definitions)); $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface'); - $map = array( - array('menu_local_task_test_tasks_settings', array(), $mock_plugin), - array('menu_local_task_test_tasks_edit', array(), $mock_plugin), - array('menu_local_task_test_tasks_view', array(), $mock_plugin), - ); - $this->factory->expects($this->any()) - ->method('createInstance') - ->will($this->returnValueMap($map)); + $this->setupFactory($mock_plugin); + $this->setupLocalTaskManager(); + + $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view'); + $result = $this->getLocalTasksForRouteResult($mock_plugin); + + $this->assertEquals($result, $local_tasks); + } + + /** + * Tests the cache of the local task manager with an empty initial cache. + */ + public function testGetLocalTaskForRouteWithEmptyCache() { + $definitions = $this->getLocalTaskFixtures(); + + $this->pluginDiscovery->expects($this->once()) + ->method('getDefinitions') + ->will($this->returnValue($definitions)); + + $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface'); + $this->setupFactory($mock_plugin); $this->setupLocalTaskManager(); + $result = $this->getLocalTasksForRouteResult($mock_plugin); + + $this->cacheBackend->expects($this->at(0)) + ->method('get') + ->with('local_task:en:menu_local_task_test_tasks_view'); + + $this->cacheBackend->expects($this->at(1)) + ->method('get') + ->with('local_task:en'); + + $this->cacheBackend->expects($this->at(2)) + ->method('set') + ->with('local_task:en', $definitions, CacheBackendInterface::CACHE_PERMANENT); + + $this->cacheBackend->expects($this->at(3)) + ->method('set') + ->with('local_task:en:menu_local_task_test_tasks_view', $result, CacheBackendInterface::CACHE_PERMANENT, array('local_task')); + $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view'); - $this->assertEquals(array(0 => array( - 'menu_local_task_test_tasks_settings' => $mock_plugin, - 'menu_local_task_test_tasks_view' => $mock_plugin, - 'menu_local_task_test_tasks_edit' => $mock_plugin, - )), $local_tasks); + $this->assertEquals($result, $local_tasks); + } + + /** + * Tests the cache of the local task manager with a filled initial cache. + */ + public function testGetLocalTaskForRouteWithFilledCache() { + $this->pluginDiscovery->expects($this->never()) + ->method('getDefinitions'); + + $mock_plugin = $this->getMock('Drupal\Core\Menu\LocalTaskInterface'); + $this->setupFactory($mock_plugin); + + $this->setupLocalTaskManager(); + + $result = $this->getLocalTasksForRouteResult($mock_plugin); + + $this->cacheBackend->expects($this->at(0)) + ->method('get') + ->with('local_task:en:menu_local_task_test_tasks_view') + ->will($this->returnValue((object) array('data' => $result))); + + $this->cacheBackend->expects($this->never()) + ->method('set'); + + $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view'); + $this->assertEquals($result, $local_tasks); } /** @@ -201,6 +242,85 @@ protected function setupLocalTaskManager() { $property = new \ReflectionProperty('Drupal\Core\Menu\LocalTaskManager', 'factory'); $property->setAccessible(TRUE); $property->setValue($this->manager, $this->factory); + + $language_manager = $this->getMockBuilder('Drupal\Core\Language\LanguageManager') + ->disableOriginalConstructor() + ->getMock(); + $language_manager->expects($this->any()) + ->method('getLanguage') + ->will($this->returnValue(new Language(array('id' => 'en')))); + + $this->manager->setCacheBackend($this->cacheBackend, $language_manager, 'local_task'); + } + + /** + * Return some local tasks plugin definitions. + * + * @return array + * An array of plugin definition keyed by plugin ID. + */ + protected function getLocalTaskFixtures() { + $definitions = array(); + $definitions['menu_local_task_test_tasks_settings'] = array( + 'id' => 'menu_local_task_test_tasks_settings', + 'route_name' => 'menu_local_task_test_tasks_settings', + 'title' => 'Settings', + 'tab_root_id' => 'menu_local_task_test_tasks_view', + 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksSettings', + ); + $definitions['menu_local_task_test_tasks_edit'] = array( + 'id' => 'menu_local_task_test_tasks_edit', + 'route_name' => 'menu_local_task_test_tasks_edit', + 'title' => 'Settings', + 'tab_root_id' => 'menu_local_task_test_tasks_view', + 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksEdit', + 'weight' => 20, + ); + $definitions['menu_local_task_test_tasks_view'] = array( + 'id' => 'menu_local_task_test_tasks_view', + 'route_name' => 'menu_local_task_test_tasks_view', + 'title' => 'Settings', + 'tab_root_id' => 'menu_local_task_test_tasks_view', + 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksView', + ); + return $definitions; + } + + /** + * Setups the plugin factory with some local task plugins. + * + * @param \PHPUnit_Framework_MockObject_MockObject $mock_plugin + * The mock plugin. + */ + protected function setupFactory($mock_plugin) { + $map = array( + array('menu_local_task_test_tasks_settings', array(), $mock_plugin), + array('menu_local_task_test_tasks_edit', array(), $mock_plugin), + array('menu_local_task_test_tasks_view', array(), $mock_plugin), + ); + $this->factory->expects($this->any()) + ->method('createInstance') + ->will($this->returnValueMap($map)); + } + + /** + * Returns an expected result for getLocalTasksForRoute. + * + * @param \PHPUnit_Framework_MockObject_MockObject $mock_plugin + * The mock plugin. + * + * @return array + * The expected result, keyed by local task leve. + */ + protected function getLocalTasksForRouteResult($mock_plugin) { + $result = array( + 0 => array( + 'menu_local_task_test_tasks_settings' => $mock_plugin, + 'menu_local_task_test_tasks_view' => $mock_plugin, + 'menu_local_task_test_tasks_edit' => $mock_plugin, + ) + ); + return $result; } }