Mercurial > hg > isophonics-drupal-site
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 } |