summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDries2012-10-02 01:47:15 (GMT)
committerDries2012-10-02 01:47:15 (GMT)
commit7b5f7b672f275e3b9c2122a2146b8752499a2af0 (patch)
tree4db96b3f5e41721dbfd2616c1e4f62e51ac551e1
parent6e78c49b5ad1dffd755d3f70a486153c92ad3ebf (diff)
parentff6804ed8229ab2911edab603c1c155bbfd3be43 (diff)
Issue #1606794 by Crell, linclark, effulgentsia, katbailey, disasm, larowlan: Implement new routing system.
-rw-r--r--core/includes/common.inc4
-rw-r--r--core/lib/Drupal/Core/CoreBundle.php20
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php9
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php51
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/RouterListener.php96
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php41
-rw-r--r--core/lib/Drupal/Core/HtmlPageController.php69
-rw-r--r--core/lib/Drupal/Core/LegacyUrlMatcher.php9
-rw-r--r--core/lib/Drupal/Core/Routing/ChainMatcher.php165
-rw-r--r--core/lib/Drupal/Core/Routing/CompiledRoute.php172
-rw-r--r--core/lib/Drupal/Core/Routing/FinalMatcherInterface.php39
-rw-r--r--core/lib/Drupal/Core/Routing/FirstEntryFinalMatcher.php82
-rw-r--r--core/lib/Drupal/Core/Routing/HttpMethodMatcher.php58
-rw-r--r--core/lib/Drupal/Core/Routing/InitialMatcherInterface.php27
-rw-r--r--core/lib/Drupal/Core/Routing/MatcherDumper.php169
-rw-r--r--core/lib/Drupal/Core/Routing/NestedMatcher.php199
-rw-r--r--core/lib/Drupal/Core/Routing/NestedMatcherInterface.php58
-rw-r--r--core/lib/Drupal/Core/Routing/PartialMatcher.php40
-rw-r--r--core/lib/Drupal/Core/Routing/PartialMatcherInterface.php39
-rw-r--r--core/lib/Drupal/Core/Routing/PathMatcher.php134
-rw-r--r--core/lib/Drupal/Core/Routing/RouteBuilder.php54
-rw-r--r--core/lib/Drupal/Core/Routing/RouteCompiler.php248
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php2
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php112
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/FirstEntryFinalMatcherTest.php112
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php105
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php144
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php37
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php59
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php65
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php302
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php61
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php89
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php185
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php10
-rw-r--r--core/modules/system/system.api.php45
-rw-r--r--core/modules/system/system.install126
-rw-r--r--core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php33
-rw-r--r--core/modules/system/tests/modules/router_test/router_test.info6
-rw-r--r--core/modules/system/tests/modules/router_test/router_test.module34
-rw-r--r--core/modules/user/user.pages.inc6
-rw-r--r--core/update.php12
42 files changed, 3213 insertions, 115 deletions
diff --git a/core/includes/common.inc b/core/includes/common.inc
index aea8529..6879def 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -1,8 +1,9 @@
<?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
diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php
index 4dca804..c1abfe5 100644
--- a/core/lib/Drupal/Core/CoreBundle.php
+++ b/core/lib/Drupal/Core/CoreBundle.php
@@ -15,6 +15,8 @@ use Symfony\Component\DependencyInjection\Scope;
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 @@ class CoreBundle extends Bundle
->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 @@ class CoreBundle extends Bundle
$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));
diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php
index f088f7e..7009eff 100644
--- a/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/LegacyControllerSubscriber.php
@@ -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']);
};
diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php
new file mode 100644
index 0000000..61c7788
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/RouteProcessorSubscriber.php
@@ -0,0 +1,51 @@
+<?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;
+ }
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/RouterListener.php b/core/lib/Drupal/Core/EventSubscriber/RouterListener.php
deleted file mode 100644
index 7b158fd..0000000
--- a/core/lib/Drupal/Core/EventSubscriber/RouterListener.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?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);
- }
- }
-}
diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
index 637d48d..e88525e 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
@@ -10,6 +10,7 @@ namespace Drupal\Core\EventSubscriber;
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 @@ class ViewSubscriber implements EventSubscriberInterface {
$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)));
}
}
diff --git a/core/lib/Drupal/Core/HtmlPageController.php b/core/lib/Drupal/Core/HtmlPageController.php
new file mode 100644
index 0000000..7d14ac0
--- /dev/null
+++ b/core/lib/Drupal/Core/HtmlPageController.php
@@ -0,0 +1,69 @@
+<?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));
+ }
+}
diff --git a/core/lib/Drupal/Core/LegacyUrlMatcher.php b/core/lib/Drupal/Core/LegacyUrlMatcher.php
index 8828f36..48987a9 100644
--- a/core/lib/Drupal/Core/LegacyUrlMatcher.php
+++ b/core/lib/Drupal/Core/LegacyUrlMatcher.php
@@ -9,13 +9,14 @@ namespace Drupal\Core;
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 @@ class LegacyUrlMatcher implements UrlMatcherInterface {
*
* @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;
diff --git a/core/lib/Drupal/Core/Routing/ChainMatcher.php b/core/lib/Drupal/Core/Routing/ChainMatcher.php
new file mode 100644
index 0000000..23410bf
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/ChainMatcher.php
@@ -0,0 +1,165 @@
+<?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;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/CompiledRoute.php b/core/lib/Drupal/Core/Routing/CompiledRoute.php
new file mode 100644
index 0000000..c5cdde8
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/CompiledRoute.php
@@ -0,0 +1,172 @@
+<?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() {
+ return $this->route->getDefaults();
+ }
+
+ /**
+ * Returns the requirements.
+ *
+ * @return array
+ * The requirements.
+ */
+ public function getRequirements() {
+ return $this->route->getRequirements();
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php b/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php
new file mode 100644
index 0000000..8b85c21
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\FinalMatcherInterface.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * A FinalMatcher returns only one route from a collection of candidate routes.
+ */
+interface FinalMatcherInterface {
+
+ /**
+ * Sets the route collection this matcher should use.
+ *
+ * @param \Symfony\Component\Routing\RouteCollection $collection
+ * The collection against which to match.
+ *
+ * @return \Drupal\Core\Routing\FinalMatcherInterface
+ * The current matcher.
+ */
+ public function setCollection(RouteCollection $collection);
+
+ /**
+ * Matches a request against multiple routes.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * A Request object against which to match.
+ *
+ * @return array
+ * An array of parameters.
+ */
+ public function matchRequest(Request $request);
+}
diff --git a/core/lib/Drupal/Core/Routing/FirstEntryFinalMatcher.php b/core/lib/Drupal/Core/Routing/FirstEntryFinalMatcher.php
new file mode 100644
index 0000000..45d0888
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/FirstEntryFinalMatcher.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\FirstEntryFinalMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Final matcher that simply returns the first item in the remaining routes.
+ *
+ * This class simply matches the first remaining route.
+ */
+class FirstEntryFinalMatcher implements FinalMatcherInterface {
+
+ /**
+ * The RouteCollection this matcher should match against.
+ *
+ * @var RouteCollection
+ */
+ protected $routes;
+
+ /**
+ * Sets the route collection this matcher should use.
+ *
+ * @param \Symfony\Component\Routing\RouteCollection $collection
+ * The collection against which to match.
+ *
+ * @return \Drupal\Core\Routing\FinalMatcherInterface
+ * The current matcher.
+ */
+ public function setCollection(RouteCollection $collection) {
+ $this->routes = $collection;
+
+ return $this;
+ }
+
+ /**
+ * Implements Drupal\Core\Routing\FinalMatcherInterface::matchRequest().
+ */
+ public function matchRequest(Request $request) {
+ // Return whatever the first route in the collection is.
+ foreach ($this->routes as $name => $route) {
+ $path = '/' . $request->attributes->get('system_path');
+
+ $route->setOption('compiler_class', '\Drupal\Core\Routing\RouteCompiler');
+ $compiled = $route->compile();
+
+ preg_match($compiled->getRegex(), $path, $matches);
+
+ return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $name));
+ }
+ }
+
+ /**
+ * Get merged default parameters.
+ *
+ * @param array $params
+ * The parameters.
+ * @param array $defaults
+ * The defaults.
+ *
+ * @return array
+ * Merged default parameters.
+ */
+ protected function mergeDefaults($params, $defaults) {
+ $parameters = $defaults;
+ foreach ($params as $key => $value) {
+ if (!is_int($key)) {
+ $parameters[$key] = $value;
+ }
+ }
+
+ return $parameters;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php b/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php
new file mode 100644
index 0000000..5064353
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\HttpMethodMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+
+/**
+ * This class filters routes based on their HTTP Method.
+ */
+class HttpMethodMatcher extends PartialMatcher {
+
+ /**
+ * Matches a request against multiple routes.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * A Request object against which to match.
+ *
+ * @return \Symfony\Component\Routing\RouteCollection
+ * A RouteCollection of matched routes.
+ */
+ public function matchRequestPartial(Request $request) {
+ $possible_methods = array();
+
+ $method = $request->getMethod();
+
+ $collection = new RouteCollection();
+
+ foreach ($this->routes->all() as $name => $route) {
+ // _method could be a |-delimited list of allowed methods, or null. If
+ // null, we accept any method.
+ $allowed_methods = array_filter(explode('|', strtoupper($route->getRequirement('_method'))));
+ if (empty($allowed_methods) || in_array($method, $allowed_methods)) {
+ $collection->add($name, $route);
+ }
+ else {
+ // Build a list of methods that would have matched. Note that we only
+ // need to do this if a route doesn't match, because if even one route
+ // passes then we'll never throw the exception that needs this array.
+ $possible_methods += $allowed_methods;
+ }
+ }
+
+ if (!count($collection->all())) {
+ throw new MethodNotAllowedException(array_unique($possible_methods));
+ }
+
+ return $collection;
+ }
+
+}
+
diff --git a/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php b/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php
new file mode 100644
index 0000000..53bc8e7
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\InitialMatcherInterface.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A PartialMatcher works like a UrlMatcher, but will return multiple candidate routes.
+ */
+interface InitialMatcherInterface {
+
+ /**
+ * Matches a request against multiple routes.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * A Request object against which to match.
+ *
+ * @return \Symfony\Component\Routing\RouteCollection
+ * A RouteCollection of matched routes.
+ */
+ public function matchRequestPartial(Request $request);
+}
diff --git a/core/lib/Drupal/Core/Routing/MatcherDumper.php b/core/lib/Drupal/Core/Routing/MatcherDumper.php
new file mode 100644
index 0000000..48b2d8c
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/MatcherDumper.php
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\MatcherDumper.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Dumps Route information to a database table.
+ */
+class MatcherDumper implements MatcherDumperInterface {
+
+ /**
+ * The maximum number of path elements for a route pattern;
+ */
+ const MAX_PARTS = 9;
+
+ /**
+ * The database connection to which to dump route information.
+ *
+ * @var Drupal\Core\Database\Connection
+ */
+ protected $connection;
+
+ /**
+ * The routes to be dumped.
+ *
+ * @var Symfony\Component\Routing\RouteCollection
+ */
+ protected $routes;
+
+ /**
+ * The name of the SQL table to which to dump the routes.
+ *
+ * @var string
+ */
+ protected $tableName;
+
+ /**
+ * Construct the MatcherDumper.
+ *
+ * @param Drupal\Core\Database\Connection $connection
+ * The database connection which will be used to store the route
+ * information.
+ * @param string $table
+ * (optional) The table to store the route info in. Defaults to 'router'.
+ */
+ public function __construct(Connection $connection, $table = 'router') {
+ $this->connection = $connection;
+
+ $this->tableName = $table;
+ }
+
+ /**
+ * Adds additional routes to be dumped.
+ *
+ * @param Symfony\Component\Routing\RouteCollection $routes
+ * A collection of routes to add to this dumper.
+ */
+ public function addRoutes(RouteCollection $routes) {
+ if (empty($this->routes)) {
+ $this->routes = $routes;
+ }
+ else {
+ $this->routes->addCollection($routes);
+ }
+ }
+
+ /**
+ * Dumps a set of routes to the router table in the database.
+ *
+ * Available options:
+ * - route_set: The route grouping that is being dumped. All existing
+ * routes with this route set will be deleted on dump.
+ * - base_class: The base class name.
+ *
+ * @param array $options
+ * An array of options.
+ */
+ public function dump(array $options = array()) {
+ $options += array(
+ 'route_set' => '',
+ );
+
+ // Convert all of the routes into database records.
+ $insert = $this->connection->insert($this->tableName)->fields(array(
+ 'name',
+ 'route_set',
+ 'fit',
+ 'pattern',
+ 'pattern_outline',
+ 'number_parts',
+ 'route',
+ ));
+
+ foreach ($this->routes as $name => $route) {
+ $route->setOption('compiler_class', '\Drupal\Core\Routing\RouteCompiler');
+ $compiled = $route->compile();
+ $values = array(
+ 'name' => $name,
+ 'route_set' => $options['route_set'],
+ 'fit' => $compiled->getFit(),
+ 'pattern' => $compiled->getPattern(),
+ 'pattern_outline' => $compiled->getPatternOutline(),
+ 'number_parts' => $compiled->getNumParts(),
+ 'route' => serialize($route),
+ );
+ $insert->values($values);
+ }
+
+ // Delete any old records in this route set first, then insert the new ones.
+ // That avoids stale data. The transaction makes it atomic to avoid
+ // unstable router states due to random failures.
+ $txn = $this->connection->startTransaction();
+
+ $this->connection->delete($this->tableName)
+ ->condition('route_set', $options['route_set'])
+ ->execute();
+
+ $insert->execute();
+
+ // We want to reuse the dumper for multiple route sets, so on dump, flush
+ // the queued routes.
+ $this->routes = NULL;
+
+ // Transaction ends here.
+ }
+
+ /**
+ * Gets the routes to match.
+ *
+ * @return \Symfony\Component\Routing\RouteCollection
+ * A RouteCollection instance representing all routes currently in the
+ * dumper.
+ */
+ public function getRoutes() {
+ return $this->routes;
+ }
+
+ /**
+ * Determines the fitness of the provided path.
+ *
+ * @param string $path
+ * The path whose fitness we want.
+ *
+ * @return int
+ * The fitness of the path, as an integer.
+ */
+ public function getFit($path) {
+ $fit = 0;
+
+ $parts = explode('/', $path, static::MAX_PARTS);
+ foreach ($parts as $k => $part) {
+ if (strpos($part, '{') === FALSE) {
+ $fit |= 1 << ($slashes - $k);
+ }
+ }
+
+ return $fit;
+ }
+}
diff --git a/core/lib/Drupal/Core/Routing/NestedMatcher.php b/core/lib/Drupal/Core/Routing/NestedMatcher.php
new file mode 100644
index 0000000..ff53715
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/NestedMatcher.php
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\NestedMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * The nested matcher layers multiple partial matchers together.
+ */
+class NestedMatcher implements NestedMatcherInterface {
+
+ /**
+ * The final matcher.
+ *
+ * @var Symfony\Component\Routing\Matcher\RequestMatcherInterface
+ */
+ protected $finalMatcher;
+
+ /**
+ * An array of PartialMatchers.
+ *
+ * @var array
+ */
+ protected $partialMatchers = array();
+
+ /**
+ * Array of PartialMatcherInterface objects, sorted.
+ *
+ * @var type
+ */
+ protected $sortedMatchers = array();
+
+ /**
+ * The initial matcher to match against.
+ *
+ * @var Drupal\core\Routing\InitialMatcherInterface
+ */
+ protected $initialMatcher;
+
+ /**
+ * The request context.
+ *
+ * @var Symfony\Component\Routing\RequestContext
+ */
+ protected $context;
+
+ /**
+ * Adds a partial matcher to the matching plan.
+ *
+ * Partial matchers will be run in the order in which they are added.
+ *
+ * @param \Drupal\Core\Routing\PartialMatcherInterface $matcher
+ * A partial matcher.
+ * @param int $priority
+ * (optional) The priority of the matcher. Higher number matchers will be checked
+ * first. Default to 0.
+ *
+ * @return NestedMatcherInterface
+ * The current matcher.
+ */
+ public function addPartialMatcher(PartialMatcherInterface $matcher, $priority = 0) {
+ if (empty($this->matchers[$priority])) {
+ $this->matchers[$priority] = array();
+ }
+
+ $this->matchers[$priority][] = $matcher;
+ $this->sortedMatchers = array();
+ }
+
+ /**
+ * Sets the final matcher for the matching plan.
+ *
+ * @param \Drupal\Core\Routing\FinalMatcherInterface $final
+ * The matcher that will be called last to ensure only a single route is
+ * found.
+ *
+ * @return \Drupal\Core\Routing\NestedMatcherInterface
+ * The current matcher.
+ */
+ public function setFinalMatcher(FinalMatcherInterface $final) {
+ $this->finalMatcher = $final;
+
+ return $this;
+ }
+
+ /**
+ * Sets the first matcher for the matching plan.
+ *
+ * Partial matchers will be run in the order in which they are added.
+ *
+ * @param \Drupal\Core\Routing\InitialMatcherInterface $matcher
+ * An initial matcher. It is responsible for its own configuration and
+ * initial route collection
+ *
+ * @return \Drupal\Core\Routing\NestedMatcherInterface
+ * The current matcher.
+ */
+ public function setInitialMatcher(InitialMatcherInterface $initial) {
+ $this->initialMatcher = $initial;
+
+ return $this;
+ }
+
+ /**
+ * Tries to match a request with a set of routes.
+ *
+ * If the matcher can not find information, it must throw one of the
+ * exceptions documented below.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The request to match.
+ *
+ * @return array
+ * An array of parameters.
+ *
+ * @throws ResourceNotFoundException
+ * If no matching resource could be found.
+ * @throws MethodNotAllowedException
+ * If a matching resource was found but the request method is not allowed.
+ */
+ public function matchRequest(Request $request) {
+ $collection = $this->initialMatcher->matchRequestPartial($request);
+
+ foreach ($this->getPartialMatchers() as $matcher) {
+ if ($collection) {
+ $matcher->setCollection($collection);
+ }
+ $collection = $matcher->matchRequestPartial($request);
+ }
+
+ $attributes = $this->finalMatcher->setCollection($collection)->matchRequest($request);
+
+ return $attributes;
+ }
+
+ /**
+ * Sorts the matchers and flattens them.
+ *
+ * @return array
+ * An array of RequestMatcherInterface objects.
+ */
+ public function getPartialMatchers() {
+ 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;
+ }
+
+ /**
+ * Sets the request context.
+ *
+ * This method is unused. It is here only to satisfy the interface.
+ *
+ * @param \Symfony\Component\Routing\RequestContext $context
+ * The context
+ */
+ public function setContext(RequestContext $context) {
+ $this->context = $context;
+ }
+
+ /**
+ * Gets the request context.
+ *
+ * This method is unused. It is here only to satisfy the interface.
+ *
+ * @return \Symfony\Component\Routing\RequestContext
+ * The context
+ */
+ public function getContext() {
+ return $this->context;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php b/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php
new file mode 100644
index 0000000..6ae0995
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\NestedMatcherInterface.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
+
+/**
+ * A NestedMatcher allows for multiple-stage resolution of a route.
+ */
+interface NestedMatcherInterface extends RequestMatcherInterface {
+
+ /**
+ * Sets the first matcher for the matching plan.
+ *
+ * Partial matchers will be run in the order in which they are added.
+ *
+ * @param \Drupal\Core\Routing\InitialMatcherInterface $matcher
+ * An initial matcher. It is responsible for its own configuration and
+ * initial route collection
+ *
+ * @return \Drupal\Core\Routing\NestedMatcherInterface
+ * The current matcher.
+ */
+ public function setInitialMatcher(InitialMatcherInterface $initial);
+
+ /**
+ * Adds a partial matcher to the matching plan.
+ *
+ * Partial matchers will be run in the order in which they are added.
+ *
+ * @param \Drupal\Core\Routing\PartialMatcherInterface $matcher
+ * A partial matcher.
+ * @param int $priority
+ * (optional) The priority of the matcher. Higher number matchers will be checked
+ * first. Default to 0.
+ *
+ * @return NestedMatcherInterface
+ * The current matcher.
+ */
+ public function addPartialMatcher(PartialMatcherInterface $matcher, $priority = 0);
+
+ /**
+ * Sets the final matcher for the matching plan.
+ *
+ * @param \Drupal\Core\Routing\FinalMatcherInterface $final
+ * The matcher that will be called last to ensure only a single route is
+ * found.
+ *
+ * @return \Drupal\Core\Routing\NestedMatcherInterface
+ * The current matcher.
+ */
+ public function setFinalMatcher(FinalMatcherInterface $final);
+}
diff --git a/core/lib/Drupal/Core/Routing/PartialMatcher.php b/core/lib/Drupal/Core/Routing/PartialMatcher.php
new file mode 100644
index 0000000..d28f9ac
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/PartialMatcher.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\PartialMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Utility base class for partial matchers.
+ */
+abstract class PartialMatcher implements PartialMatcherInterface {
+
+ /**
+ * The RouteCollection this matcher should match against.
+ *
+ * @var \Symfony\Component\Routing\RouteCollection
+ */
+ protected $routes;
+
+ /**
+ * Sets the route collection this matcher should use.
+ *
+ * @param \Symfony\Component\Routing\RouteCollection $collection
+ * The collection against which to match.
+ *
+ * @return \Drupal\Core\Routing\PartialMatcherInterface
+ * The current matcher.
+ */
+ public function setCollection(RouteCollection $collection) {
+ $this->routes = $collection;
+
+ return $this;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php b/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php
new file mode 100644
index 0000000..0d180c6
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\PathMatcherInterface.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * A PartialMatcher works like a UrlMatcher, but will return multiple candidate routes.
+ */
+interface PartialMatcherInterface {
+
+ /**
+ * Sets the route collection this matcher should use.
+ *
+ * @param \Symfony\Component\Routing\RouteCollection $collection
+ * The collection against which to match.
+ *
+ * @return \Drupal\Core\Routing\PartialMatcherInterface
+ * The current matcher.
+ */
+ public function setCollection(RouteCollection $collection);
+
+ /**
+ * Matches a request against multiple routes.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * A Request object against which to match.
+ *
+ * @return \Symfony\Component\Routing\RouteCollection
+ * A RouteCollection of matched routes.
+ */
+ public function matchRequestPartial(Request $request);
+}
diff --git a/core/lib/Drupal/Core/Routing/PathMatcher.php b/core/lib/Drupal/Core/Routing/PathMatcher.php
new file mode 100644
index 0000000..f037d9a
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/PathMatcher.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\PathMatcher.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Initial matcher to match a route against a built database, by path.
+ */
+class PathMatcher implements InitialMatcherInterface {
+
+ /**
+ * The database connection from which to read route information.
+ *
+ * @var Drupal\Core\Database\Connection
+ */
+ protected $connection;
+
+ /**
+ * The name of the SQL table from which to read the routes.
+ *
+ * @var string
+ */
+ protected $tableName;
+
+ /**
+ * Constructs a new PathMatcher.
+ *
+ * @param \Drupal\Core\Database\Connection $connection
+ * A database connection object.
+ * @param string $table
+ * The table in the database to use for matching.
+ */
+ public function __construct(Connection $connection, $table = 'router') {
+ $this->connection = $connection;
+ $this->tableName = $table;
+ }
+
+ /**
+ * Matches a request against multiple routes.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * A Request object against which to match.
+ *
+ * @return \Symfony\Component\Routing\RouteCollection
+ * A RouteCollection of matched routes.
+ */
+ public function matchRequestPartial(Request $request) {
+
+ $path = rtrim($request->getPathInfo(), '/');
+
+ $parts = array_slice(array_filter(explode('/', $path)), 0, MatcherDumper::MAX_PARTS);
+
+ $ancestors = $this->getCandidateOutlines($parts);
+
+ $routes = $this->connection->query("SELECT name, route FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE pattern_outline IN (:patterns) ORDER BY fit", array(
+ ':patterns' => $ancestors,
+ ))
+ ->fetchAllKeyed();
+
+ $collection = new RouteCollection();
+ foreach ($routes as $name => $route) {
+ $route = unserialize($route);
+ if (preg_match($route->compile()->getRegex(), $path, $matches)) {
+ $collection->add($name, $route);
+ }
+ }
+
+ if (!count($collection)) {
+ throw new ResourceNotFoundException();
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Returns an array of path pattern outlines that could match the path parts.
+ *
+ * @param array $parts
+ * The parts of the path for which we want candidates.
+ *
+ * @return array
+ * An array of outlines that could match the specified path parts.
+ */
+ public function getCandidateOutlines(array $parts) {
+ $number_parts = count($parts);
+ $ancestors = array();
+ $length = $number_parts - 1;
+ $end = (1 << $number_parts) - 1;
+
+ // The highest possible mask is a 1 bit for every part of the path. We will
+ // check every value down from there to generate a possible outline.
+ $masks = range($end, pow($number_parts - 1, 2));
+
+ // Only examine patterns that actually exist as router items (the masks).
+ foreach ($masks as $i) {
+ if ($i > $end) {
+ // Only look at masks that are not longer than the path of interest.
+ continue;
+ }
+ elseif ($i < (1 << $length)) {
+ // We have exhausted the masks of a given length, so decrease the length.
+ --$length;
+ }
+ $current = '';
+ for ($j = $length; $j >= 0; $j--) {
+ // Check the bit on the $j offset.
+ if ($i & (1 << $j)) {
+ // Bit one means the original value.
+ $current .= $parts[$length - $j];
+ }
+ else {
+ // Bit zero means means wildcard.
+ $current .= '%';
+ }
+ // Unless we are at offset 0, add a slash.
+ if ($j) {
+ $current .= '/';
+ }
+ }
+ $ancestors[] = '/' . $current;
+ }
+ return $ancestors;
+ }
+}
diff --git a/core/lib/Drupal/Core/Routing/RouteBuilder.php b/core/lib/Drupal/Core/Routing/RouteBuilder.php
new file mode 100644
index 0000000..6335ffe
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/RouteBuilder.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\RouteBuilder.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\Routing\RouteCompilerInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface;
+
+/**
+ * Managing class for rebuilding the router table.
+ *
+ * Because this class makes use of the modules system, it cannot currently
+ * be unit tested.
+ */
+class RouteBuilder {
+
+ /**
+ * The dumper to which we should send collected routes.
+ *
+ * @var \Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface
+ */
+ protected $dumper;
+
+ /**
+ * Construcs the RouteBuilder using the passed MatcherDumperInterface.
+ *
+ * @param Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface $dumper
+ * The matcher dumper used to store the route information.
+ */
+ public function __construct(MatcherDumperInterface $dumper) {
+ $this->dumper = $dumper;
+ }
+
+ /**
+ * Rebuilds the route info and dumps to dumper.
+ */
+ public function rebuild() {
+ // We need to manually call each module so that we can know which module
+ // a given item came from.
+
+ foreach (module_implements('route_info') as $module) {
+ $routes = call_user_func($module . '_route_info');
+ drupal_alter('router_info', $routes, $module);
+ $this->dumper->addRoutes($routes);
+ $this->dumper->dump(array('route_set' => $module));
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php
new file mode 100644
index 0000000..2585d74
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php
@@ -0,0 +1,248 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\RouteCompiler.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\Routing\RouteCompilerInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Compiler to generate derived information from a Route necessary for matching.
+ */
+class RouteCompiler implements RouteCompilerInterface {
+
+ /**
+ * The maximum number of path elements for a route pattern;
+ */
+ const MAX_PARTS = 9;
+
+ /**
+ * Utility constant to use for regular expressions against the path.
+ */
+ const REGEX_DELIMITER = '#';
+
+ /**
+ * Compiles the current route instance.
+ *
+ * @param \Symfony\Component\Routing\Route $route
+ * A Route instance.
+ *
+ * @return \Drupal\Core\Routing\CompiledRoute
+ * A CompiledRoute instance.
+ */
+ public function compile(Route $route) {
+
+ $stripped_path = $this->getPathWithoutDefaults($route);
+
+ $fit = $this->getFit($stripped_path);
+
+ $pattern_outline = $this->getPatternOutline($stripped_path);
+
+ $num_parts = count(explode('/', trim($pattern_outline, '/')));
+
+ $regex = $this->getRegex($route, $route->getPattern());
+
+ return new CompiledRoute($route, $fit, $pattern_outline, $num_parts, $regex);
+ }
+
+ /**
+ * Generates a regular expression that will match this pattern.
+ *
+ * This regex can be used in preg_match() to extract values inside {}.
+ *
+ * This algorithm was lifted directly from Symfony's RouteCompiler class.
+ * It is not factored out nicely there, so we cannot simply subclass it.
+ * @todo Refactor Symfony's RouteCompiler so that it's useful to subclass.
+ *
+ * @param \Symfony\Component\Routing\Route $route
+ * The route object.
+ * @param string $pattern
+ * The pattern for which we want a matching regex.
+ *
+ * @return string
+ * A regular expression that will match a path against this route.
+ *
+ * @throws \LogicException
+ */
+ public function getRegex(Route $route, $pattern) {
+ $len = strlen($pattern);
+ $tokens = array();
+ $variables = array();
+ $pos = 0;
+ preg_match_all('#.\{(\w+)\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ if ($text = substr($pattern, $pos, $match[0][1] - $pos)) {
+ $tokens[] = array('text', $text);
+ }
+
+ $pos = $match[0][1] + strlen($match[0][0]);
+ $var = $match[1][0];
+
+ if ($req = $route->getRequirement($var)) {
+ $regexp = $req;
+ }
+ else {
+ // Use the character preceding the variable as a separator
+ $separators = array($match[0][0][0]);
+
+ if ($pos !== $len) {
+ // Use the character following the variable as the separator when available
+ $separators[] = $pattern[$pos];
+ }
+ $regexp = sprintf('[^%s]+', preg_quote(implode('', array_unique($separators)), self::REGEX_DELIMITER));
+ }
+
+ $tokens[] = array('variable', $match[0][0][0], $regexp, $var);
+
+ if (in_array($var, $variables)) {
+ throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $route->getPattern(), $var));
+ }
+
+ $variables[] = $var;
+ }
+
+ if ($pos < $len) {
+ $tokens[] = array('text', substr($pattern, $pos));
+ }
+
+ // find the first optional token
+ $first_optional = INF;
+ for ($i = count($tokens) - 1; $i >= 0; $i--) {
+ $token = $tokens[$i];
+ if ('variable' === $token[0] && $route->hasDefault($token[3])) {
+ $first_optional = $i;
+ } else {
+ break;
+ }
+ }
+
+ // compute the matching regexp
+ $regexp = '';
+ for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) {
+ $regexp .= $this->computeRegexp($tokens, $i, $first_optional);
+ }
+
+ return self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s';
+ }
+
+ /**
+ * Computes the regexp used to match a specific token. It can be static text or a subpattern.
+ *
+ * @param array $tokens
+ * The route tokens
+ * @param integer $index
+ * The index of the current token
+ * @param integer $first_optional
+ * The index of the first optional token
+ *
+ * @return string
+ * The regexp pattern for a single token
+ */
+ private function computeRegexp(array $tokens, $index, $first_optional) {
+ $token = $tokens[$index];
+ if ('text' === $token[0]) {
+ // Text tokens
+ return preg_quote($token[1], self::REGEX_DELIMITER);
+ }
+ else {
+ // Variable tokens
+ if (0 === $index && 0 === $first_optional) {
+ // When the only token is an optional variable token, the separator is
+ // required.
+ return sprintf('%s(?<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
+ }
+ else {
+ $regexp = sprintf('%s(?<%s>%s)', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
+ if ($index >= $first_optional) {
+ // Enclose each optional token in a subpattern to make it optional.
+ // "?:" means it is non-capturing, i.e. the portion of the subject
+ // string that matched the optional subpattern is not passed back.
+ $regexp = "(?:$regexp";
+ $nbTokens = count($tokens);
+ if ($nbTokens - 1 == $index) {
+ // Close the optional subpatterns.
+ $regexp .= str_repeat(")?", $nbTokens - $first_optional - (0 === $first_optional ? 1 : 0));
+ }
+ }
+
+ return $regexp;
+ }
+ }
+ }
+
+ /**
+ * Returns the pattern outline.
+ *
+ * The pattern outline is the path pattern but normalized so that all
+ * placeholders are equal strings and default values are removed.
+ *
+ * @param string $path
+ * The path for which we want the normalized outline.
+ *
+ * @return string
+ * The path pattern outline.
+ */
+ public function getPatternOutline($path) {
+ return preg_replace('#\{\w+\}#', '%', $path);
+ }
+
+ /**
+ * Determines the fitness of the provided path.
+ *
+ * @param string $path
+ * The path whose fitness we want.
+ *
+ * @return int
+ * The fitness of the path, as an integer.
+ */
+ public function getFit($path) {
+ $parts = explode('/', trim($path, '/'), static::MAX_PARTS);
+ $number_parts = count($parts);
+ // We store the highest index of parts here to save some work in the fit
+ // calculation loop.
+ $slashes = $number_parts - 1;
+
+ $fit = 0;
+ foreach ($parts as $k => $part) {
+ if (strpos($part, '{') === FALSE) {
+ $fit |= 1 << ($slashes - $k);
+ }
+ }
+
+ return $fit;
+ }
+
+ /**
+ * Returns the path of the route, without placeholders with a default value.
+ *
+ * When computing the path outline and fit, we want to skip default-value
+ * placeholders. If we didn't, the path would never match. Note that this
+ * only works for placeholders at the end of the path. Infix placeholders
+ * with default values don't make sense anyway, so that should not be a
+ * problem.
+ *
+ * @param \Symfony\Component\Routing\Route $route
+ * The route to have the placeholders removed from.
+ *
+ * @return string
+ * The path string, stripped of placeholders that have default values.
+ */
+ protected function getPathWithoutDefaults(Route $route) {
+ $path = $route->getPattern();
+ $defaults = $route->getDefaults();
+
+ // Remove placeholders with default values from the outline, so that they
+ // will still match.
+ $remove = array_map(function($a) {
+ return '/{' . $a . '}';
+ }, array_keys($defaults));
+ $path = str_replace($remove, '', $path);
+
+ return $path;
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php
index 90c0fc6..9483261 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php
@@ -178,7 +178,7 @@ class RouterTest extends WebTestBase {
$this->drupalGet('user/login');
// Check that we got to 'user'.
- $this->assertTrue($this->url == url('user', array('absolute' => TRUE)), "Logged-in user redirected to user on accessing user/login");
+ $this->assertTrue($this->url == url('user/' . $this->loggedInUser->uid, array('absolute' => TRUE)), "Logged-in user redirected to user on accessing user/login");
// user/register should redirect to user/UID/edit.
$this->drupalGet('user/register');
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php
new file mode 100644
index 0000000..c6b28cc
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\ChainMatcherTest.
+ */
+
+namespace Drupal\system\Tests\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 Drupal\simpletest\UnitTestBase;
+use Drupal\Core\Routing\ChainMatcher;
+
+use Exception;
+
+/**
+ * Basic tests for the ChainMatcher.
+ */
+class ChainMatcherTest extends UnitTestBase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Chain matcher tests',
+ 'description' => 'Confirm that the chain matcher is working correctly.',
+ 'group' => 'Routing',
+ );
+ }
+
+ /**
+ * Confirms that the expected exception is thrown.
+ */
+ public function testMethodNotAllowed() {
+
+ $chain = new ChainMatcher();
+
+ $method_not_allowed = new MockMatcher(function(Request $request) {
+ throw new MethodNotAllowedException(array('POST'));
+ });
+
+ try {
+ $chain->add($method_not_allowed);
+ $chain->matchRequest(Request::create('my/path'));
+ }
+ catch (MethodNotAllowedException $e) {
+ $this->pass('Correct exception thrown.');
+ }
+ catch (Exception $e) {
+ $this->fail('Incorrect exception thrown: ' . get_class($e));
+ }
+ }
+
+ /**
+ * Confirms that the expected exception is thrown.
+ */
+ public function testRequestNotFound() {
+
+ $chain = new ChainMatcher();
+
+ $resource_not_found = new MockMatcher(function(Request $request) {
+ throw new ResourceNotFoundException();
+ });
+
+ try {
+ $chain->add($resource_not_found);
+ $chain->matchRequest(Request::create('my/path'));
+ }
+ catch (ResourceNotFoundException $e) {
+ $this->pass('Correct exception thrown.');
+ }
+ catch (Exception $e) {
+ $this->fail('Incorrect exception thrown: ' . get_class($e));
+ }
+ }
+
+ /**
+ * Confirms that the expected exception is thrown.
+ */
+ public function testRequestFound() {
+
+ $chain = new ChainMatcher();
+
+ $method_not_allowed = new MockMatcher(function(Request $request) {
+ throw new MethodNotAllowedException(array('POST'));
+ });
+
+ $resource_not_found = new MockMatcher(function(Request $request) {
+ throw new ResourceNotFoundException();
+ });
+
+ $found_data = new MockMatcher(function(Request $request) {
+ return array('_controller' => 'foo');
+ });
+
+ try {
+ $chain->add($method_not_allowed);
+ $chain->add($resource_not_found);
+ $chain->add($found_data);
+ $request = Request::create('my/path');
+ $attributes = $chain->matchRequest($request);
+ $this->assertEqual($attributes['_controller'], 'foo', 'Correct attributes returned.');
+ }
+ catch (Exception $e) {
+ $this->fail('Exception thrown when a match should have been successful: ' . get_class($e));
+ }
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/FirstEntryFinalMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/FirstEntryFinalMatcherTest.php
new file mode 100644
index 0000000..a288b9e
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/FirstEntryFinalMatcherTest.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\NestedMatcherTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+
+use Drupal\simpletest\UnitTestBase;
+use Drupal\Core\Routing\HttpMethodMatcher;
+use Drupal\Core\Routing\NestedMatcher;
+use Drupal\Core\Routing\FirstEntryFinalMatcher;
+
+use Exception;
+
+/**
+ * Basic tests for the NestedMatcher class.
+ */
+class FirstEntryFinalMatcherTest extends UnitTestBase {
+
+ /**
+ * A collection of shared fixture data for tests.
+ *
+ * @var RoutingFixtures
+ */
+ protected $fixtures;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'FirstEntryFinalMatcher tests',
+ 'description' => 'Confirm that the FirstEntryFinalMatcher is working properly.',
+ 'group' => 'Routing',
+ );
+ }
+
+ function __construct($test_id = NULL) {
+ parent::__construct($test_id);
+
+ $this->fixtures = new RoutingFixtures();
+ }
+
+ /**
+ * Confirms the final matcher returns correct attributes for static paths.
+ */
+ public function testFinalMatcherStatic() {
+
+ $collection = new RouteCollection();
+ $collection->add('route_a', new Route('/path/one', array(
+ '_controller' => 'foo',
+ )));
+
+ $request = Request::create('/path/one', 'GET');
+
+ $matcher = new FirstEntryFinalMatcher();
+ $matcher->setCollection($collection);
+ $attributes = $matcher->matchRequest($request);
+
+ $this->assertEqual($attributes['_route'], 'route_a', 'The correct matching route was found.');
+ $this->assertEqual($attributes['_controller'], 'foo', 'The correct controller was found.');
+ }
+
+ /**
+ * Confirms the final matcher returns correct attributes for pattern paths.
+ */
+ public function testFinalMatcherPattern() {
+
+ $collection = new RouteCollection();
+ $collection->add('route_a', new Route('/path/one/{value}', array(
+ '_controller' => 'foo',
+ )));
+
+ $request = Request::create('/path/one/narf', 'GET');
+ $request->attributes->set('system_path', 'path/one/narf');
+
+ $matcher = new FirstEntryFinalMatcher();
+ $matcher->setCollection($collection);
+ $attributes = $matcher->matchRequest($request);
+
+ $this->assertEqual($attributes['_route'], 'route_a', 'The correct matching route was found.');
+ $this->assertEqual($attributes['_controller'], 'foo', 'The correct controller was found.');
+ $this->assertEqual($attributes['value'], 'narf', 'Required placeholder value found.');
+ }
+
+ /**
+ * Confirms the final matcher returns correct attributes with default values.
+ */
+ public function testFinalMatcherPatternDefalts() {
+
+ $collection = new RouteCollection();
+ $collection->add('route_a', new Route('/path/one/{value}', array(
+ '_controller' => 'foo',
+ 'value' => 'poink'
+ )));
+
+ $request = Request::create('/path/one', 'GET');
+ $request->attributes->set('system_path', 'path/one');
+
+ $matcher = new FirstEntryFinalMatcher();
+ $matcher->setCollection($collection);
+ $attributes = $matcher->matchRequest($request);
+
+ $this->assertEqual($attributes['_route'], 'route_a', 'The correct matching route was found.');
+ $this->assertEqual($attributes['_controller'], 'foo', 'The correct controller was found.');
+ $this->assertEqual($attributes['value'], 'poink', 'Optional placeholder value used default.');
+ }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php
new file mode 100644
index 0000000..c98da2e
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\HttpMethodMMatcherTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+
+use Drupal\simpletest\UnitTestBase;
+use Drupal\Core\Routing\HttpMethodMatcher;
+use Drupal\Core\Routing\NestedMatcher;
+use Drupal\Core\Routing\FirstEntryFinalMatcher;
+
+use Exception;
+
+/**
+ * Basic tests for the HttpMethodMatcher class.
+ */
+class HttpMethodMatcherTest extends UnitTestBase {
+
+ /**
+ * A collection of shared fixture data for tests.
+ *
+ * @var RoutingFixtures
+ */
+ protected $fixtures;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Partial matcher HTTP Method tests',
+ 'description' => 'Confirm that the Http Method partial matcher is functioning properly.',
+ 'group' => 'Routing',
+ );
+ }
+
+ function __construct($test_id = NULL) {
+ parent::__construct($test_id);
+
+ $this->fixtures = new RoutingFixtures();
+ }
+
+ /**
+ * Confirms that the HttpMethod matcher matches properly.
+ */
+ public function testFilterRoutes() {
+
+ $matcher = new HttpMethodMatcher();
+ $matcher->setCollection($this->fixtures->sampleRouteCollection());
+
+ $routes = $matcher->matchRequestPartial(Request::create('path/one', 'GET'));
+
+ $this->assertEqual(count($routes->all()), 4, 'The correct number of routes was found.');
+ $this->assertNotNull($routes->get('route_a'), 'The first matching route was found.');
+ $this->assertNull($routes->get('route_b'), 'The non-matching route was not found.');
+ $this->assertNotNull($routes->get('route_c'), 'The second matching route was found.');
+ $this->assertNotNull($routes->get('route_d'), 'The all-matching route was found.');
+ $this->assertNotNull($routes->get('route_e'), 'The multi-matching route was found.');
+ }
+
+ /**
+ * Confirms we can nest multiple partial matchers.
+ */
+ public function testNestedMatcher() {
+
+ $matcher = new NestedMatcher();
+
+ $matcher->setInitialMatcher(new MockPathMatcher($this->fixtures->sampleRouteCollection()));
+ $matcher->addPartialMatcher(new HttpMethodMatcher());
+ $matcher->setFinalMatcher(new FirstEntryFinalMatcher());
+
+ $request = Request::create('/path/one', 'GET');
+
+ $attributes = $matcher->matchRequest($request);
+
+ $this->assertEqual($attributes['_route'], 'route_a', 'The correct matching route was found.');
+ }
+
+ /**
+ * Confirms that the HttpMethod matcher throws an exception for no-route.
+ */
+ public function testNoRouteFound() {
+ $matcher = new HttpMethodMatcher();
+
+ // Remove the sample route that would match any method.
+ $routes = $this->fixtures->sampleRouteCollection();
+ $routes->remove('route_d');
+
+ $matcher->setCollection($routes);
+
+ try {
+ $routes = $matcher->matchRequestPartial(Request::create('path/one', 'DELETE'));
+ $this->fail(t('No exception was thrown.'));
+ }
+ catch (Exception $e) {
+ $this->assertTrue($e instanceof MethodNotAllowedException, 'The correct exception was thrown.');
+ }
+
+ }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php
new file mode 100644
index 0000000..7f6f312
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\UrlMatcherDumperTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+use Drupal\simpletest\UnitTestBase;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Routing\MatcherDumper;
+
+/**
+ * Basic tests for the UrlMatcherDumper.
+ */
+class MatcherDumperTest extends UnitTestBase {
+
+ /**
+ * A collection of shared fixture data for tests.
+ *
+ * @var RoutingFixtures
+ */
+ protected $fixtures;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Dumper tests',
+ 'description' => 'Confirm that the matcher dumper is functioning properly.',
+ 'group' => 'Routing',
+ );
+ }
+
+ function __construct($test_id = NULL) {
+ parent::__construct($test_id);
+
+ $this->fixtures = new RoutingFixtures();
+ }
+
+ function setUp() {
+ parent::setUp();
+ }
+
+ /**
+ * Confirms that the dumper can be instantiated successfuly.
+ */
+ function testCreate() {
+ $connection = Database::getConnection();
+ $dumper= new MatcherDumper($connection);
+
+ $class_name = 'Drupal\Core\Routing\MatcherDumper';
+ $this->assertTrue($dumper instanceof $class_name, 'Dumper created successfully');
+ }
+
+ /**
+ * Confirms that we can add routes to the dumper.
+ */
+ function testAddRoutes() {
+ $connection = Database::getConnection();
+ $dumper= new MatcherDumper($connection);
+
+ $route = new Route('test');
+ $collection = new RouteCollection();
+ $collection->add('test_route', $route);
+
+ $dumper->addRoutes($collection);
+
+ $dumper_routes = $dumper->getRoutes()->all();
+ $collection_routes = $collection->all();
+
+ foreach ($dumper_routes as $name => $route) {
+ $this->assertEqual($route->getPattern(), $collection_routes[$name]->getPattern(), 'Routes match');
+ }
+ }
+
+ /**
+ * Confirms that we can add routes to the dumper when it already has some.
+ */
+ function testAddAdditionalRoutes() {
+ $connection = Database::getConnection();
+ $dumper= new MatcherDumper($connection);
+
+ $route = new Route('test');
+ $collection = new RouteCollection();
+ $collection->add('test_route', $route);
+ $dumper->addRoutes($collection);
+
+ $route = new Route('test2');
+ $collection2 = new RouteCollection();
+ $collection2->add('test_route2', $route);
+ $dumper->addRoutes($collection2);
+
+ // Merge the two collections together so we can test them.
+ $collection->addCollection(clone $collection2);
+
+ $dumper_routes = $dumper->getRoutes()->all();
+ $collection_routes = $collection->all();
+
+ $success = TRUE;
+ foreach ($collection_routes as $name => $route) {
+ if (empty($dumper_routes[$name])) {
+ $success = FALSE;
+ $this->fail(t('Not all routes found in the dumper.'));
+ }
+ }
+
+ if ($success) {
+ $this->pass('All routes found in the dumper.');
+ }
+ }
+
+ /**
+ * Confirm that we can dump a route collection to the database.
+ */
+ public function testDump() {
+ $connection = Database::getConnection();
+ $dumper= new MatcherDumper($connection, 'test_routes');
+
+ $route = new Route('/test/{my}/path');
+ $route->setOption('compiler_class', 'Drupal\Core\Routing\RouteCompiler');
+ $collection = new RouteCollection();
+ $collection->add('test_route', $route);
+
+ $dumper->addRoutes($collection);
+
+ $this->fixtures->createTables($connection);
+
+ $dumper->dump(array('route_set' => 'test'));
+
+ $record = $connection->query("SELECT * FROM {test_routes} WHERE name= :name", array(':name' => 'test_route'))->fetchObject();
+
+ $loaded_route = unserialize($record->route);
+
+ $this->assertEqual($record->name, 'test_route', 'Dumped route has correct name.');
+ $this->assertEqual($record->pattern, '/test/{my}/path', 'Dumped route has correct pattern.');
+ $this->assertEqual($record->pattern_outline, '/test/%/path', 'Dumped route has correct pattern outline.');
+ $this->assertEqual($record->fit, 5 /* 101 in binary */, 'Dumped route has correct fit.');
+ $this->assertTrue($loaded_route instanceof Route, 'Route object retrieved successfully.');
+
+ }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php
new file mode 100644
index 0000000..71611d4
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\MockMatcher.
+ */
+
+namespace Drupal\system\Tests\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 Closure;
+
+/**
+ * A mock matcher that can be configured with any matching logic for testing.
+ *
+ */
+class MockMatcher implements RequestMatcherInterface {
+
+ /**
+ * The matcher being tested.
+ */
+ protected $matcher;
+
+ public function __construct(Closure $matcher) {
+ $this->matcher = $matcher;
+ }
+
+ public function matchRequest(Request $request) {
+ $matcher = $this->matcher;
+ return $matcher($request);
+ }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php
new file mode 100644
index 0000000..1592cbf
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\system\Tests\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+use Drupal\Core\Routing\InitialMatcherInterface;
+
+/**
+ * Provides a mock path matcher.
+ */
+class MockPathMatcher implements InitialMatcherInterface {
+
+ /**
+ * Routes to be matched.
+ *
+ * @var Symfony\Component\Routing\RouteCollection
+ */
+ protected $routes;
+
+ /**
+ * Construct the matcher given the route collection.
+ *
+ * @param Symfony\Component\Routing\RouteCollection $routes
+ * The routes being matched.
+ */
+ public function __construct(RouteCollection $routes) {
+ $this->routes = $routes;
+ }
+
+ /**
+ * Matches a request against multiple routes.
+ *
+ * @param Request $request
+ * A Request object against which to match.
+ *
+ * @return RouteCollection
+ * A RouteCollection of matched routes.
+ */
+ public function matchRequestPartial(Request $request) {
+ // For now for testing we'll just do a straight string match.
+
+ $path = $request->getPathInfo();
+
+ $return = new RouteCollection();
+
+ foreach ($this->routes as $name => $route) {
+ if ($route->getPattern() == $path) {
+ $return->add($name, $route);
+ }
+ }
+
+ return $return;
+ }
+
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php
new file mode 100644
index 0000000..444785c
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\NestedMatcherTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+
+use Drupal\simpletest\UnitTestBase;
+use Drupal\Core\Routing\HttpMethodMatcher;
+use Drupal\Core\Routing\NestedMatcher;
+use Drupal\Core\Routing\FirstEntryFinalMatcher;
+
+use Exception;
+
+/**
+ * Basic tests for the NestedMatcher class.
+ */
+class NestedMatcherTest extends UnitTestBase {
+
+ /**
+ * A collection of shared fixture data for tests.
+ *
+ * @var RoutingFixtures
+ */
+ protected $fixtures;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'NestedMatcher tests',
+ 'description' => 'Confirm that the NestedMatcher system is working properly.',
+ 'group' => 'Routing',
+ );
+ }
+
+ function __construct($test_id = NULL) {
+ parent::__construct($test_id);
+
+ $this->fixtures = new RoutingFixtures();
+ }
+
+ /**
+ * Confirms we can nest multiple partial matchers.
+ */
+ public function testNestedMatcher() {
+
+ $matcher = new NestedMatcher();
+
+ $matcher->setInitialMatcher(new MockPathMatcher($this->fixtures->sampleRouteCollection()));
+ $matcher->addPartialMatcher(new HttpMethodMatcher(), 1);
+ $matcher->setFinalMatcher(new FirstEntryFinalMatcher());
+
+ $request = Request::create('/path/one', 'GET');
+
+ $attributes = $matcher->matchRequest($request);
+
+ $this->assertEqual($attributes['_route'], 'route_a', 'The correct matching route was found.');
+ }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php
new file mode 100644
index 0000000..0780250
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php
@@ -0,0 +1,302 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\PartialMatcherTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+
+use Drupal\simpletest\UnitTestBase;
+use Drupal\Core\Routing\PathMatcher;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Routing\MatcherDumper;
+
+use Exception;
+
+/**
+ * Basic tests for the UrlMatcherDumper.
+ */
+class PathMatcherTest extends UnitTestBase {
+
+ /**
+ * A collection of shared fixture data for tests.
+ *
+ * @var RoutingFixtures
+ */
+ protected $fixtures;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Path matcher tests',
+ 'description' => 'Confirm that the path matching library is working correctly.',
+ 'group' => 'Routing',
+ );
+ }
+
+ function __construct($test_id = NULL) {
+ parent::__construct($test_id);
+
+ $this->fixtures = new RoutingFixtures();
+ }
+
+ public function tearDown() {
+ $this->fixtures->dropTables(Database::getConnection());
+
+ parent::tearDown();
+ }
+
+ /**
+ * Confirms that the correct candidate outlines are generated.
+ */
+ public function testCandidateOutlines() {
+
+ $connection = Database::getConnection();
+ $matcher = new PathMatcher($connection);
+
+ $parts = array('node', '5', 'edit');
+
+ $candidates = $matcher->getCandidateOutlines($parts);
+
+ $candidates = array_flip($candidates);
+
+ $this->assertTrue(count($candidates) == 4, 'Correct number of candidates found');
+ $this->assertTrue(array_key_exists('/node/5/edit', $candidates), 'First candidate found.');
+ $this->assertTrue(array_key_exists('/node/5/%', $candidates), 'Second candidate found.');
+ $this->assertTrue(array_key_exists('/node/%/edit', $candidates), 'Third candidate found.');
+ $this->assertTrue(array_key_exists('/node/%/%', $candidates), 'Fourth candidate found.');
+ }
+
+ /**
+ * Confirms that we can find routes with the exact incoming path.
+ */
+ function testExactPathMatch() {
+ $connection = Database::getConnection();
+ $matcher = new PathMatcher($connection, 'test_routes');
+
+ $this->fixtures->createTables($connection);
+
+ $dumper = new MatcherDumper($connection, 'test_routes');
+ $dumper->addRoutes($this->fixtures->sampleRouteCollection());
+ $dumper->dump();
+
+ $path = '/path/one';
+
+ $request = Request::create($path, 'GET');
+
+ $routes = $matcher->matchRequestPartial($request);
+
+ foreach ($routes as $route) {
+ $this->assertEqual($route->getPattern(), $path, 'Found path has correct pattern');
+ }
+ }
+
+ /**
+ * Confirms that we can find routes whose pattern would match the request.
+ */
+ function testOutlinePathMatch() {
+ $connection = Database::getConnection();
+ $matcher = new PathMatcher($connection, 'test_routes');
+
+ $this->fixtures->createTables($connection);
+
+ $dumper = new MatcherDumper($connection, 'test_routes');
+ $dumper->addRoutes($this->fixtures->complexRouteCollection());
+ $dumper->dump();
+
+ $path = '/path/1/one';
+
+ $request = Request::create($path, 'GET');
+
+ $routes = $matcher->matchRequestPartial($request);
+
+ // All of the matching paths have the correct pattern.
+ foreach ($routes as $route) {
+ $this->assertEqual($route->compile()->getPatternOutline(), '/path/%/one', 'Found path has correct pattern');
+ }
+
+ $this->assertEqual(count($routes->all()), 2, 'The correct number of routes was found.');
+ $this->assertNotNull($routes->get('route_a'), 'The first matching route was found.');
+ $this->assertNotNull($routes->get('route_b'), 'The second matching route was not found.');
+ }
+
+ /**
+ * Confirms that a trailing slash on the request doesn't result in a 404.
+ */
+ function testOutlinePathMatchTrailingSlash() {
+ $connection = Database::getConnection();
+ $matcher = new PathMatcher($connection, 'test_routes');
+
+ $this->fixtures->createTables($connection);
+
+ $dumper = new MatcherDumper($connection, 'test_routes');
+ $dumper->addRoutes($this->fixtures->complexRouteCollection());
+ $dumper->dump();
+
+ $path = '/path/1/one/';
+
+ $request = Request::create($path, 'GET');
+
+ $routes = $matcher->matchRequestPartial($request);
+
+ // All of the matching paths have the correct pattern.
+ foreach ($routes as $route) {
+ $this->assertEqual($route->compile()->getPatternOutline(), '/path/%/one', 'Found path has correct pattern');
+ }
+
+ $this->assertEqual(count($routes->all()), 2, 'The correct number of routes was found.');
+ $this->assertNotNull($routes->get('route_a'), 'The first matching route was found.');
+ $this->assertNotNull($routes->get('route_b'), 'The second matching route was not found.');
+ }
+
+ /**
+ * Confirms that we can find routes whose pattern would match the request.
+ */
+ function testOutlinePathMatchDefaults() {
+ $connection = Database::getConnection();
+ $matcher = new PathMatcher($connection, 'test_routes');
+
+ $this->fixtures->createTables($connection);
+
+ $collection = new RouteCollection();
+ $collection->add('poink', new Route('/some/path/{value}', array(
+ 'value' => 'poink',
+ )));
+
+ $dumper = new MatcherDumper($connection, 'test_routes');
+ $dumper->addRoutes($collection);
+ $dumper->dump();
+
+ $path = '/some/path';
+
+ $request = Request::create($path, 'GET');
+
+ try {
+ $routes = $matcher->matchRequestPartial($request);
+
+ // All of the matching paths have the correct pattern.
+ foreach ($routes as $route) {
+ $compiled = $route->compile();
+ $this->assertEqual($route->compile()->getPatternOutline(), '/some/path', 'Found path has correct pattern');
+ }
+
+ $this->assertEqual(count($routes->all()), 1, 'The correct number of routes was found.');
+ $this->assertNotNull($routes->get('poink'), 'The first matching route was found.');
+ }
+ catch (ResourceNotFoundException $e) {
+ $this->fail('No matching route found with default argument value.');
+ }
+ }
+
+ /**
+ * Confirms that we can find routes whose pattern would match the request.
+ */
+ function testOutlinePathMatchDefaultsCollision() {
+ $connection = Database::getConnection();
+ $matcher = new PathMatcher($connection, 'test_routes');
+
+ $this->fixtures->createTables($connection);
+
+ $collection = new RouteCollection();
+ $collection->add('poink', new Route('/some/path/{value}', array(
+ 'value' => 'poink',
+ )));
+ $collection->add('narf', new Route('/some/path/here'));
+
+ $dumper = new MatcherDumper($connection, 'test_routes');
+ $dumper->addRoutes($collection);
+ $dumper->dump();
+
+ $path = '/some/path';
+
+ $request = Request::create($path, 'GET');
+
+ try {
+ $routes = $matcher->matchRequestPartial($request);
+
+ // All of the matching paths have the correct pattern.
+ foreach ($routes as $route) {
+ $compiled = $route->compile();
+ $this->assertEqual($route->compile()->getPatternOutline(), '/some/path', 'Found path has correct pattern');
+ }
+
+ $this->assertEqual(count($routes->all()), 1, 'The correct number of routes was found.');
+ $this->assertNotNull($routes->get('poink'), 'The first matching route was found.');
+ }
+ catch (ResourceNotFoundException $e) {
+ $this->fail('No matching route found with default argument value.');
+ }
+ }
+
+ /**
+ * Confirms that we can find routes whose pattern would match the request.
+ */
+ function testOutlinePathMatchDefaultsCollision2() {
+ $connection = Database::getConnection();
+ $matcher = new PathMatcher($connection, 'test_routes');
+
+ $this->fixtures->createTables($connection);
+
+ $collection = new RouteCollection();
+ $collection->add('poink', new Route('/some/path/{value}', array(
+ 'value' => 'poink',
+ )));
+ $collection->add('narf', new Route('/some/path/here'));
+
+ $dumper = new MatcherDumper($connection, 'test_routes');
+ $dumper->addRoutes($collection);
+ $dumper->dump();
+
+ $path = '/some/path/here';
+
+ $request = Request::create($path, 'GET');
+
+ try {
+ $routes = $matcher->matchRequestPartial($request);
+
+ // All of the matching paths have the correct pattern.
+ foreach ($routes as $route) {
+ $this->assertEqual($route->compile()->getPatternOutline(), '/some/path/here', 'Found path has correct pattern');
+ }
+
+ $this->assertEqual(count($routes->all()), 1, 'The correct number of routes was found.');
+ $this->assertNotNull($routes->get('narf'), 'The first matching route was found.');
+ }
+ catch (ResourceNotFoundException $e) {
+ $this->fail('No matching route found with default argument value.');
+ }
+ }
+
+ /**
+ * Confirms that an exception is thrown when no matching path is found.
+ */
+ function testOutlinePathNoMatch() {
+ $connection = Database::getConnection();
+ $matcher = new PathMatcher($connection, 'test_routes');
+
+ $this->fixtures->createTables($connection);
+
+ $dumper = new MatcherDumper($connection, 'test_routes');
+ $dumper->addRoutes($this->fixtures->complexRouteCollection());
+ $dumper->dump();
+
+ $path = '/no/such/path';
+
+ $request = Request::create($path, 'GET');
+
+ try {
+ $routes = $matcher->matchRequestPartial($request);
+ $this->fail(t('No exception was thrown.'));
+ }
+ catch (Exception $e) {
+ $this->assertTrue($e instanceof ResourceNotFoundException, 'The correct exception was thrown.');
+ }
+
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php
new file mode 100644
index 0000000..4fc7a11
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\RouteTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Symfony\Component\Routing\Route;
+
+use Drupal\simpletest\UnitTestBase;
+use Drupal\Core\Database\Database;
+
+/**
+ * Basic tests for the Route.
+ */
+class RouteTest extends UnitTestBase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Routes',
+ 'description' => 'Confirm that route object is functioning properly.',
+ 'group' => 'Routing',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ }
+
+ /**
+ * Confirms that a route compiles properly with the necessary data.
+ */
+ public function testCompilation() {
+ $route = new Route('/test/{something}/more');
+ $route->setOption('compiler_class', 'Drupal\Core\Routing\RouteCompiler');
+ $compiled = $route->compile();
+
+ $this->assertEqual($route, $compiled->getRoute(), 'Compiled route has the correct route object.');
+ $this->assertEqual($compiled->getFit(), 5 /* That's 101 binary*/, 'The fit was correct.');
+ $this->assertEqual($compiled->getPatternOutline(), '/test/%/more', 'The pattern outline was correct.');
+ }
+
+ /**
+ * Confirms that a compiled route with default values has the correct outline.
+ */
+ public function testCompilationDefaultValue() {
+ // Because "here" has a default value, it should not factor into the outline
+ // or the fitness.
+ $route = new Route('/test/{something}/more/{here}', array(
+ 'here' => 'there',
+ ));
+ $route->setOption('compiler_class', 'Drupal\Core\Routing\RouteCompiler');
+ $compiled = $route->compile();
+
+ $this->assertEqual($route, $compiled->getRoute(), 'Compiled route has the correct route object.');
+ $this->assertEqual($compiled->getFit(), 5 /* That's 101 binary*/, 'The fit was correct.');
+ $this->assertEqual($compiled->getPatternOutline(), '/test/%/more', 'The pattern outline was correct.');
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php
new file mode 100644
index 0000000..4137375
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Routing\RouterTest.
+ */
+
+namespace Drupal\system\Tests\Routing;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Functional class for the full integrated routing system.
+ */
+class RouterTest extends WebTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('block', 'router_test');
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Integrated Router tests',
+ 'description' => 'Function Tests for the fully integrated routing system.',
+ 'group' => 'Routing',
+ );
+ }
+
+ /**
+ * Confirms that the router can get to a controller.
+ */
+ public function testCanRoute() {
+ $this->drupalGet('router_test/test1');
+ $this->assertRaw('test1', 'The correct string was returned because the route was successful.');
+ }
+
+ /**
+ * Confirms that our default controller logic works properly.
+ */
+ public function testDefaultController() {
+ $this->drupalGet('router_test/test2');
+ $this->assertRaw('test2', 'The correct string was returned because the route was successful.');
+
+ // Confirm that the page wrapping is being added, so we're not getting a
+ // raw body returned.
+ $this->assertRaw('</html>', 'Page markup was found.');
+
+ // In some instances, the subrequest handling may get confused and render
+ // a page inception style. This test verifies that is not happening.
+ $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+ }
+
+ /**
+ * Confirms that placeholders in paths work correctly.
+ */
+ public function testControllerPlaceholders() {
+ $value = $this->randomName();
+ $this->drupalGet('router_test/test3/' . $value);
+ $this->assertRaw($value, 'The correct string was returned because the route was successful.');
+
+ // Confirm that the page wrapping is being added, so we're not getting a
+ // raw body returned.
+ $this->assertRaw('</html>', 'Page markup was found.');
+
+ // In some instances, the subrequest handling may get confused and render
+ // a page inception style. This test verifies that is not happening.
+ $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+ }
+
+ /**
+ * Confirms that default placeholders in paths work correctly.
+ */
+ public function testControllerPlaceholdersDefaultValues() {
+ $this->drupalGet('router_test/test4');
+ $this->assertRaw('narf', 'The correct string was returned because the route was successful.');
+
+ // Confirm that the page wrapping is being added, so we're not getting a
+ // raw body returned.
+ $this->assertRaw('</html>', 'Page markup was found.');
+
+ // In some instances, the subrequest handling may get confused and render
+ // a page inception style. This test verifies that is not happening.
+ $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
+ }
+
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php
new file mode 100644
index 0000000..f243ff1
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Drupal\system\Tests\Routing;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Utility methods to generate sample data, database configuration, etc.
+ */
+class RoutingFixtures {
+
+ /**
+ * Create the tables required for the sample data.
+ *
+ * @param Drupal\Core\Database\Connection $connection
+ * The connection to use to create the tables.
+ */
+ public function createTables(Connection $connection) {
+ $tables = $this->routingTableDefinition();
+ $schema = $connection->schema();
+
+ foreach ($tables as $name => $table) {
+ $schema->dropTable($name);
+ $schema->createTable($name, $table);
+ }
+ }
+
+ /**
+ * Drop the tables used for the sample data.
+ *
+ * @param Drupal\Core\Database\Connection $connection
+ * The connection to use to drop the tables.
+ */
+ public function dropTables(Connection $connection) {
+ $tables = $this->routingTableDefinition();
+ $schema = $connection->schema();
+
+ foreach ($tables as $name => $table) {
+ $schema->dropTable($name);
+ }
+ }
+
+ /**
+ * Returns a standard set of routes for testing.
+ *
+ * @return \Symfony\Component\Routing\RouteCollection
+ */
+ public function sampleRouteCollection() {
+ $collection = new RouteCollection();
+
+ $route = new Route('path/one');
+ $route->setRequirement('_method', 'GET');
+ $collection->add('route_a', $route);
+
+ $route = new Route('path/one');
+ $route->setRequirement('_method', 'PUT');
+ $collection->add('route_b', $route);
+
+ $route = new Route('path/two');
+ $route->setRequirement('_method', 'GET');
+ $collection->add('route_c', $route);
+
+ $route = new Route('path/three');
+ $collection->add('route_d', $route);
+
+ $route = new Route('path/two');
+ $route->setRequirement('_method', 'GET|HEAD');
+ $collection->add('route_e', $route);
+
+ return $collection;
+ }
+
+ /**
+ * Returns a complex set of routes for testing.
+ *
+ * @return \Symfony\Component\Routing\RouteCollection
+ */
+ public function complexRouteCollection() {
+ $collection = new RouteCollection();
+
+ $route = new Route('/path/{thing}/one');
+ $route->setRequirement('_method', 'GET');
+ $collection->add('route_a', $route);
+
+ $route = new Route('/path/{thing}/one');
+ $route->setRequirement('_method', 'PUT');
+ $collection->add('route_b', $route);
+
+ $route = new Route('/somewhere/{item}/over/the/rainbow');
+ $route->setRequirement('_method', 'GET');
+ $collection->add('route_c', $route);
+
+ $route = new Route('/another/{thing}/about/{item}');
+ $collection->add('route_d', $route);
+
+ $route = new Route('/path/add/one');
+ $route->setRequirement('_method', 'GET|HEAD');
+ $collection->add('route_e', $route);
+
+ return $collection;
+ }
+
+ /**
+ * Returns the table definition for the routing fixtures.
+ *
+ * @return array
+ * Table definitions.
+ */
+ public function routingTableDefinition() {
+
+ $tables['test_routes'] = array(
+ 'description' => 'Maps paths to various callbacks (access, page and title)',
+ 'fields' => array(
+ 'name' => array(
+ 'description' => 'Primary Key: Machine name of this route',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'pattern' => array(
+ 'description' => 'The path pattern for this URI',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'pattern_outline' => array(
+ 'description' => 'The pattern',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'route_set' => array(
+ 'description' => 'The route set grouping to which a route belongs.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'access_callback' => array(
+ 'description' => 'The callback which determines the access to this router path. Defaults to user_access.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'access_arguments' => array(
+ 'description' => 'A serialized array of arguments for the access callback.',
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ ),
+ 'fit' => array(
+ 'description' => 'A numeric representation of how specific the path is.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'number_parts' => array(
+ 'description' => 'Number of parts in this router path.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'small',
+ ),
+ 'route' => array(
+ 'description' => 'A serialized Route object',
+ 'type' => 'text',
+ ),
+ ),
+ 'indexes' => array(
+ 'fit' => array('fit'),
+ 'pattern_outline' => array('pattern_outline'),
+ 'route_set' => array('route_set'),
+ ),
+ 'primary key' => array('name'),
+ );
+
+ return $tables;
+ }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php
index 86456e5..812bde7 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php
@@ -10,6 +10,7 @@ namespace Drupal\system\Tests\Upgrade;
use Drupal\Core\Database\Database;
use Drupal\simpletest\WebTestBase;
use Exception;
+use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
/**
* Perform end-to-end tests of the upgrade path.
@@ -246,7 +247,14 @@ abstract class UpgradePathTestBase extends WebTestBase {
module_load_all(FALSE, TRUE);
// Rebuild caches.
- drupal_flush_all_caches();
+ // @todo Remove the try/catch when UpgradePathTestBase::setup() is fixed to
+ // boot DrupalKernel (as WebTestBase::setup() does).
+ drupal_static_reset();
+ try {
+ drupal_flush_all_caches();
+ }
+ catch (InvalidArgumentException $e) {
+ }
// Reload global $conf array and permissions.
$this->refreshVariables();
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 66aa5e8..7957a64 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -566,6 +566,51 @@ function hook_menu_get_item_alter(&$router_item, $path, $original_map) {
}
/**
+ * Defines routes in the new router system.
+ *
+ * A route is a Symfony Route object. See the Symfony documentation for more
+ * details on the available options. Of specific note:
+ * - _controller: This is the PHP callable that will handle a request matching
+ * the route.
+ * - _content: This is the PHP callable that will handle the body of a request
+ * matching this route. A default controller will provide the page
+ * rendering around it.
+ *
+ * Typically you will only specify one or the other of those properties.
+ *
+ * @deprecated
+ * This mechanism for registering routes is temporary. It will be replaced
+ * by a more robust mechanism in the near future. It is documented here
+ * only for completeness.
+ */
+function hook_route_info() {
+ $collection = new RouteCollection();
+
+ $route = new Route('router_test/test1', array(
+ '_controller' => '\Drupal\router_test\TestControllers::test1'
+ ));
+ $collection->add('router_test_1', $route);
+
+ $route = new Route('router_test/test2', array(
+ '_content' => '\Drupal\router_test\TestControllers::test2'
+ ));
+ $collection->add('router_test_2', $route);
+
+ $route = new Route('router_test/test3/{value}', array(
+ '_content' => '\Drupal\router_test\TestControllers::test3'
+ ));
+ $collection->add('router_test_3', $route);
+
+ $route = new Route('router_test/test4/{value}', array(
+ '_content' => '\Drupal\router_test\TestControllers::test4',
+ 'value' => 'narf',
+ ));
+ $collection->add('router_test_4', $route);
+
+ return $collection;
+}
+
+/**
* Define menu items and page callbacks.
*
* This hook enables modules to register paths in order to define how URL
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 3790f2b..05459d0 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1225,6 +1225,63 @@ function system_schema() {
),
);
+ $schema['router'] = array(
+ 'description' => 'Maps paths to various callbacks (access, page and title)',
+ 'fields' => array(
+ 'name' => array(
+ 'description' => 'Primary Key: Machine name of this route',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'pattern' => array(
+ 'description' => 'The path pattern for this URI',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'pattern_outline' => array(
+ 'description' => 'The pattern',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'route_set' => array(
+ 'description' => 'The route set grouping to which a route belongs.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'fit' => array(
+ 'description' => 'A numeric representation of how specific the path is.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'route' => array(
+ 'description' => 'A serialized Route object',
+ 'type' => 'text',
+ ),
+ 'number_parts' => array(
+ 'description' => 'Number of parts in this router path.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'small',
+ ),
+ ),
+ 'indexes' => array(
+ 'fit' => array('fit'),
+ 'pattern_outline' => array('pattern_outline'),
+ 'route_set' => array('route_set'),
+ ),
+ 'primary key' => array('name'),
+ );
+
$schema['semaphore'] = array(
'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as Drupal variables since they must not be cached.',
'fields' => array(
@@ -1975,6 +2032,75 @@ function system_update_8021() {
}
}
+/*
+ * Create the new routing table.
+ */
+function system_update_8022() {
+
+ $tables['router'] = array(
+ 'description' => 'Maps paths to various callbacks (access, page and title)',
+ 'fields' => array(
+ 'name' => array(
+ 'description' => 'Primary Key: Machine name of this route',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'pattern' => array(
+ 'description' => 'The path pattern for this URI',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'pattern_outline' => array(
+ 'description' => 'The pattern',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'route_set' => array(
+ 'description' => 'The route set grouping to which a route belongs.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'fit' => array(
+ 'description' => 'A numeric representation of how specific the path is.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'route' => array(
+ 'description' => 'A serialized Route object',
+ 'type' => 'text',
+ ),
+ 'number_parts' => array(
+ 'description' => 'Number of parts in this router path.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'size' => 'small',
+ ),
+ ),
+ 'indexes' => array(
+ 'fit' => array('fit'),
+ 'pattern_outline' => array('pattern_outline'),
+ 'route_set' => array('route_set'),
+ ),
+ 'primary key' => array('name'),
+ );
+
+ $schema = Database::getConnection()->schema();
+
+ $schema->dropTable('router');
+
+ $schema->createTable('router', $tables['router']);
+}
+
/**
* @} End of "defgroup updates-7.x-to-8.x".
* The next series of updates should start at 9000.
diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php
new file mode 100644
index 0000000..fa92fd8
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\router_test\TestControllers.
+ */
+
+namespace Drupal\router_test;
+
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Controller routines for testing the routing system.
+ */
+class TestControllers {
+
+ public function test1() {
+ return new Response('test1');
+ }
+
+ public function test2() {
+ return "test2";
+ }
+
+ public function test3($value) {
+ return $value;
+ }
+
+ public function test4($value) {
+ return $value;
+ }
+
+}
diff --git a/core/modules/system/tests/modules/router_test/router_test.info b/core/modules/system/tests/modules/router_test/router_test.info
new file mode 100644
index 0000000..d729865
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/router_test.info
@@ -0,0 +1,6 @@
+name = "Router test"
+description = "Support module for routing testing."
+package = Testing
+version = VERSION
+core = 8.x
+hidden = TRUE
diff --git a/core/modules/system/tests/modules/router_test/router_test.module b/core/modules/system/tests/modules/router_test/router_test.module
new file mode 100644
index 0000000..4da939d
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/router_test.module
@@ -0,0 +1,34 @@
+<?php
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Implements hook_router_info().
+ */
+function router_test_route_info() {
+ $collection = new RouteCollection();
+
+ $route = new Route('router_test/test1', array(
+ '_controller' => '\Drupal\router_test\TestControllers::test1'
+ ));
+ $collection->add('router_test_1', $route);
+
+ $route = new Route('router_test/test2', array(
+ '_content' => '\Drupal\router_test\TestControllers::test2'
+ ));
+ $collection->add('router_test_2', $route);
+
+ $route = new Route('router_test/test3/{value}', array(
+ '_content' => '\Drupal\router_test\TestControllers::test3'
+ ));
+ $collection->add('router_test_3', $route);
+
+ $route = new Route('router_test/test4/{value}', array(
+ '_content' => '\Drupal\router_test\TestControllers::test4',
+ 'value' => 'narf',
+ ));
+ $collection->add('router_test_4', $route);
+
+ return $collection;
+}
diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc
index 9dd2cc9..90d804e 100644
--- a/core/modules/user/user.pages.inc
+++ b/core/modules/user/user.pages.inc
@@ -7,6 +7,7 @@
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -419,10 +420,7 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
function user_page() {
global $user;
if ($user->uid) {
- // @todo: Cleaner sub request handling.
- $request = drupal_container()->get('request');
- $subrequest = Request::create('/user/' . $user->uid, 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
- return drupal_container()->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST);
+ return new RedirectResponse(url('user/' . $user->uid, array('absolute' => TRUE)));
}
else {
return drupal_get_form('user_login');
diff --git a/core/update.php b/core/update.php
index 52cf3d0..2273b40 100644
--- a/core/update.php
+++ b/core/update.php
@@ -16,6 +16,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\DependencyInjection\Reference;
// Change the directory to the Drupal root.
chdir('..');
@@ -445,6 +446,17 @@ update_fix_d8_requirements();
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
drupal_maintenance_theme();
+// @todo Remove after converting update.php to use DrupalKernel.
+$container = drupal_container();
+$container->register('database', 'Drupal\Core\Database\Connection')
+ ->setFactoryClass('Drupal\Core\Database\Database')
+ ->setFactoryMethod('getConnection')
+ ->addArgument('default');
+$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'));
+
// Turn error reporting back on. From now on, only fatal errors (which are
// not passed through the error handler) will cause a message to be printed.
ini_set('display_errors', TRUE);