diff --git a/core/core.services.yml b/core/core.services.yml index 6ad63cf..c789956 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -271,6 +271,11 @@ services: arguments: ['@database', '@router.builder'] tags: - { name: event_subscriber } + router.route_preloader: + class: Drupal\Core\Routing\RoutePreloader + arguments: ['@router.route_provider', '@state', '@content_negotiation'] + tags: + - { name: 'event_subscriber' } router.matcher.final_matcher: class: Drupal\Core\Routing\UrlMatcher router.matcher: diff --git a/core/lib/Drupal/Core/Routing/RoutePreloader.php b/core/lib/Drupal/Core/Routing/RoutePreloader.php new file mode 100644 index 0000000..f95429c --- /dev/null +++ b/core/lib/Drupal/Core/Routing/RoutePreloader.php @@ -0,0 +1,135 @@ +routeProvider = $route_provider; + $this->state = $state; + $this->negotiation = $negotiation; + } + + /** + * Loads all non-admin routes right before the actual page is rendered. + * + * @param \Symfony\Component\HttpKernel\Event\KernelEvent $event + * The event to process. + */ + public function onRequest(KernelEvent $event) { + // Just preload on normal HTML pages, as they will display menu links. + if ($this->negotiation->getContentType($event->getRequest()) == 'html') { + $this->loadNonAdminRoutes(); + } + } + + /** + * Load all the non-admin routes at once. + */ + protected function loadNonAdminRoutes() { + if ($routes = $this->state->get('routing.non_admin_routes', array())) { + $this->routeProvider->getRoutesByNames($routes); + } + } + + /** + * Alters existing routes for a specific collection. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onAlterRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); + foreach ($collection->all() as $name => $route) { + if (strpos($route->getPath(), '/admin') === FALSE && + ($route->hasOption('_content') || $route->hasOption('_form') || $route->hasOption('_entity_form') | $route->hasOption('_entity_view') || $route->hasOption('_preload')) + ) { + $this->nonAdminRoutesOnRebuild[] = $name; + } + } + $this->nonAdminRoutesOnRebuild = array_unique($this->nonAdminRoutesOnRebuild); + } + + /** + * Store the non admin routes in state when the route building is finished. + * + * @param \Symfony\Component\EventDispatcher\Event $event + * The route finish event. + */ + public function onFinishedRoutes(Event $event) { + $this->state->set('routing.non_admin_routes', $this->nonAdminRoutesOnRebuild); + $this->nonAdminRoutesOnRebuild = array(); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Set a really low priority to catch as many as possible routes. + $events[RoutingEvents::ALTER] = array('onAlterRoutes', -1024); + $events[RoutingEvents::FINISHED] = array('onFinishedRoutes'); + // Load the routes before the controller is executed (which happens after + // the kernel request event). + $events[KernelEvents::REQUEST][] = array('onRequest'); + return $events; + } + +} diff --git a/core/modules/comment/comment.routing.yml b/core/modules/comment/comment.routing.yml index 6273eb7..11d2912 100644 --- a/core/modules/comment/comment.routing.yml +++ b/core/modules/comment/comment.routing.yml @@ -39,6 +39,8 @@ comment.permalink: defaults: _title: 'Comment permalink' _controller: '\Drupal\comment\Controller\CommentController::commentPermalink' + options: + _preload: TRUE requirements: _entity_access: 'comment.view' diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index f59aabc..4c209a5 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -10,6 +10,8 @@ user.logout: path: '/user/logout' defaults: _controller: '\Drupal\user\Controller\UserController::logout' + options: + _preload: TRUE requirements: _user_is_logged_in: 'TRUE' diff --git a/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php b/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php new file mode 100644 index 0000000..7b24ba2 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php @@ -0,0 +1,168 @@ + 'Route preloader', + 'description' => 'Tests the non admin routes preloader.', + 'group' => 'Routing', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); + $this->state = $this->getMock('\Drupal\Core\KeyValueStore\StateInterface'); + $this->negotiation = $this->getMockBuilder('\Drupal\Core\ContentNegotiation') + ->disableOriginalConstructor() + ->getMock(); + $this->preloader = new RoutePreloader($this->routeProvider, $this->state, $this->negotiation); + } + + /** + * Tests onAlterRoutes with just admin routes. + */ + public function testOnAlterRoutesWithAdminRoutes() { + $event = $this->getMockBuilder('Drupal\Core\Routing\RouteBuildEvent') + ->disableOriginalConstructor() + ->getMock(); + $route_collection = new RouteCollection(); + $route_collection->add('test', new Route('/admin/foo', array('_content' => 'Drupal\ExampleController'))); + $route_collection->add('test2', new Route('/admin/bar', array('_content' => 'Drupal\ExampleController'))); + $event->expects($this->once()) + ->method('getRouteCollection') + ->will($this->returnValue($route_collection)); + + $this->state->expects($this->once()) + ->method('set') + ->with('routing.non_admin_routes', array()); + $this->preloader->onAlterRoutes($event); + $this->preloader->onFinishedRoutes(new Event()); + } + + /** + * Tests onAlterRoutes with admin routes and non admin routes. + */ + public function testOnAlterRoutesWithNonAdminRoutes() { + $event = $this->getMockBuilder('Drupal\Core\Routing\RouteBuildEvent') + ->disableOriginalConstructor() + ->getMock(); + $route_collection = new RouteCollection(); + $route_collection->add('test', new Route('/admin/foo', array('_content' => 'Drupal\ExampleController'))); + $route_collection->add('test2', new Route('/bar', array('_content' => 'Drupal\ExampleController'))); + // Non content routes, like ajax callbacks should be ignored. + $route_collection->add('test3', new Route('/bar', array('_controller' => 'Drupal\ExampleController'))); + $event->expects($this->once()) + ->method('getRouteCollection') + ->will($this->returnValue($route_collection)); + + $this->state->expects($this->once()) + ->method('set') + ->with('routing.non_admin_routes', array('test2', 'test3')); + $this->preloader->onAlterRoutes($event); + $this->preloader->onFinishedRoutes(new Event()); + } + + /** + * Tests onRequest on a non html request. + */ + public function testOnRequestNonHtml() { + $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\KernelEvent') + ->disableOriginalConstructor() + ->getMock(); + $request = new Request(); + $event->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + $this->negotiation->expects($this->once()) + ->method('getContentType') + ->will($this->returnValue('non-html')); + + $this->routeProvider->expects($this->never()) + ->method('getRoutesByNames'); + $this->state->expects($this->never()) + ->method('get'); + + $this->preloader->onRequest($event); + } + + /** + * Tests onRequest on a html request. + */ + public function testOnRequestOnHtml() { + $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\KernelEvent') + ->disableOriginalConstructor() + ->getMock(); + $request = new Request(); + $event->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + $this->negotiation->expects($this->once()) + ->method('getContentType') + ->will($this->returnValue('html')); + + $this->routeProvider->expects($this->once()) + ->method('getRoutesByNames') + ->with(array('test2')); + $this->state->expects($this->once()) + ->method('get') + ->with('routing.non_admin_routes') + ->will($this->returnValue(array('test2'))); + + $this->preloader->onRequest($event); + } + +}