Commit 7b5f7b67 authored by Dries's avatar Dries

Issue #1606794 by Crell, linclark, effulgentsia, katbailey, disasm, larowlan:...

Issue #1606794 by Crell, linclark, effulgentsia, katbailey, disasm, larowlan: Implement new routing system.
parents 6e78c49b ff6804ed
<?php
use Drupal\Component\Utility\NestedArray;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\Template\Attribute;
......@@ -6829,6 +6830,7 @@ function drupal_flush_all_caches() {
// Rebuild the menu router based on all rebuilt data.
// Important: This rebuild must happen last, so the menu router is guaranteed
// to be based on up to date information.
drupal_container()->get('router.builder')->rebuild();
menu_router_rebuild();
// Re-initialize the maintenance theme, if the current request attempted to
......
......@@ -15,6 +15,8 @@
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Drupal\Core\Database\Database;
/**
* Bundle class for mandatory core services.
*
......@@ -54,12 +56,25 @@ public function build(ContainerBuilder $container) {
->addArgument('slave');
$container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager');
$container->register('router.dumper', '\Drupal\Core\Routing\MatcherDumper')
->addArgument(new Reference('database'));
$container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder')
->addArgument(new Reference('router.dumper'));
// @todo Replace below lines with the commented out block below it when it's
// performant to do so: http://drupal.org/node/1706064.
$dispatcher = $container->get('dispatcher');
$matcher = new \Drupal\Core\LegacyUrlMatcher();
$matcher = new \Drupal\Core\Routing\ChainMatcher();
$matcher->add(new \Drupal\Core\LegacyUrlMatcher());
$nested = new \Drupal\Core\Routing\NestedMatcher();
$nested->setInitialMatcher(new \Drupal\Core\Routing\PathMatcher(Database::getConnection()));
$nested->addPartialMatcher(new \Drupal\Core\Routing\HttpMethodMatcher());
$nested->setFinalMatcher(new \Drupal\Core\Routing\FirstEntryFinalMatcher());
$matcher->add($nested, 5);
$content_negotation = new \Drupal\Core\ContentNegotiation();
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RouterListener($matcher));
$dispatcher->addSubscriber(new \Symfony\Component\HttpKernel\EventListener\RouterListener($matcher));
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\ViewSubscriber($content_negotation));
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\AccessSubscriber());
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\MaintenanceModeSubscriber());
......@@ -69,6 +84,7 @@ public function build(ContainerBuilder $container) {
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\FinishResponseSubscriber());
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RequestCloseSubscriber());
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber());
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RouteProcessorSubscriber());
$container->set('content_negotiation', $content_negotation);
$dispatcher->addSubscriber(\Drupal\Core\ExceptionController::getExceptionListener($container));
......
......@@ -36,11 +36,18 @@ class LegacyControllerSubscriber implements EventSubscriberInterface {
* The Event to process.
*/
public function onKernelControllerLegacy(FilterControllerEvent $event) {
$router_item = $event->getRequest()->attributes->get('drupal_menu_item');
$request = $event->getRequest();
$router_item = $request->attributes->get('drupal_menu_item');
$controller = $event->getController();
// This BC logic applies only to functions. Otherwise, skip it.
if (is_string($controller) && function_exists($controller)) {
// Flag this as a legacy request. We need to use this for subrequest
// handling so that we can treat older page callbacks and new routes
// differently.
// @todo Remove this line as soon as possible.
$request->attributes->set('_legacy', TRUE);
$new_controller = function() use ($router_item) {
return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
};
......
<?php
/**
* @file
* Definition of Drupal\Core\EventSubscriber\RouteProcessorSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* Listener to process request controller information.
*/
class RouteProcessorSubscriber implements EventSubscriberInterface {
/**
* Sets a default controller for a route if one was not specified.
*
* @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* Event that is created to create a response for a request.
*/
public function onRequestSetController(GetResponseEvent $event) {
$request = $event->getRequest();
if (!$request->attributes->has('_controller') && $request->attributes->has('_content')) {
$request->attributes->set('_controller', '\Drupal\Core\HtmlPageController::content');
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
// The RouterListener has priority 32, and we need to run after that.
$events[KernelEvents::REQUEST][] = array('onRequestSetController', 30);
return $events;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\EventSubscriber\RouterListener.
*/
namespace Drupal\Core\EventSubscriber;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\EventListener\RouterListener as SymfonyRouterListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* Drupal-specific Router listener.
*
* This is the bridge from the kernel to the UrlMatcher.
*/
class RouterListener extends SymfonyRouterListener {
/**
* The Matcher object for this listener.
*
* This property is private in the base class, so we have to hack around it.
*
* @var Symfony\Component\Router\Matcher\UrlMatcherInterface
*/
protected $urlMatcher;
/**
* The Logging object for this listener.
*
* This property is private in the base class, so we have to hack around it.
*
* @var Symfony\Component\HttpKernel\Log\LoggerInterface
*/
protected $logger;
public function __construct(UrlMatcherInterface $urlMatcher, LoggerInterface $logger = null) {
parent::__construct($urlMatcher, $logger);
$this->urlMatcher = $urlMatcher;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*
* This method is nearly identical to the parent, except it passes the
* $request->attributes->get('system_path') variable to the matcher.
* That is where Drupal stores its processed, de-aliased, and sanitized
* internal path. We also pass the full request object to the URL Matcher,
* since we want attributes to be available to the matcher and to controllers.
*/
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) {
$this->urlMatcher->getContext()->fromRequest($request);
$this->urlMatcher->setRequest($request);
}
if ($request->attributes->has('_controller')) {
// Routing is already done.
return;
}
// Add attributes based on the path info (routing).
try {
$parameters = $this->urlMatcher->match($request->attributes->get('system_path'));
if (null !== $this->logger) {
$this->logger->info(sprintf('Matched route "%s" (parameters: %s)', $parameters['_route'], $this->parametersToString($parameters)));
}
$request->attributes->add($parameters);
unset($parameters['_route']);
unset($parameters['_controller']);
$request->attributes->set('_route_params', $parameters);
}
catch (ResourceNotFoundException $e) {
$message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo());
throw new NotFoundHttpException($message, $e);
}
catch (MethodNotAllowedException $e) {
$message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getPathInfo(), strtoupper(implode(', ', $e->getAllowedMethods())));
throw new MethodNotAllowedHttpException($e->getAllowedMethods(), $message, $e);
}
}
}
......@@ -10,6 +10,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -46,13 +47,43 @@ public function onView(GetResponseForControllerResultEvent $event) {
$request = $event->getRequest();
$method = 'on' . $this->negotiation->getContentType($request);
if (method_exists($this, $method)) {
$event->setResponse($this->$method($event));
// For a master request, we process the result and wrap it as needed.
// For a subrequest, all we want is the string value. We assume that
// is just an HTML string from a controller, so wrap that into a response
// object. The subrequest's response will get dissected and placed into
// the larger page as needed.
if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
$method = 'on' . $this->negotiation->getContentType($request);
if (method_exists($this, $method)) {
$event->setResponse($this->$method($event));
}
else {
$event->setResponse(new Response('Unsupported Media Type', 415));
}
}
elseif ($request->attributes->get('_legacy')) {
// This is an old hook_menu-based subrequest, which means we assume
// the body is supposed to be the complete page.
$page_result = $event->getControllerResult();
if (!is_array($page_result)) {
$page_result = array(
'#markup' => $page_result,
);
}
$event->setResponse(new Response(drupal_render_page($page_result)));
}
else {
$event->setResponse(new Response('Unsupported Media Type', 415));
// This is a new-style Symfony-esque subrequest, which means we assume
// the body is not supposed to be a complete page but just a page
// fragment.
$page_result = $event->getControllerResult();
if (!is_array($page_result)) {
$page_result = array(
'#markup' => $page_result,
);
}
$event->setResponse(new Response(drupal_render($page_result)));
}
}
......
<?php
/**
* @file
* Definition of Drupal\Core\HtmlPageController.
*/
namespace Drupal\Core;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Default controller for most HTML pages.
*/
class HtmlPageController implements ContainerAwareInterface {
/**
* The injection container for this object.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Injects the service container used by this object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The service container this object should use.
*/
public function setContainer(ContainerInterface $container = NULL) {
$this->container = $container;
}
/**
* Controller method for generic HTML pages.
*
* @param Request $request
* The request object.
* @param callable $_content
* The body content callable that contains the body region of this page.
*
* @return \Symfony\Component\HttpFoundation\Response
* A response object.
*/
public function content(Request $request, $_content) {
// @todo When we have a Generator, we can replace the forward() call with
// a render() call, which would handle ESI and hInclude as well. That will
// require an _internal route. For examples, see:
// https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/internal.xml
// https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/InternalController.php
$attributes = $request->attributes;
$controller = $_content;
// We need to clean off the derived information and such so that the
// subrequest can be processed properly without leaking data through.
$attributes->remove('system_path');
$attributes->remove('_content');
$response = $this->container->get('http_kernel')->forward($controller, $attributes->all(), $request->query->all());
$page_content = $response->getContent();
return new Response(drupal_render_page($page_content));
}
}
......@@ -9,13 +9,14 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use Symfony\Component\Routing\RequestContextAwareInterface;
use Symfony\Component\Routing\RequestContext;
/**
* UrlMatcher matches URL based on a set of routes.
*/
class LegacyUrlMatcher implements UrlMatcherInterface {
class LegacyUrlMatcher implements RequestMatcherInterface, RequestContextAwareInterface {
/**
* The request context for this matcher.
......@@ -98,8 +99,8 @@ public function getRequest() {
*
* @api
*/
public function match($pathinfo) {
if ($router_item = $this->matchDrupalItem($pathinfo)) {
public function matchRequest(Request $request) {
if ($router_item = $this->matchDrupalItem($request->attributes->get('system_path'))) {
$ret = $this->convertDrupalItem($router_item);
// Stash the router item in the attributes while we're transitioning.
$ret['drupal_menu_item'] = $router_item;
......
<?php
/**
* @file
* Definition of Drupal\Core\Routing\ChainMatcher.
*/
namespace Drupal\Core\Routing;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\RequestContextAwareInterface;
use Symfony\Component\Routing\RequestContext;
/**
* Aggregates multiple matchers together in series.
*
* The RequestContext is entirely unused. It's included only to satisfy the
* interface needed for RouterListener. Hopefully we can remove it later.
*/
class ChainMatcher implements RequestMatcherInterface, RequestContextAwareInterface {
/**
* Array of RequestMatcherInterface objects to be checked in order.
*
* @var array
*/
protected $matchers = array();
/**
* Array of RequestMatcherInterface objects, sorted.
*
* @var type
*/
protected $sortedMatchers = array();
/**
* The request context for this matcher.
*
* This is unused. It's just to satisfy the interface.
*
* @var Symfony\Component\Routing\RequestContext
*/
protected $context;
/**
* Constructor.
*/
public function __construct() {
// We will not actually use this object, but it's needed to conform to
// the interface.
$this->context = new RequestContext();
}
/**
* Sets the request context.
*
* This method is just to satisfy the interface, and is largely vestigial.
* The request context object does not contain the information we need, so
* we will use the original request object.
*
* @param Symfony\Component\Routing\RequestContext $context
* The context.
*/
public function setContext(RequestContext $context) {
$this->context = $context;
}
/**
* Gets the request context.
*
* This method is just to satisfy the interface, and is largely vestigial.
* The request context object does not contain the information we need, so
* we will use the original request object.
*
* @return Symfony\Component\Routing\RequestContext
* The context.
*/
public function getContext() {
return $this->context;
}
/**
* Matches a request against all queued matchers.
*
* @param Request $request The request to match
*
* @return array An array of parameters
*
* @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException
* If no matching resource could be found
* @throws \Symfony\Component\Routing\Exception\MethodNotAllowedException
* If a matching resource was found but the request method is not allowed
*/
public function matchRequest(Request $request) {
$methodNotAllowed = null;
foreach ($this->all() as $matcher) {
try {
return $matcher->matchRequest($request);
} catch (ResourceNotFoundException $e) {
// Needs special care
} catch (MethodNotAllowedException $e) {
$methodNotAllowed = $e;
}
}
throw $methodNotAllowed ?: new ResourceNotFoundException("None of the matchers in the chain matched this request.");
}
/**
* Adds a Matcher to the index.
*
* @param MatcherInterface $matcher
* The matcher to add.
* @param int $priority
* (optional) The priority of the matcher. Higher number matchers will be checked
* first. Default to 0.
*/
public function add(RequestMatcherInterface $matcher, $priority = 0) {
if (empty($this->matchers[$priority])) {
$this->matchers[$priority] = array();
}
$this->matchers[$priority][] = $matcher;
$this->sortedMatchers = array();
}
/**
* Sorts the matchers and flattens them.
*
* @return array
* An array of RequestMatcherInterface objects.
*/
public function all() {
if (empty($this->sortedMatchers)) {
$this->sortedMatchers = $this->sortMatchers();
}
return $this->sortedMatchers;
}
/**
* Sort matchers by priority.
*
* The highest priority number is the highest priority (reverse sorting).
*
* @return \Symfony\Component\Routing\RequestMatcherInterface[]
* An array of Matcher objects in the order they should be used.
*/
protected function sortMatchers() {
$sortedMatchers = array();
krsort($this->matchers);
foreach ($this->matchers as $matchers) {
$sortedMatchers = array_merge($sortedMatchers, $matchers);
}
return $sortedMatchers;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Routing\CompiledRoute.
*/
namespace Drupal\Core\Routing;
use Symfony\Component\Routing\Route;
/**
* Description of CompiledRoute
*/
class CompiledRoute {
/**
* The fitness of this route.
*
* @var int
*/
protected $fit;
/**
* The pattern outline of this route.
*
* @var string
*/
protected $patternOutline;
/**
* The number of parts in the path of this route.
*
* @var int
*/
protected $numParts;
/**
* The Route object of which this object is the compiled version.
*
* @var Symfony\Component\Routing\Route
*/
protected $route;
/**
* The regular expression to match placeholders out of this path.
*
* @var string
*/
protected $regex;
/**
* Constructs a new CompiledRoute object.
*
* @param \Symfony\Component\Routing\Route $route
* A original Route instance.
* @param int $fit
* The fitness of the route.
* @param string $fit
* The pattern outline for this route.
* @param int $num_parts
* The number of parts in the path.
* @param string $regex
* The regular expression to match placeholders out of this path.
*/
public function __construct(Route $route, $fit, $pattern_outline, $num_parts, $regex) {
$this->route = $route;
$this->fit = $fit;
$this->patternOutline = $pattern_outline;
$this->numParts = $num_parts;
$this->regex = $regex;
}
/**
* Returns the fit of this route.
*
* See RouteCompiler for a definition of how the fit is calculated.
*
* @return int
* The fit of the route.
*/
public function getFit() {
return $this->fit;
}
/**
* Returns the number of parts in this route's path.
*
* The string "foo/bar/baz" has 3 parts, regardless of how many of them are
* placeholders.
*
* @return int
* The number of parts in the path.
*/
public function getNumParts() {
return $this->numParts;
}
/**
* Returns the pattern outline of this route.
*
* The pattern outline of a route is the path pattern of the route, but
* normalized such that all placeholders are replaced with %.
*
* @return string
* The normalized path pattern.
*/
public function getPatternOutline() {
return $this->patternOutline;
}
/**
* Returns the placeholder regex.
*
* @return string
* The regex to locate placeholders in this pattern.
*/
public function getRegex() {
return $this->regex;
}
/**
* Returns the Route instance.
*
* @return Route
* A Route instance.
*/
public function getRoute() {
return $this->route;
}
/**
* Returns the pattern.
*
* @return string
* The pattern.
*/
public function getPattern() {
return $this->route->getPattern();
}
/**
* Returns the options.
*
* @return array
* The options.
*/
public function getOptions() {
return $this->route->getOptions();
}
/**
* Returns the defaults.
*
* @return array
* The defaults.
*/
public function getDefaults() {