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