Commit 17cc6dab authored by Dries's avatar Dries

Issue #2145041 by tim.plunkett: Allow dynamic routes to be defined via a callback.

parent 5b6f9d79
......@@ -313,7 +313,7 @@ services:
arguments: ['@database']
router.builder:
class: Drupal\Core\Routing\RouteBuilder
arguments: ['@router.dumper', '@lock', '@event_dispatcher', '@module_handler']
arguments: ['@router.dumper', '@lock', '@event_dispatcher', '@module_handler', '@controller_resolver']
path.alias_manager.cached:
class: Drupal\Core\CacheDecorator\AliasManagerCacheDecorator
arguments: ['@path.alias_manager', '@cache.path']
......
......@@ -53,7 +53,7 @@ public function onRoutingRouteAlterSetAccessCheck(RouteBuildEvent $event) {
*/
static function getSubscribedEvents() {
// Setting very low priority to ensure access checks are run after alters.
$events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetAccessCheck', -50);
$events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetAccessCheck', -1000);
return $events;
}
......
......@@ -72,7 +72,7 @@ public function onRoutingRouteAlterSetType(RouteBuildEvent $event) {
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetType', 100);
$events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetType', -150);
return $events;
}
}
......@@ -49,7 +49,7 @@ public function onRoutingRouteAlterSetParameterConverters(RouteBuildEvent $event
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetParameterConverters', 10);
$events[RoutingEvents::ALTER][] = array('onRoutingRouteAlterSetParameterConverters', -200);
return $events;
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Routing;
use Drupal\Component\Discovery\YamlDiscovery;
use Drupal\Core\Controller\ControllerResolverInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Routing\RouteCollection;
......@@ -59,6 +60,13 @@ class RouteBuilder {
*/
protected $moduleHandler;
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* Construcs the RouteBuilder using the passed MatcherDumperInterface.
*
......@@ -70,12 +78,15 @@ class RouteBuilder {
* The event dispatcher to notify of routes.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
*/
public function __construct(MatcherDumperInterface $dumper, LockBackendInterface $lock, EventDispatcherInterface $dispatcher, ModuleHandlerInterface $module_handler) {
public function __construct(MatcherDumperInterface $dumper, LockBackendInterface $lock, EventDispatcherInterface $dispatcher, ModuleHandlerInterface $module_handler, ControllerResolverInterface $controller_resolver) {
$this->dumper = $dumper;
$this->lock = $lock;
$this->dispatcher = $dispatcher;
$this->moduleHandler = $module_handler;
$this->controllerResolver = $controller_resolver;
}
/**
......@@ -98,6 +109,23 @@ public function rebuild() {
foreach ($yaml_discovery->findAll() as $provider => $routes) {
$collection = new RouteCollection();
// The top-level 'routes_callback' is a list of methods in controller
// syntax, see \Drupal\Core\Controller\ControllerResolver. These methods
// should return a set of \Symfony\Component\Routing\Route objects, either
// in an associative array keyed by the route name, or as a new
// \Symfony\Component\Routing\RouteCollection, which will be iterated over
// and added to the collection for this provider.
if (isset($routes['route_callbacks'])) {
foreach ($routes['route_callbacks'] as $route_callback) {
$callback = $this->controllerResolver->getControllerFromDefinition($route_callback);
if ($callback_routes = call_user_func($callback)) {
foreach ($callback_routes as $name => $callback_route) {
$collection->add($name, $callback_route);
}
}
}
unset($routes['route_callbacks']);
}
foreach ($routes as $name => $route_info) {
$route_info += array(
'defaults' => array(),
......@@ -115,8 +143,8 @@ public function rebuild() {
}
// Now allow modules to register additional, dynamic routes.
// @todo Either remove this alter or the per-provider alter.
$collection = new RouteCollection();
$this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection, 'dynamic_routes'));
$this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection, 'dynamic_routes'));
$this->dumper->addRoutes($collection);
$this->dumper->dump(array('provider' => 'dynamic_routes'));
......
......@@ -16,18 +16,6 @@
*/
abstract class RouteSubscriberBase implements EventSubscriberInterface {
/**
* Provides new routes by adding them to the collection.
*
* Subclasses should use this method and add \Symfony\Component\Routing\Route
* objects with $collection->add('route_name', $route);.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* The route collection for adding routes.
*/
protected function routes(RouteCollection $collection) {
}
/**
* Alters existing routes for a specific collection.
*
......@@ -44,22 +32,10 @@ protected function alterRoutes(RouteCollection $collection, $provider) {
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[RoutingEvents::DYNAMIC] = 'onDynamicRoutes';
$events[RoutingEvents::ALTER] = 'onAlterRoutes';
return $events;
}
/**
* Delegates the route gathering to self::routes().
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The route build event.
*/
public function onDynamicRoutes(RouteBuildEvent $event) {
$collection = $event->getRouteCollection();
$this->routes($collection);
}
/**
* Delegates the route altering to self::alterRoutes().
*
......
......@@ -23,16 +23,4 @@ final class RoutingEvents {
*/
const ALTER = 'routing.route_alter';
/**
* The DYNAMIC event is fired to allow modules to register additional routes.
*
* Most routes are static, an should be defined as such. Dynamic routes are
* only those whose existence changes depending on the state of the system
* at runtime, depending on configuration.
*
* @see \Drupal\Core\Routing\RouteBuildEvent
*
* @var string
*/
const DYNAMIC = 'routing.route_dynamic';
}
......@@ -36,7 +36,14 @@ public function __construct(ConfigMapperManagerInterface $mapper_manager) {
/**
* {@inheritdoc}
*/
public function routes(RouteCollection $collection) {
protected function alterRoutes(RouteCollection $collection, $provider) {
// @todo \Drupal\config_translation\ConfigNamesMapper uses the route
// provider directly, which is unsafe during rebuild. This currently only
// works by coincidence; fix in https://drupal.org/node/2158571.
if ($provider != 'dynamic_routes') {
return;
}
$mappers = $this->mapperManager->getMappers();
foreach ($mappers as $mapper) {
$collection->add($mapper->getOverviewRouteName(), $mapper->getOverviewRoute());
......
......@@ -5,7 +5,7 @@ services:
content_translation.subscriber:
class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber
arguments: ['@content_translation.manager', '@router.route_provider']
arguments: ['@content_translation.manager']
tags:
- { name: event_subscriber }
......
......@@ -8,12 +8,10 @@
namespace Drupal\content_translation\Routing;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
* Subscriber for entity translation routes.
......@@ -27,42 +25,24 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
*/
protected $contentTranslationManager;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* Constructs a ContentTranslationRouteSubscriber object.
*
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
* The content translation manager.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
*/
public function __construct(ContentTranslationManagerInterface $content_translation_manager, RouteProviderInterface $route_provider) {
public function __construct(ContentTranslationManagerInterface $content_translation_manager) {
$this->contentTranslationManager = $content_translation_manager;
$this->routeProvider = $route_provider;
}
/**
* {@inheritdoc}
*/
protected function routes(RouteCollection $collection) {
protected function alterRoutes(RouteCollection $collection, $provider) {
foreach ($this->contentTranslationManager->getSupportedEntityTypes() as $entity_type => $entity_info) {
// First try to get the route from the dynamic_routes collection.
// Try to get the route from the current collection.
if (!$entity_route = $collection->get($entity_info['links']['canonical'])) {
// Then try to get the route from the route provider itself, checking
// all previous collections.
try {
$entity_route = $this->routeProvider->getRouteByName($entity_info['links']['canonical']);
}
// If the route was not found, skip this entity type.
catch (RouteNotFoundException $e) {
continue;
}
continue;
}
$path = $entity_route->getPath() . '/translations';
......@@ -167,7 +147,7 @@ protected function routes(RouteCollection $collection) {
*/
public static function getSubscribedEvents() {
$events = parent::getSubscribedEvents();
$events[RoutingEvents::DYNAMIC] = array('onDynamicRoutes', -100);
$events[RoutingEvents::ALTER] = array('onAlterRoutes', -100);
return $events;
}
......
services:
field_ui.subscriber:
class: Drupal\field_ui\Routing\RouteSubscriber
arguments: ['@entity.manager', '@router.route_provider']
arguments: ['@entity.manager']
tags:
- { name: event_subscriber }
access_check.field_ui.view_mode:
......
......@@ -8,10 +8,8 @@
namespace Drupal\field_ui\Routing;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
......@@ -27,44 +25,26 @@ class RouteSubscriber extends RouteSubscriberBase {
*/
protected $manager;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* Constructs a RouteSubscriber object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* The entity type manager.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
*/
public function __construct(EntityManagerInterface $manager, RouteProviderInterface $route_provider) {
public function __construct(EntityManagerInterface $manager) {
$this->manager = $manager;
$this->routeProvider = $route_provider;
}
/**
* {@inheritdoc}
*/
protected function routes(RouteCollection $collection) {
protected function alterRoutes(RouteCollection $collection, $provider) {
foreach ($this->manager->getDefinitions() as $entity_type => $entity_info) {
$defaults = array();
if ($entity_info['fieldable'] && isset($entity_info['links']['admin-form'])) {
// First try to get the route from the dynamic_routes collection.
// Try to get the route from the current collection.
if (!$entity_route = $collection->get($entity_info['links']['admin-form'])) {
// Then try to get the route from the route provider itself, checking
// all previous collections.
try {
$entity_route = $this->routeProvider->getRouteByName($entity_info['links']['admin-form']);
}
// If the route was not found, skip this entity type.
catch (RouteNotFoundException $e) {
continue;
}
continue;
}
$path = $entity_route->getPath();
......@@ -155,7 +135,7 @@ protected function routes(RouteCollection $collection) {
*/
public static function getSubscribedEvents() {
$events = parent::getSubscribedEvents();
$events[RoutingEvents::DYNAMIC] = array('onDynamicRoutes', -100);
$events[RoutingEvents::ALTER] = array('onAlterRoutes', -100);
return $events;
}
......
......@@ -68,3 +68,6 @@ image.effect_edit_form:
_title: 'Edit image effect'
requirements:
_permission: 'administer image styles'
route_callbacks:
- '\Drupal\image\Routing\ImageStyleRoutes::routes'
services:
image.route_subscriber:
class: Drupal\image\EventSubscriber\RouteSubscriber
tags:
- { name: 'event_subscriber' }
path_processor.image_styles:
class: Drupal\image\PathProcessor\PathProcessorImageStyles
tags:
......
......@@ -5,28 +5,31 @@
* Contains \Drupal\image\EventSubscriber\RouteSubscriber.
*/
namespace Drupal\image\EventSubscriber;
namespace Drupal\image\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Defines a route subscriber to register a url for serving image styles.
*/
class RouteSubscriber extends RouteSubscriberBase {
class ImageStyleRoutes {
/**
* {@inheritdoc}
* Returns an array of route objects.
*
* @return \Symfony\Component\Routing\Route[]
* An array of route objects.
*/
protected function routes(RouteCollection $collection) {
public function routes() {
$routes = array();
// Generate image derivatives of publicly available files. If clean URLs are
// disabled image derivatives will always be served through the menu system.
// If clean URLs are enabled and the image derivative already exists, PHP
// will be bypassed.
$directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath();
$route = new Route('/' . $directory_path . '/styles/{image_style}/{scheme}',
$routes['image.style_public'] = new Route(
'/' . $directory_path . '/styles/{image_style}/{scheme}',
array(
'_controller' => 'Drupal\image\Controller\ImageStyleDownloadController::deliver',
),
......@@ -34,7 +37,7 @@ protected function routes(RouteCollection $collection) {
'_access' => 'TRUE',
)
);
$collection->add('image.style_public', $route);
return $routes;
}
}
......@@ -5,17 +5,17 @@
* Contains \Drupal\rest\EventSubscriber\RouteSubscriber.
*/
namespace Drupal\rest\EventSubscriber;
namespace Drupal\rest\Routing;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\rest\Plugin\Type\ResourcePluginManager;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Subscriber for REST-style routes.
*/
class RouteSubscriber extends RouteSubscriberBase {
class ResourceRoutes implements ContainerInjectionInterface {
/**
* The plugin manager for REST plugins.
......@@ -47,7 +47,21 @@ public function __construct(ResourcePluginManager $manager, ConfigFactory $confi
/**
* {@inheritdoc}
*/
protected function routes(RouteCollection $collection) {
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.rest'),
$container->get('config.factory')
);
}
/**
* Returns an array of route objects.
*
* @return \Symfony\Component\Routing\Route[]
* An array of route objects.
*/
public function routes() {
$routes = array();
$enabled_resources = $this->config->get('rest.settings')->load()->get('resources');
// Iterate over all enabled resource plugins.
......@@ -82,10 +96,11 @@ protected function routes(RouteCollection $collection) {
// The configuration seems legit at this point, so we set the
// authentication provider and add the route.
$route->setOption('_auth', $enabled_methods[$method]['supported_auth']);
$collection->add("rest.$name", $route);
$routes["rest.$name"] = $route;
}
}
}
return $routes;
}
}
......@@ -4,3 +4,6 @@ rest.csrftoken:
_controller: '\Drupal\rest\RequestHandler::csrfToken'
requirements:
_access: 'TRUE'
route_callbacks:
- '\Drupal\rest\Routing\ResourceRoutes::routes'
......@@ -9,11 +9,6 @@ services:
factory_method: get
factory_service: cache_factory
arguments: [rest]
rest.route_subscriber:
class: Drupal\rest\EventSubscriber\RouteSubscriber
tags:
- { name: event_subscriber }
arguments: ['@plugin.manager.rest', '@config.factory']
access_check.rest.csrf:
class: Drupal\rest\Access\CSRFAccessCheck
tags:
......
......@@ -2,20 +2,20 @@
/**
* @file
* Contains \Drupal\search\Routing\SearchRouteSubscriber.
* Contains \Drupal\search\Routing\SearchPluginRoutes.
*/
namespace Drupal\search\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\search\SearchPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides dynamic routes for search.
*/
class SearchRouteSubscriber extends RouteSubscriberBase {
class SearchPluginRoutes implements ContainerInjectionInterface {
/**
* The search plugin manager.
......@@ -37,23 +37,37 @@ public function __construct(SearchPluginManager $search_plugin_manager) {
/**
* {@inheritdoc}
*/
protected function routes(RouteCollection $collection) {
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.search')
);
}
/**
* Returns an array of route objects.
*
* @return \Symfony\Component\Routing\Route[]
* An array of route objects.
*/
public function routes() {
$routes = array();
foreach ($this->searchManager->getActiveDefinitions() as $plugin_id => $search_info) {
$path = 'search/' . $search_info['path'] . '/{keys}';
$defaults = array(
'_content' => 'Drupal\search\Controller\SearchController::view',
'_title' => $search_info['title'],
'plugin_id' => $plugin_id,
'keys' => '',
);
$requirements = array(
'keys' => '.+',
'_search_plugin_view_access' => $plugin_id,
'_permission' => 'search content',
$routes["search.view_$plugin_id"] = new Route(
'search/' . $search_info['path'] . '/{keys}',
array(
'_content' => 'Drupal\search\Controller\SearchController::view',
'_title' => $search_info['title'],
'plugin_id' => $plugin_id,
'keys' => '',
),
array(
'keys' => '.+',
'_search_plugin_view_access' => $plugin_id,
'_permission' => 'search content',
)
);
$route = new Route($path, $defaults, $requirements);
$collection->add('search.view_' . $plugin_id, $route);
}
return $routes;
}
}
......@@ -25,3 +25,6 @@ search.view:
keys: '.+'
_permission: 'search content'
_search_access: 'TRUE'
route_callbacks:
- '\Drupal\search\Routing\SearchPluginRoutes::routes'
......@@ -14,9 +14,3 @@ services:
arguments: ['@plugin.manager.search']
tags:
- { name: access_check }
route_subscriber.search:
class: Drupal\search\Routing\SearchRouteSubscriber
arguments: ['@plugin.manager.search']
tags:
- { name: event_subscriber }
......@@ -115,11 +115,6 @@ public function testControllerPlaceholdersDefaultValuesProvided() {
* @see \Drupal\router_test\RouteSubscriber
*/
public function testDynamicRoutes() {
// Test the dynamically added route.
$this->drupalGet('router_test/test5');
$this->assertResponse(200);
$this->assertRaw('test5', 'The correct string was returned because the route was successful.');
// Test the altered route.
$this->drupalGet('router_test/test6');
$this->assertResponse(200);
......
......@@ -22,3 +22,6 @@ entity_test.render_no_view_mode:
_entity_view: 'entity_test'
requirements:
_access: 'TRUE'
route_callbacks:
- '\Drupal\entity_test\Routing\EntityTestRoutes::routes'
services:
entity_test.subscriber:
class: Drupal\entity_test\Routing\RouteSubscriber
tags:
- { name: event_subscriber }