Issue #2058845 by dawehner, msonnabaum: Pre-load non-admin routes.
parent
428d0f0ad9
commit
3cd6aa897e
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue