You are here

class LocalTaskManager in Drupal 10

Same name and namespace in other branches
  1. 8 core/lib/Drupal/Core/Menu/LocalTaskManager.php \Drupal\Core\Menu\LocalTaskManager
  2. 9 core/lib/Drupal/Core/Menu/LocalTaskManager.php \Drupal\Core\Menu\LocalTaskManager

Provides the default local task manager using YML as primary definition.

Hierarchy

Expanded class hierarchy of LocalTaskManager

1 string reference to 'LocalTaskManager'
core.services.yml in core/core.services.yml
core/core.services.yml
1 service uses LocalTaskManager
plugin.manager.menu.local_task in core/core.services.yml
Drupal\Core\Menu\LocalTaskManager

File

core/lib/Drupal/Core/Menu/LocalTaskManager.php, line 27

Namespace

Drupal\Core\Menu
View source
class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerInterface {

  /**
   * {@inheritdoc}
   */
  protected $defaults = [
    // (required) The name of the route this task links to.
    'route_name' => '',
    // Parameters for route variables when generating a link.
    'route_parameters' => [],
    // 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' => [],
    // 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' => '',
  ];

  /**
   * An argument resolver object.
   *
   * @var \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface
   */
  protected $argumentResolver;

  /**
   * 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 = [];

  /**
   * 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 \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface $argument_resolver
   *   An object to use in resolving route arguments.
   * @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(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) {
    $this->factory = new ContainerFactory($this, '\\Drupal\\Core\\Menu\\LocalTaskInterface');
    $this->argumentResolver = $argument_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(), [
      '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 = [
      $local_task,
      'getTitle',
    ];
    $request = $this->requestStack
      ->getCurrentRequest();
    $arguments = $this->argumentResolver
      ->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++ * 1.0E-6;
      }
    }
    return $definitions;
  }

  /**
   * {@inheritdoc}
   */
  public function getLocalTasksForRoute($route_name) {
    if (!isset($this->instances[$route_name])) {
      $this->instances[$route_name] = [];
      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 = [];
        $parents = [];
        $children = [];
        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 = [
          '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 = [];

    // Collect all route names.
    $route_names = [];
    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 \Drupal\Core\Menu\LocalTaskInterface[] $instances */
      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
   *   The parameter for the route.
   *
   * @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;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DefaultPluginManager::$additionalAnnotationNamespaces protected property Additional namespaces the annotation discovery mechanism should scan for annotation definitions.
DefaultPluginManager::$alterHook protected property Name of the alter hook if one should be invoked.
DefaultPluginManager::$cacheKey protected property The cache key.
DefaultPluginManager::$cacheTags protected property An array of cache tags to use for the cached definitions.
DefaultPluginManager::$moduleHandler protected property The module handler to invoke the alter hook. 1
DefaultPluginManager::$namespaces protected property An object that implements \Traversable which contains the root paths keyed by the corresponding namespace to look for plugin implementations.
DefaultPluginManager::$pluginDefinitionAnnotationName protected property The name of the annotation that contains the plugin definition.
DefaultPluginManager::$pluginInterface protected property The interface each plugin should implement. 1
DefaultPluginManager::$subdir protected property The subdirectory within a namespace to look for plugins, or FALSE if the plugins are in the top level of the namespace.
DefaultPluginManager::alterDefinitions protected function Invokes the hook to alter the definitions if the alter hook is set. 1
DefaultPluginManager::alterInfo protected function Sets the alter hook name.
DefaultPluginManager::clearCachedDefinitions public function Clears static and persistent plugin definition caches. Overrides CachedDiscoveryInterface::clearCachedDefinitions 6
DefaultPluginManager::extractProviderFromDefinition protected function Extracts the provider from a plugin definition.
DefaultPluginManager::findDefinitions protected function Finds plugin definitions. 7
DefaultPluginManager::getCacheContexts public function The cache contexts associated with this object. Overrides CacheableDependencyInterface::getCacheContexts
DefaultPluginManager::getCachedDefinitions protected function Returns the cached plugin definitions of the decorated discovery class.
DefaultPluginManager::getCacheMaxAge public function The maximum age for which this object may be cached. Overrides CacheableDependencyInterface::getCacheMaxAge
DefaultPluginManager::getCacheTags public function The cache tags associated with this object. Overrides CacheableDependencyInterface::getCacheTags
DefaultPluginManager::getFactory protected function Gets the plugin factory. Overrides PluginManagerBase::getFactory
DefaultPluginManager::providerExists protected function Determines if the provider of a definition exists. 3
DefaultPluginManager::setCacheBackend public function Initialize the cache backend.
DefaultPluginManager::setCachedDefinitions protected function Sets a cache of plugin definitions for the decorated discovery class.
DefaultPluginManager::useCaches public function Disable the use of caches. Overrides CachedDiscoveryInterface::useCaches 1
DiscoveryCachedTrait::$definitions protected property Cached definitions array. 1
DiscoveryCachedTrait::getDefinition public function Overrides DiscoveryTrait::getDefinition 3
DiscoveryTrait::doGetDefinition protected function Gets a specific plugin definition.
DiscoveryTrait::hasDefinition public function
LocalTaskManager::$accessManager protected property The access manager.
LocalTaskManager::$account protected property The current user.
LocalTaskManager::$argumentResolver protected property An argument resolver object.
LocalTaskManager::$defaults protected property A set of defaults to be referenced by $this->processDefinition() if additional processing of plugins is necessary or helpful for development purposes. Overrides DefaultPluginManager::$defaults
LocalTaskManager::$instances protected property The plugin instances.
LocalTaskManager::$requestStack protected property The request stack.
LocalTaskManager::$routeMatch protected property The current route match.
LocalTaskManager::$routeProvider protected property The route provider to load routes by name.
LocalTaskManager::$taskData protected property The local task render arrays for the current route.
LocalTaskManager::getDefinitions public function Gets the definition of all plugins for this type. Overrides DefaultPluginManager::getDefinitions
LocalTaskManager::getDiscovery protected function Gets the plugin discovery. Overrides DefaultPluginManager::getDiscovery
LocalTaskManager::getLocalTasks public function Renders the local tasks (tabs) for the given route. Overrides LocalTaskManagerInterface::getLocalTasks
LocalTaskManager::getLocalTasksForRoute public function Find all local tasks that appear on a named route. Overrides LocalTaskManagerInterface::getLocalTasksForRoute
LocalTaskManager::getTasksBuild public function Gets the render array for all local tasks. Overrides LocalTaskManagerInterface::getTasksBuild
LocalTaskManager::getTitle public function Gets the title for a local task. Overrides LocalTaskManagerInterface::getTitle
LocalTaskManager::isRouteActive protected function Determines whether the route of a certain local task is currently active.
LocalTaskManager::processDefinition public function Performs extra processing on plugin definitions. Overrides DefaultPluginManager::processDefinition
LocalTaskManager::__construct public function Constructs a \Drupal\Core\Menu\LocalTaskManager object. Overrides DefaultPluginManager::__construct
PluginManagerBase::$discovery protected property The object that discovers plugins managed by this manager.
PluginManagerBase::$factory protected property The object that instantiates plugins managed by this manager.
PluginManagerBase::$mapper protected property The object that returns the preconfigured plugin instance appropriate for a particular runtime condition.
PluginManagerBase::createInstance public function Creates a pre-configured instance of a plugin. Overrides FactoryInterface::createInstance 12
PluginManagerBase::getInstance public function Gets a preconfigured instance of a plugin. Overrides MapperInterface::getInstance 6
PluginManagerBase::handlePluginNotFound protected function Allows plugin managers to specify custom behavior if a plugin is not found. 1
UseCacheBackendTrait::$cacheBackend protected property Cache backend instance.
UseCacheBackendTrait::$useCaches protected property Flag whether caches should be used or skipped.
UseCacheBackendTrait::cacheGet protected function Fetches from the cache backend, respecting the use caches flag.
UseCacheBackendTrait::cacheSet protected function Stores data in the persistent cache, respecting the use caches flag.