Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
55.56% |
5 / 9 |
CRAP | |
75.78% |
97 / 128 |
| LocalTaskManager | |
0.00% |
0 / 1 |
|
55.56% |
5 / 9 |
76.06 | |
75.78% |
97 / 128 |
| __construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
11 / 11 |
|||
| getDiscovery | |
0.00% |
0 / 1 |
2.86 | |
40.00% |
2 / 5 |
|||
| processDefinition | |
0.00% |
0 / 1 |
2.06 | |
75.00% |
3 / 4 |
|||
| getTitle | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
| getDefinitions | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
| getLocalTasksForRoute | |
100.00% |
1 / 1 |
21 | |
100.00% |
44 / 44 |
|||
| getTasksBuild | |
100.00% |
1 / 1 |
7 | |
100.00% |
27 / 27 |
|||
| getLocalTasks | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 21 |
|||
| isRouteActive | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 6 |
|||
| <?php | |
| /** | |
| * @file | |
| * Contains \Drupal\Core\Menu\LocalTaskManager. | |
| */ | |
| namespace Drupal\Core\Menu; | |
| use Drupal\Component\Plugin\Exception\PluginException; | |
| use Drupal\Core\Access\AccessManagerInterface; | |
| use Drupal\Core\Cache\Cache; | |
| use Drupal\Core\Cache\CacheableMetadata; | |
| use Drupal\Core\Cache\CacheBackendInterface; | |
| use Drupal\Core\Cache\RefinableCacheableDependencyInterface; | |
| use Drupal\Core\Controller\ControllerResolverInterface; | |
| use Drupal\Core\Extension\ModuleHandlerInterface; | |
| use Drupal\Core\Language\LanguageManagerInterface; | |
| use Drupal\Core\Plugin\DefaultPluginManager; | |
| use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator; | |
| use Drupal\Core\Plugin\Discovery\YamlDiscovery; | |
| use Drupal\Core\Plugin\Factory\ContainerFactory; | |
| use Drupal\Core\Routing\RouteMatchInterface; | |
| use Drupal\Core\Routing\RouteProviderInterface; | |
| use Drupal\Core\Session\AccountInterface; | |
| use Drupal\Core\Url; | |
| use Symfony\Component\HttpFoundation\RequestStack; | |
| /** | |
| * Provides the default local task manager using YML as primary definition. | |
| */ | |
| class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerInterface { | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| protected $defaults = array( | |
| // (required) The name of the route this task links to. | |
| 'route_name' => '', | |
| // Parameters for route variables when generating a link. | |
| 'route_parameters' => array(), | |
| // The static title for the local task. | |
| 'title' => '', | |
| // The route name where the root tab appears. | |
| 'base_route' => '', | |
| // The plugin ID of the parent tab (or NULL for the top-level tab). | |
| 'parent_id' => NULL, | |
| // The weight of the tab. | |
| 'weight' => NULL, | |
| // The default link options. | |
| 'options' => array(), | |
| // Default class for local task implementations. | |
| 'class' => 'Drupal\Core\Menu\LocalTaskDefault', | |
| // The plugin id. Set by the plugin system based on the top-level YAML key. | |
| 'id' => '', | |
| ); | |
| /** | |
| * A controller resolver object. | |
| * | |
| * @var \Drupal\Core\Controller\ControllerResolverInterface | |
| */ | |
| protected $controllerResolver; | |
| /** | |
| * The request stack. | |
| * | |
| * @var \Symfony\Component\HttpFoundation\RequestStack | |
| */ | |
| protected $requestStack; | |
| /** | |
| * The current route match. | |
| * | |
| * @var \Drupal\Core\Routing\RouteMatchInterface | |
| */ | |
| protected $routeMatch; | |
| /** | |
| * The plugin instances. | |
| * | |
| * @var array | |
| */ | |
| protected $instances = array(); | |
| /** | |
| * The local task render arrays for the current route. | |
| * | |
| * @var array | |
| */ | |
| protected $taskData; | |
| /** | |
| * The route provider to load routes by name. | |
| * | |
| * @var \Drupal\Core\Routing\RouteProviderInterface | |
| */ | |
| protected $routeProvider; | |
| /** | |
| * The access manager. | |
| * | |
| * @var \Drupal\Core\Access\AccessManagerInterface | |
| */ | |
| protected $accessManager; | |
| /** | |
| * The current user. | |
| * | |
| * @var \Drupal\Core\Session\AccountInterface | |
| */ | |
| protected $account; | |
| /** | |
| * Constructs a \Drupal\Core\Menu\LocalTaskManager object. | |
| * | |
| * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver | |
| * An object to use in introspecting route methods. | |
| * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack | |
| * The request object to use for building titles and paths for plugin instances. | |
| * @param \Drupal\Core\Routing\RouteMatchInterface $route_match | |
| * The current route match. | |
| * @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. | |
| * @param \Drupal\Core\Cache\CacheBackendInterface $cache | |
| * The cache backend. | |
| * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager | |
| * The language manager. | |
| * @param \Drupal\Core\Access\AccessManagerInterface $access_manager | |
| * The access manager. | |
| * @param \Drupal\Core\Session\AccountInterface $account | |
| * The current user. | |
| */ | |
| public function __construct(ControllerResolverInterface $controller_resolver, RequestStack $request_stack, RouteMatchInterface $route_match, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, AccessManagerInterface $access_manager, AccountInterface $account) { | |
| $this->factory = new ContainerFactory($this, '\Drupal\Core\Menu\LocalTaskInterface'); | |
| $this->controllerResolver = $controller_resolver; | |
| $this->requestStack = $request_stack; | |
| $this->routeMatch = $route_match; | |
| $this->routeProvider = $route_provider; | |
| $this->accessManager = $access_manager; | |
| $this->account = $account; | |
| $this->moduleHandler = $module_handler; | |
| $this->alterInfo('local_tasks'); | |
| $this->setCacheBackend($cache, 'local_task_plugins:' . $language_manager->getCurrentLanguage()->getId(), array('local_task')); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| protected function getDiscovery() { | |
| if (!isset($this->discovery)) { | |
| $yaml_discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories()); | |
| $yaml_discovery->addTranslatableProperty('title', 'title_context'); | |
| $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery); | |
| } | |
| return $this->discovery; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function processDefinition(&$definition, $plugin_id) { | |
| parent::processDefinition($definition, $plugin_id); | |
| // If there is no route name, this is a broken definition. | |
| if (empty($definition['route_name'])) { | |
| throw new PluginException(sprintf('Plugin (%s) definition must include "route_name"', $plugin_id)); | |
| } | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getTitle(LocalTaskInterface $local_task) { | |
| $controller = array($local_task, 'getTitle'); | |
| $request = $this->requestStack->getCurrentRequest(); | |
| $arguments = $this->controllerResolver->getArguments($request, $controller); | |
| return call_user_func_array($controller, $arguments); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getDefinitions() { | |
| $definitions = parent::getDefinitions(); | |
| $count = 0; | |
| foreach ($definitions as &$definition) { | |
| if (isset($definition['weight'])) { | |
| // Add some micro weight. | |
| $definition['weight'] += ($count++) * 1e-6; | |
| } | |
| } | |
| return $definitions; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getLocalTasksForRoute($route_name) { | |
| if (!isset($this->instances[$route_name])) { | |
| $this->instances[$route_name] = array(); | |
| if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $route_name)) { | |
| $base_routes = $cache->data['base_routes']; | |
| $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. | |
| $base_routes = array(); | |
| $parents = array(); | |
| $children = array(); | |
| foreach ($definitions as $plugin_id => $task_info) { | |
| // Fill in the base_route from the parent to insure consistency. | |
| if (!empty($task_info['parent_id']) && !empty($definitions[$task_info['parent_id']])) { | |
| $task_info['base_route'] = $definitions[$task_info['parent_id']]['base_route']; | |
| // Populate the definitions we use in the next loop. Using a | |
| // reference like &$task_info causes bugs. | |
| $definitions[$plugin_id]['base_route'] = $definitions[$task_info['parent_id']]['base_route']; | |
| } | |
| if ($route_name == $task_info['route_name']) { | |
| if(!empty($task_info['base_route'])) { | |
| $base_routes[$task_info['base_route']] = $task_info['base_route']; | |
| } | |
| // 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. | |
| // instead need to iterate up. | |
| $parents[$plugin_id] = TRUE; | |
| if (!empty($task_info['parent_id'])) { | |
| $parents[$task_info['parent_id']] = TRUE; | |
| } | |
| } | |
| } | |
| if ($base_routes) { | |
| // Find all the plugins with the same root and that are at the top | |
| // level or that have a visible parent. | |
| foreach ($definitions as $plugin_id => $task_info) { | |
| if (!empty($base_routes[$task_info['base_route']]) && (empty($task_info['parent_id']) || !empty($parents[$task_info['parent_id']]))) { | |
| // Concat '> ' with root ID for the parent of top-level tabs. | |
| $parent = empty($task_info['parent_id']) ? '> ' . $task_info['base_route'] : $task_info['parent_id']; | |
| $children[$parent][$plugin_id] = $task_info; | |
| } | |
| } | |
| } | |
| $data = array( | |
| 'base_routes' => $base_routes, | |
| 'parents' => $parents, | |
| 'children' => $children, | |
| ); | |
| $this->cacheBackend->set($this->cacheKey . ':' . $route_name, $data, Cache::PERMANENT, $this->cacheTags); | |
| } | |
| // Create a plugin instance for each element of the hierarchy. | |
| foreach ($base_routes as $base_route) { | |
| // Convert the tree keyed by plugin IDs into a simple one with | |
| // integer depth. Create instances for each plugin along the way. | |
| $level = 0; | |
| // We used this above as the top-level parent array key. | |
| $next_parent = '> ' . $base_route; | |
| do { | |
| $parent = $next_parent; | |
| $next_parent = FALSE; | |
| foreach ($children[$parent] as $plugin_id => $task_info) { | |
| $plugin = $this->createInstance($plugin_id); | |
| $this->instances[$route_name][$level][$plugin_id] = $plugin; | |
| // Normally, the link generator compares the href of every link with | |
| // the current path and sets the active class accordingly. But the | |
| // parents of the current local task may be on a different route in | |
| // which case we have to set the class manually by flagging it | |
| // active. | |
| if (!empty($parents[$plugin_id]) && $route_name != $task_info['route_name']) { | |
| $plugin->setActive(); | |
| } | |
| if (isset($children[$plugin_id])) { | |
| // This tab has visible children. | |
| $next_parent = $plugin_id; | |
| } | |
| } | |
| $level++; | |
| } while ($next_parent); | |
| } | |
| } | |
| return $this->instances[$route_name]; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getTasksBuild($current_route_name, RefinableCacheableDependencyInterface &$cacheability) { | |
| $tree = $this->getLocalTasksForRoute($current_route_name); | |
| $build = array(); | |
| // Collect all route names. | |
| $route_names = array(); | |
| foreach ($tree as $instances) { | |
| foreach ($instances as $child) { | |
| $route_names[] = $child->getRouteName(); | |
| } | |
| } | |
| // Pre-fetch all routes involved in the tree. This reduces the number | |
| // of SQL queries that would otherwise be triggered by the access manager. | |
| if ($route_names) { | |
| $this->routeProvider->getRoutesByNames($route_names); | |
| } | |
| foreach ($tree as $level => $instances) { | |
| /** @var $instances \Drupal\Core\Menu\LocalTaskInterface[] */ | |
| foreach ($instances as $plugin_id => $child) { | |
| $route_name = $child->getRouteName(); | |
| $route_parameters = $child->getRouteParameters($this->routeMatch); | |
| // Given that the active flag depends on the route we have to add the | |
| // route cache context. | |
| $cacheability->addCacheContexts(['route']); | |
| $active = $this->isRouteActive($current_route_name, $route_name, $route_parameters); | |
| // The plugin may have been set active in getLocalTasksForRoute() if | |
| // one of its child tabs is the active tab. | |
| $active = $active || $child->getActive(); | |
| // @todo It might make sense to use link render elements instead. | |
| $link = [ | |
| 'title' => $this->getTitle($child), | |
| 'url' => Url::fromRoute($route_name, $route_parameters), | |
| 'localized_options' => $child->getOptions($this->routeMatch), | |
| ]; | |
| $access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account, TRUE); | |
| $build[$level][$plugin_id] = [ | |
| '#theme' => 'menu_local_task', | |
| '#link' => $link, | |
| '#active' => $active, | |
| '#weight' => $child->getWeight(), | |
| '#access' => $access, | |
| ]; | |
| $cacheability->addCacheableDependency($access)->addCacheableDependency($child); | |
| } | |
| } | |
| return $build; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getLocalTasks($route_name, $level = 0) { | |
| if (!isset($this->taskData[$route_name])) { | |
| $cacheability = new CacheableMetadata(); | |
| $cacheability->addCacheContexts(['route']); | |
| // Look for route-based tabs. | |
| $this->taskData[$route_name] = [ | |
| 'tabs' => [], | |
| 'cacheability' => $cacheability, | |
| ]; | |
| if (!$this->requestStack->getCurrentRequest()->attributes->has('exception')) { | |
| // Safe to build tasks only when no exceptions raised. | |
| $data = []; | |
| $local_tasks = $this->getTasksBuild($route_name, $cacheability); | |
| foreach ($local_tasks as $tab_level => $items) { | |
| $data[$tab_level] = empty($data[$tab_level]) ? $items : array_merge($data[$tab_level], $items); | |
| } | |
| $this->taskData[$route_name]['tabs'] = $data; | |
| // Allow modules to alter local tasks. | |
| $this->moduleHandler->alter('menu_local_tasks', $this->taskData[$route_name], $route_name, $cacheability); | |
| $this->taskData[$route_name]['cacheability'] = $cacheability; | |
| } | |
| } | |
| if (isset($this->taskData[$route_name]['tabs'][$level])) { | |
| return [ | |
| 'tabs' => $this->taskData[$route_name]['tabs'][$level], | |
| 'route_name' => $route_name, | |
| 'cacheability' => $this->taskData[$route_name]['cacheability'], | |
| ]; | |
| } | |
| return [ | |
| 'tabs' => [], | |
| 'route_name' => $route_name, | |
| 'cacheability' => $this->taskData[$route_name]['cacheability'], | |
| ]; | |
| } | |
| /** | |
| * Determines whether the route of a certain local task is currently active. | |
| * | |
| * @param string $current_route_name | |
| * The route name of the current main request. | |
| * @param string $route_name | |
| * The route name of the local task to determine the active status. | |
| * @param array $route_parameters | |
| * | |
| * @return bool | |
| * Returns TRUE if the passed route_name and route_parameters is considered | |
| * as the same as the one from the request, otherwise FALSE. | |
| */ | |
| protected function isRouteActive($current_route_name, $route_name, $route_parameters) { | |
| // Flag the list element as active if this tab's route and parameters match | |
| // the current request's route and route variables. | |
| $active = $current_route_name == $route_name; | |
| if ($active) { | |
| // The request is injected, so we need to verify that we have the expected | |
| // _raw_variables attribute. | |
| $raw_variables_bag = $this->routeMatch->getRawParameters(); | |
| // If we don't have _raw_variables, we assume the attributes are still the | |
| // original values. | |
| $raw_variables = $raw_variables_bag ? $raw_variables_bag->all() : $this->routeMatch->getParameters()->all(); | |
| $active = array_intersect_assoc($route_parameters, $raw_variables) == $route_parameters; | |
| } | |
| return $active; | |
| } | |
| } |