Newer
Older
Dries Buytaert
committed
<?php
/**
* @file
* Contains \Drupal\Core\Menu\MenuLocalTaskManager.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
Dries Buytaert
committed
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManager;
Dries Buytaert
committed
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
Dries Buytaert
committed
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Manages discovery and instantiation of menu local task plugins.
*
* This manager finds plugins that are rendered as local tasks (usually tabs).
* Derivatives are supported for modules that wish to generate multiple tabs on
* behalf of something else.
*/
class LocalTaskManager extends DefaultPluginManager {
/**
* {@inheritdoc}
*/
protected $defaults = array(
// (required) The name of the route this task links to.
'route_name' => '',
// Parameters for route variables when generating a link.
'route_parameters' => array(),
// The static title for the local task.
'title' => '',
// The plugin ID of the root tab.
'tab_root_id' => '',
// The plugin ID of the parent tab (or NULL for the top-level tab).
'tab_parent_id' => NULL,
// The weight of the tab.
Angie Byron
committed
'weight' => NULL,
// The default link options.
'options' => array(),
// Default class for local task implementations.
'class' => 'Drupal\Core\Menu\LocalTaskDefault',
Angie Byron
committed
// The plugin id. Set by the plugin system based on the top-level YAML key.
'id' => '',
);
Dries Buytaert
committed
/**
* A controller resolver object.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
Dries Buytaert
committed
*/
protected $controllerResolver;
/**
* A request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The plugin instances.
*
* @var array
*/
protected $instances = array();
/**
* The route provider to load routes by name.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManager
*/
protected $accessManager;
Dries Buytaert
committed
/**
* Constructs a \Drupal\Core\Menu\LocalTaskManager object.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
Dries Buytaert
committed
* An object to use in introspecting route methods.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object to use for building titles and paths for plugin instances.
* @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\LanguageManager $language_manager
* The language manager.
* @param \Drupal\Core\Access\AccessManager $access_manager
* The access manager.
Dries Buytaert
committed
*/
public function __construct(ControllerResolverInterface $controller_resolver, Request $request, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManager $language_manager, AccessManager $access_manager) {
$this->discovery = new YamlDiscovery('local_tasks', $module_handler->getModuleDirectories());
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
$this->factory = new ContainerFactory($this);
Dries Buytaert
committed
$this->controllerResolver = $controller_resolver;
$this->request = $request;
$this->routeProvider = $route_provider;
$this->accessManager = $access_manager;
Dries Buytaert
committed
$this->alterInfo($module_handler, 'local_tasks');
$this->setCacheBackend($cache, $language_manager, 'local_task_plugins', array('local_task' => 1));
Dries Buytaert
committed
}
/**
* {@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));
}
}
Dries Buytaert
committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* Gets the title for a local task.
*
* @param \Drupal\Core\Menu\LocalTaskInterface $local_task
* A local task plugin instance to get the title for.
*
* @return string
* The localized title.
*/
public function getTitle(LocalTaskInterface $local_task) {
$controller = array($local_task, 'getTitle');
$arguments = $this->controllerResolver->getArguments($this->request, $controller);
return call_user_func_array($controller, $arguments);
}
/**
* Find all local tasks that appear on a named route.
*
* @param string $route_name
* The route for which to find local tasks.
*
* @return array
* Returns an array of task levels. Each task level contains instances
* of local tasks (LocalTaskInterface) which appear on the tab route.
* The array keys are the depths and the values are arrays of plugin
* instances.
*/
public function getLocalTasksForRoute($route_name) {
if (!isset($this->instances[$route_name])) {
$this->instances[$route_name] = array();
if ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $route_name)) {
$tab_root_ids = $cache->data['tab_root_ids'];
$parents = $cache->data['parents'];
$children = $cache->data['children'];
Dries Buytaert
committed
}
else {
$definitions = $this->getDefinitions();
// We build the hierarchy by finding all tabs that should
// appear on the current route.
$tab_root_ids = array();
$parents = array();
Dries Buytaert
committed
$children = array();
foreach ($definitions as $plugin_id => $task_info) {
if ($route_name == $task_info['route_name']) {
$tab_root_ids[$task_info['tab_root_id']] = $task_info['tab_root_id'];
// 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['tab_parent_id'])) {
$parents[$task_info['tab_parent_id']] = TRUE;
}
Dries Buytaert
committed
}
}
if ($tab_root_ids) {
// 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($tab_root_ids[$task_info['tab_root_id']]) && (empty($task_info['tab_parent_id']) || !empty($parents[$task_info['tab_parent_id']]))) {
// Concat '> ' with root ID for the parent of top-level tabs.
$parent = empty($task_info['tab_parent_id']) ? '> ' . $task_info['tab_root_id'] : $task_info['tab_parent_id'];
$children[$parent][$plugin_id] = $task_info;
Dries Buytaert
committed
}
Dries Buytaert
committed
}
$data = array(
'tab_root_ids' => $tab_root_ids,
'parents' => $parents,
'children' => $children,
);
$this->cacheBackend->set($this->cacheKey . ':' . $route_name, $data, CacheBackendInterface::CACHE_PERMANENT, $this->cacheTags);
Dries Buytaert
committed
}
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Create a plugin instance for each element of the hierarchy.
foreach ($tab_root_ids as $root_id) {
// 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 = '> ' . $root_id;
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, l() 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);
}
Dries Buytaert
committed
}
return $this->instances[$route_name];
}
/**
* Gets the render array for all local tasks.
*
* @param string $current_route_name
Dries Buytaert
committed
* The route for which to make renderable local tasks.
*
* @return array
* A render array as expected by theme_menu_local_tasks.
*/
public function getTasksBuild($current_route_name) {
$tree = $this->getLocalTasksForRoute($current_route_name);
Dries Buytaert
committed
$build = array();
Alex Pott
committed
// Collect all route names.
$route_names = array();
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.
Alex Pott
committed
$routes = $route_names ? $this->routeProvider->getRoutesByNames($route_names) : array();
Dries Buytaert
committed
foreach ($tree as $level => $instances) {
foreach ($instances as $plugin_id => $child) {
$route_name = $child->getRouteName();
$route_parameters = $child->getRouteParameters($this->request);
Dries Buytaert
committed
// Find out whether the user has access to the task.
$access = $this->accessManager->checkNamedRoute($route_name, $route_parameters);
Dries Buytaert
committed
if ($access) {
$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();
Dries Buytaert
committed
// @todo It might make sense to use menu link entities instead of
// arrays.
$link = array(
Dries Buytaert
committed
'title' => $this->getTitle($child),
'route_name' => $route_name,
'route_parameters' => $route_parameters,
'localized_options' => $child->getOptions($this->request),
Dries Buytaert
committed
);
$build[$level][$plugin_id] = array(
Dries Buytaert
committed
'#theme' => 'menu_local_task',
'#link' => $link,
Dries Buytaert
committed
'#active' => $active,
'#weight' => $child->getWeight(),
'#access' => $access,
);
}
}
}
return $build;
}
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
/**
* 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->request->attributes->get('_raw_variables');
// 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->request->attributes->all();
$active = array_intersect_assoc($route_parameters, $raw_variables) == $route_parameters;
}
return $active;
}