Issue #1606794 by Crell, linclark, effulgentsia, katbailey, disasm, larowlan: Implement new routing system.
commit
7b5f7b672f
|
@ -1,8 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
|
@ -6829,6 +6830,7 @@ function drupal_flush_all_caches() {
|
|||
// Rebuild the menu router based on all rebuilt data.
|
||||
// Important: This rebuild must happen last, so the menu router is guaranteed
|
||||
// to be based on up to date information.
|
||||
drupal_container()->get('router.builder')->rebuild();
|
||||
menu_router_rebuild();
|
||||
|
||||
// Re-initialize the maintenance theme, if the current request attempted to
|
||||
|
|
|
@ -15,6 +15,8 @@ use Symfony\Component\DependencyInjection\Scope;
|
|||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
|
||||
/**
|
||||
* Bundle class for mandatory core services.
|
||||
*
|
||||
|
@ -54,12 +56,25 @@ class CoreBundle extends Bundle
|
|||
->addArgument('slave');
|
||||
$container->register('typed_data', 'Drupal\Core\TypedData\TypedDataManager');
|
||||
|
||||
$container->register('router.dumper', '\Drupal\Core\Routing\MatcherDumper')
|
||||
->addArgument(new Reference('database'));
|
||||
$container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder')
|
||||
->addArgument(new Reference('router.dumper'));
|
||||
|
||||
// @todo Replace below lines with the commented out block below it when it's
|
||||
// performant to do so: http://drupal.org/node/1706064.
|
||||
$dispatcher = $container->get('dispatcher');
|
||||
$matcher = new \Drupal\Core\LegacyUrlMatcher();
|
||||
$matcher = new \Drupal\Core\Routing\ChainMatcher();
|
||||
$matcher->add(new \Drupal\Core\LegacyUrlMatcher());
|
||||
|
||||
$nested = new \Drupal\Core\Routing\NestedMatcher();
|
||||
$nested->setInitialMatcher(new \Drupal\Core\Routing\PathMatcher(Database::getConnection()));
|
||||
$nested->addPartialMatcher(new \Drupal\Core\Routing\HttpMethodMatcher());
|
||||
$nested->setFinalMatcher(new \Drupal\Core\Routing\FirstEntryFinalMatcher());
|
||||
$matcher->add($nested, 5);
|
||||
|
||||
$content_negotation = new \Drupal\Core\ContentNegotiation();
|
||||
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RouterListener($matcher));
|
||||
$dispatcher->addSubscriber(new \Symfony\Component\HttpKernel\EventListener\RouterListener($matcher));
|
||||
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\ViewSubscriber($content_negotation));
|
||||
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\AccessSubscriber());
|
||||
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\MaintenanceModeSubscriber());
|
||||
|
@ -69,6 +84,7 @@ class CoreBundle extends Bundle
|
|||
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\FinishResponseSubscriber());
|
||||
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RequestCloseSubscriber());
|
||||
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber());
|
||||
$dispatcher->addSubscriber(new \Drupal\Core\EventSubscriber\RouteProcessorSubscriber());
|
||||
$container->set('content_negotiation', $content_negotation);
|
||||
$dispatcher->addSubscriber(\Drupal\Core\ExceptionController::getExceptionListener($container));
|
||||
|
||||
|
|
|
@ -36,11 +36,18 @@ class LegacyControllerSubscriber implements EventSubscriberInterface {
|
|||
* The Event to process.
|
||||
*/
|
||||
public function onKernelControllerLegacy(FilterControllerEvent $event) {
|
||||
$router_item = $event->getRequest()->attributes->get('drupal_menu_item');
|
||||
$request = $event->getRequest();
|
||||
$router_item = $request->attributes->get('drupal_menu_item');
|
||||
$controller = $event->getController();
|
||||
|
||||
// This BC logic applies only to functions. Otherwise, skip it.
|
||||
if (is_string($controller) && function_exists($controller)) {
|
||||
// Flag this as a legacy request. We need to use this for subrequest
|
||||
// handling so that we can treat older page callbacks and new routes
|
||||
// differently.
|
||||
// @todo Remove this line as soon as possible.
|
||||
$request->attributes->set('_legacy', TRUE);
|
||||
|
||||
$new_controller = function() use ($router_item) {
|
||||
return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ namespace Drupal\Core\EventSubscriber;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
|
@ -46,13 +47,43 @@ class ViewSubscriber implements EventSubscriberInterface {
|
|||
|
||||
$request = $event->getRequest();
|
||||
|
||||
$method = 'on' . $this->negotiation->getContentType($request);
|
||||
// For a master request, we process the result and wrap it as needed.
|
||||
// For a subrequest, all we want is the string value. We assume that
|
||||
// is just an HTML string from a controller, so wrap that into a response
|
||||
// object. The subrequest's response will get dissected and placed into
|
||||
// the larger page as needed.
|
||||
if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
|
||||
$method = 'on' . $this->negotiation->getContentType($request);
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$event->setResponse($this->$method($event));
|
||||
if (method_exists($this, $method)) {
|
||||
$event->setResponse($this->$method($event));
|
||||
}
|
||||
else {
|
||||
$event->setResponse(new Response('Unsupported Media Type', 415));
|
||||
}
|
||||
}
|
||||
elseif ($request->attributes->get('_legacy')) {
|
||||
// This is an old hook_menu-based subrequest, which means we assume
|
||||
// the body is supposed to be the complete page.
|
||||
$page_result = $event->getControllerResult();
|
||||
if (!is_array($page_result)) {
|
||||
$page_result = array(
|
||||
'#markup' => $page_result,
|
||||
);
|
||||
}
|
||||
$event->setResponse(new Response(drupal_render_page($page_result)));
|
||||
}
|
||||
else {
|
||||
$event->setResponse(new Response('Unsupported Media Type', 415));
|
||||
// This is a new-style Symfony-esque subrequest, which means we assume
|
||||
// the body is not supposed to be a complete page but just a page
|
||||
// fragment.
|
||||
$page_result = $event->getControllerResult();
|
||||
if (!is_array($page_result)) {
|
||||
$page_result = array(
|
||||
'#markup' => $page_result,
|
||||
);
|
||||
}
|
||||
$event->setResponse(new Response(drupal_render($page_result)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -9,13 +9,14 @@ namespace Drupal\Core;
|
|||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
|
||||
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
|
||||
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
|
||||
use Symfony\Component\Routing\RequestContextAwareInterface;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
|
||||
/**
|
||||
* UrlMatcher matches URL based on a set of routes.
|
||||
*/
|
||||
class LegacyUrlMatcher implements UrlMatcherInterface {
|
||||
class LegacyUrlMatcher implements RequestMatcherInterface, RequestContextAwareInterface {
|
||||
|
||||
/**
|
||||
* The request context for this matcher.
|
||||
|
@ -98,8 +99,8 @@ class LegacyUrlMatcher implements UrlMatcherInterface {
|
|||
*
|
||||
* @api
|
||||
*/
|
||||
public function match($pathinfo) {
|
||||
if ($router_item = $this->matchDrupalItem($pathinfo)) {
|
||||
public function matchRequest(Request $request) {
|
||||
if ($router_item = $this->matchDrupalItem($request->attributes->get('system_path'))) {
|
||||
$ret = $this->convertDrupalItem($router_item);
|
||||
// Stash the router item in the attributes while we're transitioning.
|
||||
$ret['drupal_menu_item'] = $router_item;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -178,7 +178,7 @@ class RouterTest extends WebTestBase {
|
|||
|
||||
$this->drupalGet('user/login');
|
||||
// Check that we got to 'user'.
|
||||
$this->assertTrue($this->url == url('user', array('absolute' => TRUE)), "Logged-in user redirected to user on accessing user/login");
|
||||
$this->assertTrue($this->url == url('user/' . $this->loggedInUser->uid, array('absolute' => TRUE)), "Logged-in user redirected to user on accessing user/login");
|
||||
|
||||
// user/register should redirect to user/UID/edit.
|
||||
$this->drupalGet('user/register');
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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.');
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ namespace Drupal\system\Tests\Upgrade;
|
|||
use Drupal\Core\Database\Database;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Exception;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Perform end-to-end tests of the upgrade path.
|
||||
|
@ -246,7 +247,14 @@ abstract class UpgradePathTestBase extends WebTestBase {
|
|||
module_load_all(FALSE, TRUE);
|
||||
|
||||
// Rebuild caches.
|
||||
drupal_flush_all_caches();
|
||||
// @todo Remove the try/catch when UpgradePathTestBase::setup() is fixed to
|
||||
// boot DrupalKernel (as WebTestBase::setup() does).
|
||||
drupal_static_reset();
|
||||
try {
|
||||
drupal_flush_all_caches();
|
||||
}
|
||||
catch (InvalidArgumentException $e) {
|
||||
}
|
||||
|
||||
// Reload global $conf array and permissions.
|
||||
$this->refreshVariables();
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -1225,6 +1225,63 @@ function system_schema() {
|
|||
),
|
||||
);
|
||||
|
||||
$schema['router'] = array(
|
||||
'description' => 'Maps paths to various callbacks (access, page and title)',
|
||||
'fields' => array(
|
||||
'name' => array(
|
||||
'description' => 'Primary Key: Machine name of this route',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'pattern' => array(
|
||||
'description' => 'The path pattern for this URI',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'pattern_outline' => array(
|
||||
'description' => 'The pattern',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'route_set' => array(
|
||||
'description' => 'The route set grouping to which a route belongs.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'fit' => array(
|
||||
'description' => 'A numeric representation of how specific the path is.',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'route' => array(
|
||||
'description' => 'A serialized Route object',
|
||||
'type' => 'text',
|
||||
),
|
||||
'number_parts' => array(
|
||||
'description' => 'Number of parts in this router path.',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'small',
|
||||
),
|
||||
),
|
||||
'indexes' => array(
|
||||
'fit' => array('fit'),
|
||||
'pattern_outline' => array('pattern_outline'),
|
||||
'route_set' => array('route_set'),
|
||||
),
|
||||
'primary key' => array('name'),
|
||||
);
|
||||
|
||||
$schema['semaphore'] = array(
|
||||
'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as Drupal variables since they must not be cached.',
|
||||
'fields' => array(
|
||||
|
@ -1975,6 +2032,75 @@ function system_update_8021() {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Create the new routing table.
|
||||
*/
|
||||
function system_update_8022() {
|
||||
|
||||
$tables['router'] = array(
|
||||
'description' => 'Maps paths to various callbacks (access, page and title)',
|
||||
'fields' => array(
|
||||
'name' => array(
|
||||
'description' => 'Primary Key: Machine name of this route',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'pattern' => array(
|
||||
'description' => 'The path pattern for this URI',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'pattern_outline' => array(
|
||||
'description' => 'The pattern',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'route_set' => array(
|
||||
'description' => 'The route set grouping to which a route belongs.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'fit' => array(
|
||||
'description' => 'A numeric representation of how specific the path is.',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'route' => array(
|
||||
'description' => 'A serialized Route object',
|
||||
'type' => 'text',
|
||||
),
|
||||
'number_parts' => array(
|
||||
'description' => 'Number of parts in this router path.',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'small',
|
||||
),
|
||||
),
|
||||
'indexes' => array(
|
||||
'fit' => array('fit'),
|
||||
'pattern_outline' => array('pattern_outline'),
|
||||
'route_set' => array('route_set'),
|
||||
),
|
||||
'primary key' => array('name'),
|
||||
);
|
||||
|
||||
$schema = Database::getConnection()->schema();
|
||||
|
||||
$schema->dropTable('router');
|
||||
|
||||
$schema->createTable('router', $tables['router']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "defgroup updates-7.x-to-8.x".
|
||||
* The next series of updates should start at 9000.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
name = "Router test"
|
||||
description = "Support module for routing testing."
|
||||
package = Testing
|
||||
version = VERSION
|
||||
core = 8.x
|
||||
hidden = TRUE
|
|
@ -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;
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
|
@ -419,10 +420,7 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
|
|||
function user_page() {
|
||||
global $user;
|
||||
if ($user->uid) {
|
||||
// @todo: Cleaner sub request handling.
|
||||
$request = drupal_container()->get('request');
|
||||
$subrequest = Request::create('/user/' . $user->uid, 'GET', $request->query->all(), $request->cookies->all(), array(), $request->server->all());
|
||||
return drupal_container()->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST);
|
||||
return new RedirectResponse(url('user/' . $user->uid, array('absolute' => TRUE)));
|
||||
}
|
||||
else {
|
||||
return drupal_get_form('user_login');
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
// Change the directory to the Drupal root.
|
||||
chdir('..');
|
||||
|
@ -445,6 +446,17 @@ update_fix_d8_requirements();
|
|||
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
|
||||
drupal_maintenance_theme();
|
||||
|
||||
// @todo Remove after converting update.php to use DrupalKernel.
|
||||
$container = drupal_container();
|
||||
$container->register('database', 'Drupal\Core\Database\Connection')
|
||||
->setFactoryClass('Drupal\Core\Database\Database')
|
||||
->setFactoryMethod('getConnection')
|
||||
->addArgument('default');
|
||||
$container->register('router.dumper', '\Drupal\Core\Routing\MatcherDumper')
|
||||
->addArgument(new Reference('database'));
|
||||
$container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder')
|
||||
->addArgument(new Reference('router.dumper'));
|
||||
|
||||
// Turn error reporting back on. From now on, only fatal errors (which are
|
||||
// not passed through the error handler) will cause a message to be printed.
|
||||
ini_set('display_errors', TRUE);
|
||||
|
|
Loading…
Reference in New Issue