Commit 3cd6aa89 authored by alexpott's avatar alexpott

Issue #2058845 by dawehner, msonnabaum: Pre-load non-admin routes.

parent 428d0f0a
......@@ -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:
......
<?php
/**
* @file
* Contains \Drupal\Core\Routing\RoutePreloader.
*/
namespace Drupal\Core\Routing;
use Drupal\Core\ContentNegotiation;
use Drupal\Core\KeyValueStore\StateInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\KernelEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Defines a class which preloads non-admin routes.
*
* On an actual site we want to avoid too many database queries so we build a
* list of all routes which most likely appear on the actual site, which are all
* HTML routes not starting with "/admin".
*/
class RoutePreloader implements EventSubscriberInterface {
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The state key value store.
*
* @var \Drupal\Core\KeyValueStore\StateInterface
*/
protected $state;
/**
* The content negotiation.
*
* @var \Drupal\Core\ContentNegotiation
*/
protected $negotiation;
/**
* Contains the non-admin routes while rebuilding the routes.
*
* @var array
*/
protected $nonAdminRoutesOnRebuild = array();
/**
* Constructs a new RoutePreloader.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\KeyValueStore\StateInterface $state
* The state key value store.
* @param \Drupal\Core\ContentNegotiation $negotiation
* The content negotiation.
*/
public function __construct(RouteProviderInterface $route_provider, StateInterface $state, ContentNegotiation $negotiation) {
$this->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;
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Routing\RoutePreloaderTest.
*/
namespace Drupal\Tests\Core\Routing;
use Drupal\Core\Routing\RoutePreloader;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Tests the non admin routes preloader.
*
* @see \Drupal\Core\Routing\RoutePreloader
*/
class RoutePreloaderTest extends UnitTestCase {
/**
* The mocked route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $routeProvider;
/**
* The mocked state.
*
* @var \Drupal\Core\KeyValueStore\StateInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $state;
/**
* The mocked content negotiator.
*
* @var \Drupal\Core\ContentNegotiation|\PHPUnit_Framework_MockObject_MockObject
*/
protected $negotiation;
/**
* The tested preloader.
*
* @var \Drupal\Core\Routing\RoutePreloader
*/
protected $preloader;
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => '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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment