diff --git a/core/core.services.yml b/core/core.services.yml index 2b31f93..e00369e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -278,6 +278,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..daa4130 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/RoutePreloader.php @@ -0,0 +1,132 @@ +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/') !== 0 && $route->getPath() != '/admin') { + $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/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php b/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php new file mode 100644 index 0000000..334e903 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Routing/RoutePreloaderTest.php @@ -0,0 +1,192 @@ + '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" appearing in the path. + */ + public function testOnAlterRoutesWithAdminPathNoAdminRoute() { + $event = $this->getMockBuilder('Drupal\Core\Routing\RouteBuildEvent') + ->disableOriginalConstructor() + ->getMock(); + $route_collection = new RouteCollection(); + $route_collection->add('test', new Route('/foo/admin/foo', array('_content' => 'Drupal\ExampleController'))); + $route_collection->add('test2', new Route('/bar/admin/bar', array('_content' => 'Drupal\ExampleController'))); + $route_collection->add('test3', new Route('/administrator/a', array('_content' => 'Drupal\ExampleController'))); + $route_collection->add('test4', new Route('/admin', 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('test', 'test2', 'test3')); + $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); + } + +}