Commit 0fc52f11 authored by alexpott's avatar alexpott

Issue #2077473 by dawehner, damiankloip, pwolanin: Replace local action plugin...

Issue #2077473 by dawehner, damiankloip, pwolanin: Replace local action plugin discovery with YamlDiscovery and handle routes with parameters.
parent f3669304
......@@ -174,7 +174,7 @@ services:
arguments: ['@container.namespaces']
plugin.manager.menu.local_action:
class: Drupal\Core\Menu\LocalActionManager
arguments: ['@container.namespaces', '@controller_resolver', '@request', '@module_handler', '@cache.cache', '@language_manager']
arguments: ['@controller_resolver', '@request', '@router.route_provider', '@module_handler', '@cache.cache', '@language_manager', '@access_manager']
plugin.manager.menu.local_task:
class: Drupal\Core\Menu\LocalTaskManager
arguments: ['@controller_resolver', '@request', '@router.route_provider', '@module_handler', '@cache.cache', '@language_manager', '@access_manager']
......
......@@ -1763,12 +1763,21 @@ function theme_menu_local_action($variables) {
$link += array(
'href' => '',
'localized_options' => array(),
'route_parameters' => array(),
);
$link['localized_options']['attributes']['class'][] = 'button';
$link['localized_options']['attributes']['class'][] = 'button-action';
$output = '<li>';
$output .= l($link['title'], $link['href'], $link['localized_options']);
// @todo Remove this check and the call to l() when all pages are converted to
// routes.
// @todo Figure out how to support local actions without a href properly.
if ($link['href'] === '' && !empty($link['route_name'])) {
$output .= Drupal::l($link['title'], $link['route_name'], $link['route_parameters'], $link['localized_options']);
}
else {
$output .= l($link['title'], $link['href'], $link['localized_options']);
}
$output .= "</li>";
return $output;
......@@ -2316,21 +2325,9 @@ function menu_secondary_local_tasks() {
*/
function menu_get_local_actions() {
$links = menu_local_tasks();
$router_item = menu_get_item();
$route_name = Drupal::request()->attributes->get(RouteObjectInterface::ROUTE_NAME);
$manager = \Drupal::service('plugin.manager.menu.local_action');
$local_actions = $manager->getActionsForRoute($router_item['route_name']);
foreach ($local_actions as $plugin) {
$route_path = $manager->getPath($plugin);
$action_router_item = menu_get_item($route_path);
$links['actions'][$route_path] = array(
'#theme' => 'menu_local_action',
'#link' => array(
'title' => $manager->getTitle($plugin),
'href' => $route_path,
),
'#access' => $action_router_item['access'],
);
}
$links['actions'] += $manager->getActionsForRoute($route_name);
return $links['actions'];
}
......
<?php
/**
* @file
* Contains \Drupal\Core\Annotation\LocalAction.
*/
namespace Drupal\Core\Annotation\Menu;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a LocalAction type Plugin annotation object.
*
* @Annotation
*/
class LocalAction extends Plugin {
/**
* The ID.
*
* @var string
*/
public $id;
/**
* A static title for the local action.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $title;
/**
* The route name.
*
* @var string
*/
public $route_name;
/**
* An array of route names where this action appears.
*
* @var array (optional)
*/
public $appears_on = array();
}
......@@ -2,59 +2,45 @@
/**
* @file
* Contains \Drupal\Core\Menu\LocalActionBase.
* Contains \Drupal\Core\Menu\LocalActionDefault.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Menu\LocalActionInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\StringTranslation\Translator\TranslatorInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides defaults and base methods for menu local action plugins.
*
* @todo This class needs more documentation and/or @see references.
* Provides a default implementation for local action plugins.
*/
abstract class LocalActionBase extends PluginBase implements LocalActionInterface, ContainerFactoryPluginInterface {
class LocalActionDefault extends PluginBase implements LocalActionInterface, ContainerFactoryPluginInterface {
/**
* String translation object.
* The route provider to load routes by name.
*
* @var \Drupal\Core\StringTranslation\Translator\TranslatorInterface
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $t;
protected $routeProvider;
/**
* URL generator object.
* Constructs a LocalActionDefault object.
*
* @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
*/
protected $generator;
/**
* Constructs a LocalActionBase object.
*
* @param \Drupal\Core\StringTranslation\Translator\TranslatorInterface $string_translation
* A translator object for use by subclasses generating localized titles.
* @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $generator
* A URL generator object used to get the path from the route.
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider to load routes by name.
*/
public function __construct(TranslatorInterface $string_translation, UrlGeneratorInterface $generator, array $configuration, $plugin_id, array $plugin_definition) {
public function __construct(array $configuration, $plugin_id, array $plugin_definition, RouteProviderInterface $route_provider) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->generator = $generator;
// This is available for subclasses that need to translate a dynamic title.
$this->t = $string_translation;
$this->routeProvider = $route_provider;
}
/**
......@@ -62,11 +48,10 @@ public function __construct(TranslatorInterface $string_translation, UrlGenerat
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
return new static(
$container->get('string_translation'),
$container->get('url_generator'),
$configuration,
$plugin_id,
$plugin_definition
$plugin_definition,
$container->get('router.route_provider')
);
}
......@@ -82,23 +67,53 @@ public function getRouteName() {
*/
public function getTitle() {
// Subclasses may pull in the request or specific attributes as parameters.
return $this->pluginDefinition['title'];
return $this->t($this->pluginDefinition['title']);
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->pluginDefinition['weight'];
}
/**
* {@inheritdoc}
*/
public function getPath() {
// Subclasses may set a request into the generator or use any desired method
// to generate the path.
// @todo - use the new method from https://drupal.org/node/2031353
$path = $this->generator->generate($this->getRouteName());
// In order to get the Drupal path the base URL has to be stripped off.
$base_url = $this->generator->getContext()->getBaseUrl();
if (!empty($base_url) && strpos($path, $base_url) === 0) {
$path = substr($path, strlen($base_url));
public function getRouteParameters(Request $request) {
$parameters = isset($this->pluginDefinition['route_parameters']) ? $this->pluginDefinition['route_parameters'] : array();
$route = $this->routeProvider->getRouteByName($this->getRouteName());
$variables = $route->compile()->getVariables();
// Normally the \Drupal\Core\ParamConverter\ParamConverterManager has
// processed the Request attributes, and in that case the _raw_variables
// attribute holds the original path strings keyed to the corresponding
// slugs in the path patterns. For example, if the route's path pattern is
// /filter/tips/{filter_format} and the path is /filter/tips/plain_text then
// $raw_variables->get('filter_format') == 'plain_text'.
$raw_variables = $request->attributes->get('_raw_variables');
foreach ($variables as $name) {
if (isset($parameters[$name])) {
continue;
}
if ($raw_variables && $raw_variables->has($name)) {
$parameters[$name] = $raw_variables->get($name);
}
elseif ($request->attributes->has($name)) {
$parameters[$name] = $request->attributes->get($name);
}
}
return trim($path, '/');
// The UrlGenerator will throw an exception if expected parameters are
// missing. This method should be overridden if that is possible.
return $parameters;
}
/**
* {@inheritdoc}
*/
public function getOptions(Request $request) {
return (array) $this->pluginDefinition['options'];
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Menu;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines an interface for menu local actions.
*/
......@@ -20,6 +22,39 @@ interface LocalActionInterface {
*/
public function getRouteName();
/**
* Returns the route parameters needed to render a link for the local action.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
*
* @return array
* An array of parameter names and values.
*
* @see \Drupal\Core\Utility\LinkGeneratorInterface::generate()
*/
public function getRouteParameters(Request $request);
/**
* Returns the weight for the local action.
*
* @return int
*/
public function getWeight();
/**
* Returns options for rendering a link for the local action.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object representing the current request.
*
* @return array
* An associative array of options.
*
* @see \Drupal\Core\Utility\LinkGeneratorInterface::generate()
*/
public function getOptions(Request $request);
/**
* Returns the localized title to be shown for this action.
*
......@@ -33,17 +68,6 @@ public function getRouteName();
*/
public function getTitle();
/**
* Return an internal Drupal path to use when linking to the action.
*
* Subclasses may add arguments for request attributes which will then be
* automatically supplied by the controller resolver.
*
* @return string
* The path to use when linking to the action.
*
* @see \Drupal\Core\Menu\LocalActionManager::getPath()
*/
public function getPath();
}
......@@ -7,11 +7,17 @@
namespace Drupal\Core\Menu;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Menu\LocalActionInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Component\Plugin\Discovery\ProcessDecorator;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
......@@ -25,6 +31,30 @@
*/
class LocalActionManager extends DefaultPluginManager {
/**
* Provides some default values for all local action plugins.
*
* @var array
*/
protected $defaults = array(
// The plugin id. Set by the plugin system based on the top-level YAML key.
'id' => NULL,
// The static title for the local action.
'title' => '',
// The weight of the local action.
'weight' => NULL,
// (Required) the route name used to generate a link.
'route_name' => NULL,
// Default route parameters for generating links.
'route_parameters' => array(),
// Associative array of link options.
'options' => array(),
// The route names where this local action appears.
'appears_on' => array(),
// Default class for local action implementations.
'class' => 'Drupal\Core\Menu\LocalActionDefault',
);
/**
* A controller resolver object.
*
......@@ -40,6 +70,20 @@ class LocalActionManager extends DefaultPluginManager {
protected $request;
/**
* 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;
/**
* The plugin instances.
*
* @var array
......@@ -49,28 +93,35 @@ class LocalActionManager extends DefaultPluginManager {
/**
* Constructs a LocalActionManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface $controller_resolver
* 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.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Language\LanguageManager $language_manager
* The language manager.
* @param \Drupal\Core\Access\AccessManager $access_manager
* The access manager.
*/
public function __construct(\Traversable $namespaces, ControllerResolverInterface $controller_resolver, Request $request, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager) {
parent::__construct('Plugin/Menu/LocalAction', $namespaces, 'Drupal\Core\Annotation\Menu\LocalAction');
public function __construct(ControllerResolverInterface $controller_resolver, Request $request, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager, AccessManager $access_manager) {
// Skip calling the parent constructor, since that assumes annotation-based
// discovery.
$this->discovery = new YamlDiscovery('local_actions', $module_handler->getModuleDirectories());
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
$this->factory = new ContainerFactory($this);
$this->routeProvider = $route_provider;
$this->accessManager = $access_manager;
$this->controllerResolver = $controller_resolver;
$this->request = $request;
$this->alterInfo($module_handler, 'menu_local_actions');
$this->setCacheBackend($cache_backend, $language_manager, 'local_action_plugins');
$this->setCacheBackend($cache_backend, $language_manager, 'local_action_plugins', array('local_action' => TRUE));
}
/**
......@@ -91,45 +142,50 @@ public function getTitle(LocalActionInterface $local_action) {
return call_user_func_array($controller, $arguments);
}
/**
* Gets the Drupal path for a local action.
*
* @param \Drupal\Core\Menu\LocalActionInterface $local_action
* An object to get the path from.
*
* @return string
* The path.
*
* @throws \BadMethodCallException
* If the plugin does not implement the getPath() method.
*/
public function getPath(LocalActionInterface $local_action) {
$controller = array($local_action, 'getPath');
$arguments = $this->controllerResolver->getArguments($this->request, $controller);
return call_user_func_array($controller, $arguments);
}
/**
* Finds all local actions that appear on a named route.
*
* @param string $route_name
* The route for which to find local actions.
* @param string $route_appears
* The route name for which to find local actions.
*
* @return \Drupal\Core\Menu\LocalActionInterface[]
* An array of LocalActionInterface objects that appear on the route path.
* @return array
* An array of link render arrays.
*/
public function getActionsForRoute($route_name) {
if (!isset($this->instances[$route_name])) {
$this->instances[$route_name] = array();
public function getActionsForRoute($route_appears) {
if (!isset($this->instances[$route_appears])) {
$route_names = array();
$this->instances[$route_appears] = array();
// @todo - optimize this lookup by compiling or caching.
foreach ($this->getDefinitions() as $plugin_id => $action_info) {
if (in_array($route_name, $action_info['appears_on'])) {
if (in_array($route_appears, $action_info['appears_on'])) {
$plugin = $this->createInstance($plugin_id);
$this->instances[$route_name][$plugin_id] = $plugin;
$route_names[] = $plugin->getRouteName();
$this->instances[$route_appears][$plugin_id] = $plugin;
}
}
// Pre-fetch all the action route objects. This reduces the number of SQL
// queries that would otherwise be triggered by the access manager.
if (!empty($route_names)) {
$this->routeProvider->getRoutesByNames($route_names);
}
}
$links = array();
foreach ($this->instances[$route_appears] as $plugin) {
$route_name = $plugin->getRouteName();
$route_parameters = $plugin->getRouteParameters($this->request);
$links[$route_name] = array(
'#theme' => 'menu_local_action',
'#link' => array(
'title' => $this->getTitle($plugin),
'route_name' => $route_name,
'route_parameters' => $route_parameters,
'localized_options' => $plugin->getOptions($this->request),
),
'#access' => $this->accessManager->checkNamedRoute($route_name, $route_parameters),
'#weight' => $plugin->getWeight(),
);
}
return $this->instances[$route_name];
return $links;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\Plugin\Derivative\StaticLocalActionDeriver.
*/
namespace Drupal\Core\Menu\Plugin\Derivative;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Component\Utility\String;
use Drupal\Component\Discovery\YamlDiscovery;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides plugin derivatives for local actions provided in YAML files.
*/
class StaticLocalActionDeriver implements ContainerDerivativeInterface {
/**
* List of derivative definitions.
*
* @var array
*/
protected $derivatives = array();
/**
* The base plugin ID.
*
* @var string
*/
protected $basePluginId;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The translation manager.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $translationManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('module_handler'),
$container->get('string_translation')
);
}
/**
* Constructs a StaticLocalActionDeriver object.
*
* @param string $base_plugin_id
* The base plugin ID.
* @param\Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\StringTranslation\TranslationInterface translation_manager
* The translation manager.
*/
public function __construct($base_plugin_id, ModuleHandlerInterface $module_handler, TranslationInterface $translation_manager) {
$this->basePluginId = $base_plugin_id;
$this->moduleHandler = $module_handler;
$this->translationManager = $translation_manager;
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinition($derivative_id, array $base_plugin_definition) {
if (!empty($this->derivatives) && !empty($this->derivatives[$derivative_id])) {
return $this->derivatives[$derivative_id];
}
$this->getDerivativeDefinitions($base_plugin_definition);
return $this->derivatives[$derivative_id];
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions(array $base_plugin_definition) {
$yaml_discovery = new YamlDiscovery('local_actions', $this->moduleHandler->getModuleDirectories());
$required_keys = array('title' => 1, 'route_name' => 1, 'appears_on' => 1);
foreach ($yaml_discovery->findAll() as $module => $local_actions) {
if (!empty($local_actions)) {
foreach ($local_actions as $name => $info) {
if ($missing_keys = array_diff_key($required_keys, array_intersect_key($info, $required_keys))) {
throw new PluginException(String::format('Static local action @name is missing @keys', array('@name' => $name, '@keys' => implode(', ', array_keys($missing_keys)))));
}
$info += array('provider' => $module);
// Make sure 'appears_on' is an array.
$info['appears_on'] = (array) $info['appears_on'];
$info['title'] = $this->t($info['title']);
$this->derivatives[$name] = $info + $base_plugin_definition;
}
}
}
return $this->derivatives;
}
/**
* Translates a string to the current language or to a given language.
*
* See the t() documentation for details.
*/
protected function t($string, array $args = array(), array $options = array()) {
return $this->translationManager->translate($string, $args, $options);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Annotation\StaticLocalAction.
*/
namespace Drupal\Core\Menu\Plugin\Menu\LocalAction;
use Drupal\Core\Annotation\Menu\LocalAction;
use Drupal\Core\Menu\LocalActionBase;
/**
* @LocalAction(
* id = "local_action_static",
* derivative = "Drupal\Core\Menu\Plugin\Derivative\StaticLocalActionDeriver"
* )
*/
class StaticLocalAction extends LocalActionBase {
}
config_test_entity_add_local_action:
route_name: config_test.entity_add
title: 'Add test configuration'
appears_on:
- config_test.list_page
......@@ -18,11 +18,6 @@ function config_test_menu() {
'title' => 'Test configuration',
'route_name' => 'config_test.list_page',
);
$items['admin/structure/config_test/add'] = array(
'title' => 'Add test configuration',
'route_name' => 'config_test.entity_add',
'type' => MENU_SIBLING_LOCAL_TASK,
);
$items['admin/structure/config_test/manage/%config_test'] = array(
'route_name' => 'config_test.entity',
);
......
<?php
/**
* @file
* Contains \Drupal\config_test\Plugin\Menu\AddConfigTestEntityLocalAction.
*/
namespace Drupal\config_test\Plugin\Menu\LocalAction;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Menu\LocalActionBase;
use Drupal\Core\Annotation\Menu\LocalAction;
/**
* @LocalAction(
* id = "config_test_entity_add_local_action",
* route_name = "config_test.entity_add",
* title = @Translation("Add test configuration"),
* appears_on = {"config_test.list_page"}
* )
*/
class AddConfigTestEntityLocalAction extends LocalActionBase {
}