Issue #1606794 by Crell, linclark, effulgentsia, katbailey, disasm, larowlan: Implement new routing system.

8.0.x
Dries 2012-10-01 21:47:15 -04:00
commit 7b5f7b672f
42 changed files with 3212 additions and 114 deletions

View File

@ -1,8 +1,9 @@
<?php <?php
use Drupal\Component\Utility\NestedArray; use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Database; use Drupal\Core\Database\Database;
use Drupal\Core\Template\Attribute; use Drupal\Core\Template\Attribute;
@ -6829,6 +6830,7 @@ function drupal_flush_all_caches() {
// Rebuild the menu router based on all rebuilt data. // Rebuild the menu router based on all rebuilt data.
// Important: This rebuild must happen last, so the menu router is guaranteed // Important: This rebuild must happen last, so the menu router is guaranteed
// to be based on up to date information. // to be based on up to date information.
drupal_container()->get('router.builder')->rebuild();
menu_router_rebuild(); menu_router_rebuild();
// Re-initialize the maintenance theme, if the current request attempted to // Re-initialize the maintenance theme, if the current request attempted to

View File

@ -15,6 +15,8 @@ use Symfony\Component\DependencyInjection\Scope;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Drupal\Core\Database\Database;
/** /**
* Bundle class for mandatory core services. * Bundle class for mandatory core services.
* *
@ -54,12 +56,25 @@ class CoreBundle extends Bundle
->addArgument('slave'); ->addArgument('slave');
$container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager'); $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 // @todo Replace below lines with the commented out block below it when it's
// performant to do so: http://drupal.org/node/1706064. // performant to do so: http://drupal.org/node/1706064.
$dispatcher = $container->get('dispatcher'); $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(); $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\ViewSubscriber($content_negotation));
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\AccessSubscriber()); $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\AccessSubscriber());
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\MaintenanceModeSubscriber()); $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\FinishResponseSubscriber());
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RequestCloseSubscriber()); $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RequestCloseSubscriber());
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber()); $dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber());
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RouteProcessorSubscriber());
$container->set('content_negotiation', $content_negotation); $container->set('content_negotiation', $content_negotation);
$dispatcher->addSubscriber(\Drupal\Core\ExceptionController::getExceptionListener($container)); $dispatcher->addSubscriber(\Drupal\Core\ExceptionController::getExceptionListener($container));

View File

@ -36,11 +36,18 @@ class LegacyControllerSubscriber implements EventSubscriberInterface {
* The Event to process. * The Event to process.
*/ */
public function onKernelControllerLegacy(FilterControllerEvent $event) { 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(); $controller = $event->getController();
// This BC logic applies only to functions. Otherwise, skip it. // This BC logic applies only to functions. Otherwise, skip it.
if (is_string($controller) && function_exists($controller)) { 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) { $new_controller = function() use ($router_item) {
return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']); return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
}; };

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -10,6 +10,7 @@ namespace Drupal\Core\EventSubscriber;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -46,13 +47,43 @@ class ViewSubscriber implements EventSubscriberInterface {
$request = $event->getRequest(); $request = $event->getRequest();
$method = 'on' . $this->negotiation->getContentType($request); // 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)) { if (method_exists($this, $method)) {
$event->setResponse($this->$method($event)); $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 { 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)));
} }
} }

View File

@ -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));
}
}

View File

@ -9,13 +9,14 @@ namespace Drupal\Core;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException; 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; use Symfony\Component\Routing\RequestContext;
/** /**
* UrlMatcher matches URL based on a set of routes. * UrlMatcher matches URL based on a set of routes.
*/ */
class LegacyUrlMatcher implements UrlMatcherInterface { class LegacyUrlMatcher implements RequestMatcherInterface, RequestContextAwareInterface {
/** /**
* The request context for this matcher. * The request context for this matcher.
@ -98,8 +99,8 @@ class LegacyUrlMatcher implements UrlMatcherInterface {
* *
* @api * @api
*/ */
public function match($pathinfo) { public function matchRequest(Request $request) {
if ($router_item = $this->matchDrupalItem($pathinfo)) { if ($router_item = $this->matchDrupalItem($request->attributes->get('system_path'))) {
$ret = $this->convertDrupalItem($router_item); $ret = $this->convertDrupalItem($router_item);
// Stash the router item in the attributes while we're transitioning. // Stash the router item in the attributes while we're transitioning.
$ret['drupal_menu_item'] = $router_item; $ret['drupal_menu_item'] = $router_item;

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -178,7 +178,7 @@ class RouterTest extends WebTestBase {
$this->drupalGet('user/login'); $this->drupalGet('user/login');
// Check that we got to 'user'. // 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. // user/register should redirect to user/UID/edit.
$this->drupalGet('user/register'); $this->drupalGet('user/register');

View File

@ -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));
}
}
}

View File

@ -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.');
}
}

View File

@ -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.');
}
}
}

View File

@ -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.');
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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.');
}
}

View File

@ -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.');
}
}
}

View File

@ -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.');
}
}

View File

@ -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.');
}
}

View File

@ -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;
}
}

View File

@ -10,6 +10,7 @@ namespace Drupal\system\Tests\Upgrade;
use Drupal\Core\Database\Database; use Drupal\Core\Database\Database;
use Drupal\simpletest\WebTestBase; use Drupal\simpletest\WebTestBase;
use Exception; use Exception;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
/** /**
* Perform end-to-end tests of the upgrade path. * Perform end-to-end tests of the upgrade path.
@ -246,7 +247,14 @@ abstract class UpgradePathTestBase extends WebTestBase {
module_load_all(FALSE, TRUE); module_load_all(FALSE, TRUE);
// Rebuild caches. // 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. // Reload global $conf array and permissions.
$this->refreshVariables(); $this->refreshVariables();

View File

@ -565,6 +565,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. * Define menu items and page callbacks.
* *

View File

@ -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( $schema['semaphore'] = array(
'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as Drupal variables since they must not be cached.', 'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as Drupal variables since they must not be cached.',
'fields' => array( '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". * @} End of "defgroup updates-7.x-to-8.x".
* The next series of updates should start at 9000. * The next series of updates should start at 9000.

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
name = "Router test"
description = "Support module for routing testing."
package = Testing
version = VERSION
core = 8.x
hidden = TRUE

View File

@ -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;
}

View File

@ -7,6 +7,7 @@
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernelInterface;
@ -419,10 +420,7 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
function user_page() { function user_page() {
global $user; global $user;
if ($user->uid) { if ($user->uid) {
// @todo: Cleaner sub request handling. return new RedirectResponse(url('user/' . $user->uid, array('absolute' => TRUE)));
$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);
} }
else { else {
return drupal_get_form('user_login'); return drupal_get_form('user_login');

View File

@ -16,6 +16,7 @@
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\DependencyInjection\Reference;
// Change the directory to the Drupal root. // Change the directory to the Drupal root.
chdir('..'); chdir('..');
@ -445,6 +446,17 @@ update_fix_d8_requirements();
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
drupal_maintenance_theme(); 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 // 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. // not passed through the error handler) will cause a message to be printed.
ini_set('display_errors', TRUE); ini_set('display_errors', TRUE);