annotate core/lib/Drupal/Core/Menu/LocalTaskManager.php @ 16:c2387f117808

Routine composer update
author Chris Cannam
date Tue, 10 Jul 2018 15:07:59 +0100
parents 4c8ae668cc8c
children 129ea1e6d783
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\Menu;
Chris@0 4
Chris@0 5 use Drupal\Component\Plugin\Exception\PluginException;
Chris@0 6 use Drupal\Core\Access\AccessManagerInterface;
Chris@0 7 use Drupal\Core\Cache\Cache;
Chris@0 8 use Drupal\Core\Cache\CacheableMetadata;
Chris@0 9 use Drupal\Core\Cache\CacheBackendInterface;
Chris@0 10 use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
Chris@0 11 use Drupal\Core\Controller\ControllerResolverInterface;
Chris@0 12 use Drupal\Core\Extension\ModuleHandlerInterface;
Chris@0 13 use Drupal\Core\Language\LanguageManagerInterface;
Chris@0 14 use Drupal\Core\Plugin\DefaultPluginManager;
Chris@0 15 use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
Chris@0 16 use Drupal\Core\Plugin\Discovery\YamlDiscovery;
Chris@0 17 use Drupal\Core\Plugin\Factory\ContainerFactory;
Chris@0 18 use Drupal\Core\Routing\RouteMatchInterface;
Chris@0 19 use Drupal\Core\Routing\RouteProviderInterface;
Chris@0 20 use Drupal\Core\Session\AccountInterface;
Chris@0 21 use Drupal\Core\Url;
Chris@0 22 use Symfony\Component\HttpFoundation\RequestStack;
Chris@0 23
Chris@0 24 /**
Chris@0 25 * Provides the default local task manager using YML as primary definition.
Chris@0 26 */
Chris@0 27 class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerInterface {
Chris@0 28
Chris@0 29 /**
Chris@0 30 * {@inheritdoc}
Chris@0 31 */
Chris@0 32 protected $defaults = [
Chris@0 33 // (required) The name of the route this task links to.
Chris@0 34 'route_name' => '',
Chris@0 35 // Parameters for route variables when generating a link.
Chris@0 36 'route_parameters' => [],
Chris@0 37 // The static title for the local task.
Chris@0 38 'title' => '',
Chris@0 39 // The route name where the root tab appears.
Chris@0 40 'base_route' => '',
Chris@0 41 // The plugin ID of the parent tab (or NULL for the top-level tab).
Chris@0 42 'parent_id' => NULL,
Chris@0 43 // The weight of the tab.
Chris@0 44 'weight' => NULL,
Chris@0 45 // The default link options.
Chris@0 46 'options' => [],
Chris@0 47 // Default class for local task implementations.
Chris@0 48 'class' => 'Drupal\Core\Menu\LocalTaskDefault',
Chris@0 49 // The plugin id. Set by the plugin system based on the top-level YAML key.
Chris@0 50 'id' => '',
Chris@0 51 ];
Chris@0 52
Chris@0 53 /**
Chris@0 54 * A controller resolver object.
Chris@0 55 *
Chris@0 56 * @var \Drupal\Core\Controller\ControllerResolverInterface
Chris@0 57 */
Chris@0 58 protected $controllerResolver;
Chris@0 59
Chris@0 60 /**
Chris@0 61 * The request stack.
Chris@0 62 *
Chris@0 63 * @var \Symfony\Component\HttpFoundation\RequestStack
Chris@0 64 */
Chris@0 65 protected $requestStack;
Chris@0 66
Chris@0 67 /**
Chris@0 68 * The current route match.
Chris@0 69 *
Chris@0 70 * @var \Drupal\Core\Routing\RouteMatchInterface
Chris@0 71 */
Chris@0 72 protected $routeMatch;
Chris@0 73
Chris@0 74 /**
Chris@0 75 * The plugin instances.
Chris@0 76 *
Chris@0 77 * @var array
Chris@0 78 */
Chris@0 79 protected $instances = [];
Chris@0 80
Chris@0 81 /**
Chris@0 82 * The local task render arrays for the current route.
Chris@0 83 *
Chris@0 84 * @var array
Chris@0 85 */
Chris@0 86 protected $taskData;
Chris@0 87
Chris@0 88 /**
Chris@0 89 * The route provider to load routes by name.
Chris@0 90 *
Chris@0 91 * @var \Drupal\Core\Routing\RouteProviderInterface
Chris@0 92 */
Chris@0 93 protected $routeProvider;
Chris@0 94
Chris@0 95 /**
Chris@0 96 * The access manager.
Chris@0 97 *
Chris@0 98 * @var \Drupal\Core\Access\AccessManagerInterface
Chris@0 99 */
Chris@0 100 protected $accessManager;
Chris@0 101
Chris@0 102 /**
Chris@0 103 * The current user.
Chris@0 104 *
Chris@0 105 * @var \Drupal\Core\Session\AccountInterface
Chris@0 106 */
Chris@0 107 protected $account;
Chris@0 108
Chris@0 109 /**
Chris@0 110 * Constructs a \Drupal\Core\Menu\LocalTaskManager object.
Chris@0 111 *
Chris@0 112 * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
Chris@0 113 * An object to use in introspecting route methods.
Chris@0 114 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
Chris@0 115 * The request object to use for building titles and paths for plugin instances.
Chris@0 116 * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
Chris@0 117 * The current route match.
Chris@0 118 * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
Chris@0 119 * The route provider to load routes by name.
Chris@0 120 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
Chris@0 121 * The module handler.
Chris@0 122 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
Chris@0 123 * The cache backend.
Chris@0 124 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
Chris@0 125 * The language manager.
Chris@0 126 * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
Chris@0 127 * The access manager.
Chris@0 128 * @param \Drupal\Core\Session\AccountInterface $account
Chris@0 129 * The current user.
Chris@0 130 */
Chris@0 131 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) {
Chris@0 132 $this->factory = new ContainerFactory($this, '\Drupal\Core\Menu\LocalTaskInterface');
Chris@0 133 $this->controllerResolver = $controller_resolver;
Chris@0 134 $this->requestStack = $request_stack;
Chris@0 135 $this->routeMatch = $route_match;
Chris@0 136 $this->routeProvider = $route_provider;
Chris@0 137 $this->accessManager = $access_manager;
Chris@0 138 $this->account = $account;
Chris@0 139 $this->moduleHandler = $module_handler;
Chris@0 140 $this->alterInfo('local_tasks');
Chris@0 141 $this->setCacheBackend($cache, 'local_task_plugins:' . $language_manager->getCurrentLanguage()->getId(), ['local_task']);
Chris@0 142 }
Chris@0 143
Chris@0 144 /**
Chris@0 145 * {@inheritdoc}
Chris@0 146 */
Chris@0 147 protected function getDiscovery() {
Chris@0 148 if (!isset($this->discovery)) {
Chris@0 149 $yaml_discovery = new YamlDiscovery('links.task', $this->moduleHandler->getModuleDirectories());
Chris@0 150 $yaml_discovery->addTranslatableProperty('title', 'title_context');
Chris@0 151 $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
Chris@0 152 }
Chris@0 153 return $this->discovery;
Chris@0 154 }
Chris@0 155
Chris@0 156 /**
Chris@0 157 * {@inheritdoc}
Chris@0 158 */
Chris@0 159 public function processDefinition(&$definition, $plugin_id) {
Chris@0 160 parent::processDefinition($definition, $plugin_id);
Chris@0 161 // If there is no route name, this is a broken definition.
Chris@0 162 if (empty($definition['route_name'])) {
Chris@0 163 throw new PluginException(sprintf('Plugin (%s) definition must include "route_name"', $plugin_id));
Chris@0 164 }
Chris@0 165 }
Chris@0 166
Chris@0 167 /**
Chris@0 168 * {@inheritdoc}
Chris@0 169 */
Chris@0 170 public function getTitle(LocalTaskInterface $local_task) {
Chris@0 171 $controller = [$local_task, 'getTitle'];
Chris@0 172 $request = $this->requestStack->getCurrentRequest();
Chris@0 173 $arguments = $this->controllerResolver->getArguments($request, $controller);
Chris@0 174 return call_user_func_array($controller, $arguments);
Chris@0 175 }
Chris@0 176
Chris@0 177 /**
Chris@0 178 * {@inheritdoc}
Chris@0 179 */
Chris@0 180 public function getDefinitions() {
Chris@0 181 $definitions = parent::getDefinitions();
Chris@0 182
Chris@0 183 $count = 0;
Chris@0 184 foreach ($definitions as &$definition) {
Chris@0 185 if (isset($definition['weight'])) {
Chris@0 186 // Add some micro weight.
Chris@0 187 $definition['weight'] += ($count++) * 1e-6;
Chris@0 188 }
Chris@0 189 }
Chris@0 190
Chris@0 191 return $definitions;
Chris@0 192 }
Chris@0 193
Chris@0 194 /**
Chris@0 195 * {@inheritdoc}
Chris@0 196 */
Chris@0 197 public function getLocalTasksForRoute($route_name) {
Chris@0 198 if (!isset($this->instances[$route_name])) {
Chris@0 199 $this->instances[$route_name] = [];
Chris@0 200 if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $route_name)) {
Chris@0 201 $base_routes = $cache->data['base_routes'];
Chris@0 202 $parents = $cache->data['parents'];
Chris@0 203 $children = $cache->data['children'];
Chris@0 204 }
Chris@0 205 else {
Chris@0 206 $definitions = $this->getDefinitions();
Chris@0 207 // We build the hierarchy by finding all tabs that should
Chris@0 208 // appear on the current route.
Chris@0 209 $base_routes = [];
Chris@0 210 $parents = [];
Chris@0 211 $children = [];
Chris@0 212 foreach ($definitions as $plugin_id => $task_info) {
Chris@0 213 // Fill in the base_route from the parent to insure consistency.
Chris@0 214 if (!empty($task_info['parent_id']) && !empty($definitions[$task_info['parent_id']])) {
Chris@0 215 $task_info['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
Chris@0 216 // Populate the definitions we use in the next loop. Using a
Chris@0 217 // reference like &$task_info causes bugs.
Chris@0 218 $definitions[$plugin_id]['base_route'] = $definitions[$task_info['parent_id']]['base_route'];
Chris@0 219 }
Chris@0 220 if ($route_name == $task_info['route_name']) {
Chris@0 221 if (!empty($task_info['base_route'])) {
Chris@0 222 $base_routes[$task_info['base_route']] = $task_info['base_route'];
Chris@0 223 }
Chris@0 224 // Tabs that link to the current route are viable parents
Chris@0 225 // and their parent and children should be visible also.
Chris@0 226 // @todo - this only works for 2 levels of tabs.
Chris@0 227 // instead need to iterate up.
Chris@0 228 $parents[$plugin_id] = TRUE;
Chris@0 229 if (!empty($task_info['parent_id'])) {
Chris@0 230 $parents[$task_info['parent_id']] = TRUE;
Chris@0 231 }
Chris@0 232 }
Chris@0 233 }
Chris@0 234 if ($base_routes) {
Chris@0 235 // Find all the plugins with the same root and that are at the top
Chris@0 236 // level or that have a visible parent.
Chris@0 237 foreach ($definitions as $plugin_id => $task_info) {
Chris@0 238 if (!empty($base_routes[$task_info['base_route']]) && (empty($task_info['parent_id']) || !empty($parents[$task_info['parent_id']]))) {
Chris@0 239 // Concat '> ' with root ID for the parent of top-level tabs.
Chris@0 240 $parent = empty($task_info['parent_id']) ? '> ' . $task_info['base_route'] : $task_info['parent_id'];
Chris@0 241 $children[$parent][$plugin_id] = $task_info;
Chris@0 242 }
Chris@0 243 }
Chris@0 244 }
Chris@0 245 $data = [
Chris@0 246 'base_routes' => $base_routes,
Chris@0 247 'parents' => $parents,
Chris@0 248 'children' => $children,
Chris@0 249 ];
Chris@0 250 $this->cacheBackend->set($this->cacheKey . ':' . $route_name, $data, Cache::PERMANENT, $this->cacheTags);
Chris@0 251 }
Chris@0 252 // Create a plugin instance for each element of the hierarchy.
Chris@0 253 foreach ($base_routes as $base_route) {
Chris@0 254 // Convert the tree keyed by plugin IDs into a simple one with
Chris@0 255 // integer depth. Create instances for each plugin along the way.
Chris@0 256 $level = 0;
Chris@0 257 // We used this above as the top-level parent array key.
Chris@0 258 $next_parent = '> ' . $base_route;
Chris@0 259 do {
Chris@0 260 $parent = $next_parent;
Chris@0 261 $next_parent = FALSE;
Chris@0 262 foreach ($children[$parent] as $plugin_id => $task_info) {
Chris@0 263 $plugin = $this->createInstance($plugin_id);
Chris@0 264 $this->instances[$route_name][$level][$plugin_id] = $plugin;
Chris@0 265 // Normally, the link generator compares the href of every link with
Chris@0 266 // the current path and sets the active class accordingly. But the
Chris@0 267 // parents of the current local task may be on a different route in
Chris@0 268 // which case we have to set the class manually by flagging it
Chris@0 269 // active.
Chris@0 270 if (!empty($parents[$plugin_id]) && $route_name != $task_info['route_name']) {
Chris@0 271 $plugin->setActive();
Chris@0 272 }
Chris@0 273 if (isset($children[$plugin_id])) {
Chris@0 274 // This tab has visible children.
Chris@0 275 $next_parent = $plugin_id;
Chris@0 276 }
Chris@0 277 }
Chris@0 278 $level++;
Chris@0 279 } while ($next_parent);
Chris@0 280 }
Chris@0 281
Chris@0 282 }
Chris@0 283 return $this->instances[$route_name];
Chris@0 284 }
Chris@0 285
Chris@0 286 /**
Chris@0 287 * {@inheritdoc}
Chris@0 288 */
Chris@0 289 public function getTasksBuild($current_route_name, RefinableCacheableDependencyInterface &$cacheability) {
Chris@0 290 $tree = $this->getLocalTasksForRoute($current_route_name);
Chris@0 291 $build = [];
Chris@0 292
Chris@0 293 // Collect all route names.
Chris@0 294 $route_names = [];
Chris@0 295 foreach ($tree as $instances) {
Chris@0 296 foreach ($instances as $child) {
Chris@0 297 $route_names[] = $child->getRouteName();
Chris@0 298 }
Chris@0 299 }
Chris@0 300 // Pre-fetch all routes involved in the tree. This reduces the number
Chris@0 301 // of SQL queries that would otherwise be triggered by the access manager.
Chris@0 302 if ($route_names) {
Chris@0 303 $this->routeProvider->getRoutesByNames($route_names);
Chris@0 304 }
Chris@0 305
Chris@0 306 foreach ($tree as $level => $instances) {
Chris@0 307 /** @var $instances \Drupal\Core\Menu\LocalTaskInterface[] */
Chris@0 308 foreach ($instances as $plugin_id => $child) {
Chris@0 309 $route_name = $child->getRouteName();
Chris@0 310 $route_parameters = $child->getRouteParameters($this->routeMatch);
Chris@0 311
Chris@0 312 // Given that the active flag depends on the route we have to add the
Chris@0 313 // route cache context.
Chris@0 314 $cacheability->addCacheContexts(['route']);
Chris@0 315 $active = $this->isRouteActive($current_route_name, $route_name, $route_parameters);
Chris@0 316
Chris@0 317 // The plugin may have been set active in getLocalTasksForRoute() if
Chris@0 318 // one of its child tabs is the active tab.
Chris@0 319 $active = $active || $child->getActive();
Chris@0 320 // @todo It might make sense to use link render elements instead.
Chris@0 321
Chris@0 322 $link = [
Chris@0 323 'title' => $this->getTitle($child),
Chris@0 324 'url' => Url::fromRoute($route_name, $route_parameters),
Chris@0 325 'localized_options' => $child->getOptions($this->routeMatch),
Chris@0 326 ];
Chris@0 327 $access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account, TRUE);
Chris@0 328 $build[$level][$plugin_id] = [
Chris@0 329 '#theme' => 'menu_local_task',
Chris@0 330 '#link' => $link,
Chris@0 331 '#active' => $active,
Chris@0 332 '#weight' => $child->getWeight(),
Chris@0 333 '#access' => $access,
Chris@0 334 ];
Chris@0 335 $cacheability->addCacheableDependency($access)->addCacheableDependency($child);
Chris@0 336 }
Chris@0 337 }
Chris@0 338
Chris@0 339 return $build;
Chris@0 340 }
Chris@0 341
Chris@0 342 /**
Chris@0 343 * {@inheritdoc}
Chris@0 344 */
Chris@0 345 public function getLocalTasks($route_name, $level = 0) {
Chris@0 346 if (!isset($this->taskData[$route_name])) {
Chris@0 347 $cacheability = new CacheableMetadata();
Chris@0 348 $cacheability->addCacheContexts(['route']);
Chris@0 349 // Look for route-based tabs.
Chris@0 350 $this->taskData[$route_name] = [
Chris@0 351 'tabs' => [],
Chris@0 352 'cacheability' => $cacheability,
Chris@0 353 ];
Chris@0 354
Chris@0 355 if (!$this->requestStack->getCurrentRequest()->attributes->has('exception')) {
Chris@0 356 // Safe to build tasks only when no exceptions raised.
Chris@0 357 $data = [];
Chris@0 358 $local_tasks = $this->getTasksBuild($route_name, $cacheability);
Chris@0 359 foreach ($local_tasks as $tab_level => $items) {
Chris@0 360 $data[$tab_level] = empty($data[$tab_level]) ? $items : array_merge($data[$tab_level], $items);
Chris@0 361 }
Chris@0 362 $this->taskData[$route_name]['tabs'] = $data;
Chris@0 363 // Allow modules to alter local tasks.
Chris@0 364 $this->moduleHandler->alter('menu_local_tasks', $this->taskData[$route_name], $route_name, $cacheability);
Chris@0 365 $this->taskData[$route_name]['cacheability'] = $cacheability;
Chris@0 366 }
Chris@0 367 }
Chris@0 368
Chris@0 369 if (isset($this->taskData[$route_name]['tabs'][$level])) {
Chris@0 370 return [
Chris@0 371 'tabs' => $this->taskData[$route_name]['tabs'][$level],
Chris@0 372 'route_name' => $route_name,
Chris@0 373 'cacheability' => $this->taskData[$route_name]['cacheability'],
Chris@0 374 ];
Chris@0 375 }
Chris@0 376
Chris@0 377 return [
Chris@0 378 'tabs' => [],
Chris@0 379 'route_name' => $route_name,
Chris@0 380 'cacheability' => $this->taskData[$route_name]['cacheability'],
Chris@0 381 ];
Chris@0 382 }
Chris@0 383
Chris@0 384 /**
Chris@0 385 * Determines whether the route of a certain local task is currently active.
Chris@0 386 *
Chris@0 387 * @param string $current_route_name
Chris@0 388 * The route name of the current main request.
Chris@0 389 * @param string $route_name
Chris@0 390 * The route name of the local task to determine the active status.
Chris@0 391 * @param array $route_parameters
Chris@0 392 *
Chris@0 393 * @return bool
Chris@0 394 * Returns TRUE if the passed route_name and route_parameters is considered
Chris@0 395 * as the same as the one from the request, otherwise FALSE.
Chris@0 396 */
Chris@0 397 protected function isRouteActive($current_route_name, $route_name, $route_parameters) {
Chris@0 398 // Flag the list element as active if this tab's route and parameters match
Chris@0 399 // the current request's route and route variables.
Chris@0 400 $active = $current_route_name == $route_name;
Chris@0 401 if ($active) {
Chris@0 402 // The request is injected, so we need to verify that we have the expected
Chris@0 403 // _raw_variables attribute.
Chris@0 404 $raw_variables_bag = $this->routeMatch->getRawParameters();
Chris@0 405 // If we don't have _raw_variables, we assume the attributes are still the
Chris@0 406 // original values.
Chris@0 407 $raw_variables = $raw_variables_bag ? $raw_variables_bag->all() : $this->routeMatch->getParameters()->all();
Chris@0 408 $active = array_intersect_assoc($route_parameters, $raw_variables) == $route_parameters;
Chris@0 409 }
Chris@0 410 return $active;
Chris@0 411 }
Chris@0 412
Chris@0 413 }