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