diff core/lib/Drupal/Core/Menu/LocalTaskManager.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 129ea1e6d783
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/lib/Drupal/Core/Menu/LocalTaskManager.php	Wed Nov 29 16:09:58 2017 +0000
@@ -0,0 +1,413 @@
+<?php
+
+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 = [
+    // (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' => '',
+  ];
+
+  /**
+   * 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 = [];
+
+  /**
+   * 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(), ['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->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] = [];
+      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 $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;
+  }
+
+}