annotate core/lib/Drupal/Core/Menu/LocalTaskManager.php @ 19:fa3358dc1485 tip

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