diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index e9d6d2f..2076707 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -10,7 +10,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass; use Drupal\Core\DependencyInjection\Compiler\RegisterMatchersPass; -use Drupal\Core\DependencyInjection\Compiler\RegisterNestedMatchersPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterSerializationClassesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -133,6 +133,7 @@ public function build(ContainerBuilder $container) { ->addArgument(new Reference('lock')); $this->registerTwig($container); + $this->registerRouting($container); // Add the entity query factory. $container->register('entity.query', 'Drupal\Core\Entity\Query\QueryFactory') @@ -145,13 +146,6 @@ public function build(ContainerBuilder $container) { ->addArgument(new Reference('lock')) ->addArgument(new Reference('event_dispatcher')); - - $container->register('matcher', 'Drupal\Core\Routing\ChainMatcher'); - $container->register('legacy_url_matcher', 'Drupal\Core\LegacyUrlMatcher') - ->addTag('chained_matcher'); - $container->register('nested_matcher', 'Drupal\Core\Routing\NestedMatcher') - ->addTag('chained_matcher', array('priority' => 5)); - $container ->register('cache.path', 'Drupal\Core\Cache\CacheBackendInterface') ->setFactoryClass('Drupal\Core\Cache\CacheFactory') @@ -175,26 +169,15 @@ public function build(ContainerBuilder $container) { $container->register('password', 'Drupal\Core\Password\PhpassHashedPassword') ->addArgument(16); - // The following services are tagged as 'nested_matcher' services and are - // processed in the RegisterNestedMatchersPass compiler pass. Each one - // needs to be set on the matcher using a different method, so we use a - // tag attribute, 'method', which can be retrieved and passed to the - // addMethodCall() method that gets called on the matcher service in the - // compiler pass. - $container->register('path_matcher', 'Drupal\Core\Routing\PathMatcher') - ->addArgument(new Reference('database')) - ->addTag('nested_matcher', array('method' => 'setInitialMatcher')); - $container->register('http_method_matcher', 'Drupal\Core\Routing\HttpMethodMatcher') - ->addTag('nested_matcher', array('method' => 'addPartialMatcher')); + // The following services are tagged as 'route_filter' services and are + // processed in the RegisterRouteFiltersPass compiler pass. $container->register('mime_type_matcher', 'Drupal\Core\Routing\MimeTypeMatcher') - ->addTag('nested_matcher', array('method' => 'addPartialMatcher')); - $container->register('first_entry_final_matcher', 'Drupal\Core\Routing\FirstEntryFinalMatcher') - ->addTag('nested_matcher', array('method' => 'setFinalMatcher')); + ->addTag('route_filter'); $container->register('router_processor_subscriber', 'Drupal\Core\EventSubscriber\RouteProcessorSubscriber') ->addTag('event_subscriber'); $container->register('router_listener', 'Symfony\Component\HttpKernel\EventListener\RouterListener') - ->addArgument(new Reference('matcher')) + ->addArgument(new Reference('router')) ->addTag('event_subscriber'); $container->register('content_negotiation', 'Drupal\Core\ContentNegotiation'); $container->register('view_subscriber', 'Drupal\Core\EventSubscriber\ViewSubscriber') @@ -254,7 +237,9 @@ public function build(ContainerBuilder $container) { ->addArgument(new Reference('database')); $container->addCompilerPass(new RegisterMatchersPass()); - $container->addCompilerPass(new RegisterNestedMatchersPass()); + $container->addCompilerPass(new RegisterRouteFiltersPass()); + // Add a compiler pass for registering event subscribers. + $container->addCompilerPass(new RegisterKernelListenersPass(), PassConfig::TYPE_AFTER_REMOVING); // Add a compiler pass for adding Normalizers and Encoders to Serializer. $container->addCompilerPass(new RegisterSerializationClassesPass()); // Add a compiler pass for registering event subscribers. @@ -263,6 +248,42 @@ public function build(ContainerBuilder $container) { } /** + * Registers the various services for the routing system. + * + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + */ + protected function registerRouting(ContainerBuilder $container) { + $container->register('router.request_context', 'Symfony\Component\Routing\RequestContext') + ->addMethodCall('fromRequest', array(new Reference('request'))); + + $container->register('router.route_provider', 'Drupal\Core\Routing\RouteProvider') + ->addArgument(new Reference('database')); + $container->register('router.matcher.final_matcher', 'Drupal\Core\Routing\UrlMatcher'); + $container->register('router.matcher', 'Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher') + ->addArgument(new Reference('router.route_provider')) + ->addMethodCall('setFinalMatcher', array(new Reference('router.matcher.final_matcher'))); + $container->register('router.generator', 'Drupal\Core\Routing\UrlGenerator') + ->addArgument(new Reference('router.route_provider')) + ->addArgument(new Reference('path.alias_manager.cached')); + $container->register('router.dynamic', 'Symfony\Cmf\Component\Routing\DynamicRouter') + ->addArgument(new Reference('router.request_context')) + ->addArgument(new Reference('router.matcher')) + ->addArgument(new Reference('router.generator')); + + $container->register('legacy_generator', 'Drupal\Core\Routing\NullGenerator'); + $container->register('legacy_url_matcher', 'Drupal\Core\LegacyUrlMatcher'); + $container->register('legacy_router', 'Symfony\Cmf\Component\Routing\DynamicRouter') + ->addArgument(new Reference('router.request_context')) + ->addArgument(new Reference('legacy_url_matcher')) + ->addArgument(new Reference('legacy_generator')); + + $container->register('router', 'Symfony\Cmf\Component\Routing\ChainRouter') + ->addMethodCall('setContext', array(new Reference('router.request_context'))) + ->addMethodCall('add', array(new Reference('router.dynamic'))) + ->addMethodCall('add', array(new Reference('legacy_router'))); + } + + /** * Registers Twig services. */ protected function registerTwig(ContainerBuilder $container) { diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterNestedMatchersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteFiltersPass.php similarity index 65% rename from core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterNestedMatchersPass.php rename to core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteFiltersPass.php index b245952..de9d222 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterNestedMatchersPass.php +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteFiltersPass.php @@ -14,7 +14,7 @@ /** * Adds services tagged 'nested_matcher' to the tagged_matcher service. */ -class RegisterNestedMatchersPass implements CompilerPassInterface { +class RegisterRouteFiltersPass implements CompilerPassInterface { /** * Adds services tagged 'nested_matcher' to the tagged_matcher service. @@ -23,13 +23,12 @@ class RegisterNestedMatchersPass implements CompilerPassInterface { * The container to process. */ public function process(ContainerBuilder $container) { - if (!$container->hasDefinition('nested_matcher')) { + if (!$container->hasDefinition('router.matcher')) { return; } - $nested = $container->getDefinition('nested_matcher'); - foreach ($container->findTaggedServiceIds('nested_matcher') as $id => $attributes) { - $method = $attributes[0]['method']; - $nested->addMethodCall($method, array(new Reference($id))); + $nested = $container->getDefinition('router.matcher'); + foreach ($container->findTaggedServiceIds('route_filter') as $id => $attributes) { + $nested->addMethodCall('addRouteFilter', array(new Reference($id))); } } } diff --git a/core/lib/Drupal/Core/LegacyUrlMatcher.php b/core/lib/Drupal/Core/LegacyUrlMatcher.php index 48987a9..c1175ab 100644 --- a/core/lib/Drupal/Core/LegacyUrlMatcher.php +++ b/core/lib/Drupal/Core/LegacyUrlMatcher.php @@ -150,6 +150,12 @@ protected function convertDrupalItem($router_item) { '_controller' => $router_item['page_callback'] ); + // A few menu items have a fake page callback temporarily. Skip those, + // we aren't going to route them. + if ($router_item['page_callback'] == 'NOT_USED') { + throw new ResourceNotFoundException(); + } + // @todo menu_get_item() does not unserialize page arguments when the access // is denied. Remove this temporary hack that always does that. if (!is_array($router_item['page_arguments'])) { diff --git a/core/lib/Drupal/Core/Routing/ChainMatcher.php b/core/lib/Drupal/Core/Routing/ChainMatcher.php deleted file mode 100644 index 23410bf..0000000 --- a/core/lib/Drupal/Core/Routing/ChainMatcher.php +++ /dev/null @@ -1,165 +0,0 @@ -context = new RequestContext(); - } - - /** - * Sets the request context. - * - * This method is just to satisfy the interface, and is largely vestigial. - * The request context object does not contain the information we need, so - * we will use the original request object. - * - * @param Symfony\Component\Routing\RequestContext $context - * The context. - */ - public function setContext(RequestContext $context) { - $this->context = $context; - } - - /** - * Gets the request context. - * - * This method is just to satisfy the interface, and is largely vestigial. - * The request context object does not contain the information we need, so - * we will use the original request object. - * - * @return Symfony\Component\Routing\RequestContext - * The context. - */ - public function getContext() { - return $this->context; - } - - /** - * Matches a request against all queued matchers. - * - * @param Request $request The request to match - * - * @return array An array of parameters - * - * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException - * If no matching resource could be found - * @throws \Symfony\Component\Routing\Exception\MethodNotAllowedException - * If a matching resource was found but the request method is not allowed - */ - public function matchRequest(Request $request) { - $methodNotAllowed = null; - - foreach ($this->all() as $matcher) { - try { - return $matcher->matchRequest($request); - } catch (ResourceNotFoundException $e) { - // Needs special care - } catch (MethodNotAllowedException $e) { - $methodNotAllowed = $e; - } - } - - throw $methodNotAllowed ?: new ResourceNotFoundException("None of the matchers in the chain matched this request."); - } - - /** - * Adds a Matcher to the index. - * - * @param MatcherInterface $matcher - * The matcher to add. - * @param int $priority - * (optional) The priority of the matcher. Higher number matchers will be checked - * first. Default to 0. - */ - public function add(RequestMatcherInterface $matcher, $priority = 0) { - if (empty($this->matchers[$priority])) { - $this->matchers[$priority] = array(); - } - - $this->matchers[$priority][] = $matcher; - $this->sortedMatchers = array(); - } - - /** - * Sorts the matchers and flattens them. - * - * @return array - * An array of RequestMatcherInterface objects. - */ - public function all() { - if (empty($this->sortedMatchers)) { - $this->sortedMatchers = $this->sortMatchers(); - } - - return $this->sortedMatchers; - } - - /** - * Sort matchers by priority. - * - * The highest priority number is the highest priority (reverse sorting). - * - * @return \Symfony\Component\Routing\RequestMatcherInterface[] - * An array of Matcher objects in the order they should be used. - */ - protected function sortMatchers() { - $sortedMatchers = array(); - krsort($this->matchers); - - foreach ($this->matchers as $matchers) { - $sortedMatchers = array_merge($sortedMatchers, $matchers); - } - - return $sortedMatchers; - } - -} diff --git a/core/lib/Drupal/Core/Routing/CompiledRoute.php b/core/lib/Drupal/Core/Routing/CompiledRoute.php index c5cdde8..b457afd 100644 --- a/core/lib/Drupal/Core/Routing/CompiledRoute.php +++ b/core/lib/Drupal/Core/Routing/CompiledRoute.php @@ -8,11 +8,12 @@ namespace Drupal\Core\Routing; use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\CompiledRoute as SymfonyCompiledRoute; /** - * Description of CompiledRoute + * A compiled route contains derived information from a route object. */ -class CompiledRoute { +class CompiledRoute extends SymfonyCompiledRoute { /** * The fitness of this route. @@ -43,14 +44,12 @@ class CompiledRoute { protected $route; /** - * The regular expression to match placeholders out of this path. + * Constructs a new compiled route object. * - * @var string - */ - protected $regex; - - /** - * Constructs a new CompiledRoute object. + * This is a ridiculously long set of constructor parameters, but as this + * object is little more than a collection of values it's not a serious + * problem. The parent Symfony class does the same, as well, making it + * difficult to override differently. * * @param \Symfony\Component\Routing\Route $route * A original Route instance. @@ -60,15 +59,30 @@ class CompiledRoute { * The pattern outline for this route. * @param int $num_parts * The number of parts in the path. + * @param string $staticPrefix + * The static prefix of the compiled route * @param string $regex - * The regular expression to match placeholders out of this path. + * The regular expression to use to match this route + * @param array $tokens + * An array of tokens to use to generate URL for this route + * @param array $pathVariables + * An array of path variables + * @param string|null $hostnameRegex + * Hostname regex + * @param array $hostnameTokens + * Hostname tokens + * @param array $hostnameVariables + * An array of hostname variables + * @param array $variables + * An array of variables (variables defined in the path and in the hostname patterns) */ - public function __construct(Route $route, $fit, $pattern_outline, $num_parts, $regex) { + public function __construct(Route $route, $fit, $pattern_outline, $num_parts, $staticPrefix, $regex, array $tokens, array $pathVariables, $hostnameRegex = null, array $hostnameTokens = array(), array $hostnameVariables = array(), array $variables = array()) { + parent::__construct($staticPrefix, $regex, $tokens, $pathVariables, $hostnameRegex, $hostnameTokens, $hostnameVariables, $variables); + $this->route = $route; $this->fit = $fit; $this->patternOutline = $pattern_outline; $this->numParts = $num_parts; - $this->regex = $regex; } /** @@ -110,16 +124,6 @@ public function getPatternOutline() { } /** - * 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 diff --git a/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php b/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php deleted file mode 100644 index 8b85c21..0000000 --- a/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php +++ /dev/null @@ -1,39 +0,0 @@ -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); - - $route->setOption('_name', $name); - return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $route)); - } - } - - /** - * Get merged default parameters. - * - * @param array $params - * The parameters. - * @param array $defaults - * The defaults. - * - * @return array - * Merged default parameters. - */ - protected function mergeDefaults($params, $defaults) { - $parameters = $defaults; - foreach ($params as $key => $value) { - if (!is_int($key)) { - $parameters[$key] = $value; - } - } - - return $parameters; - } - -} diff --git a/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php b/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php deleted file mode 100644 index 5064353..0000000 --- a/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php +++ /dev/null @@ -1,58 +0,0 @@ -getMethod(); - - $collection = new RouteCollection(); - - foreach ($this->routes->all() as $name => $route) { - // _method could be a |-delimited list of allowed methods, or null. If - // null, we accept any method. - $allowed_methods = array_filter(explode('|', strtoupper($route->getRequirement('_method')))); - if (empty($allowed_methods) || in_array($method, $allowed_methods)) { - $collection->add($name, $route); - } - else { - // Build a list of methods that would have matched. Note that we only - // need to do this if a route doesn't match, because if even one route - // passes then we'll never throw the exception that needs this array. - $possible_methods += $allowed_methods; - } - } - - if (!count($collection->all())) { - throw new MethodNotAllowedException(array_unique($possible_methods)); - } - - return $collection; - } - -} - diff --git a/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php b/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php deleted file mode 100644 index 53bc8e7..0000000 --- a/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php +++ /dev/null @@ -1,27 +0,0 @@ -getAcceptableContentTypes(); $acceptable_formats = array_map(array($request, 'getFormat'), $acceptable_mime_types); - $collection = new RouteCollection(); + $filtered_collection = new RouteCollection(); - foreach ($this->routes->all() as $name => $route) { + foreach ($collection as $name => $route) { // _format could be a |-delimited list of supported formats. $supported_formats = array_filter(explode('|', $route->getRequirement('_format'))); // The route partially matches if it doesn't care about format, if it // explicitly allows any format, or if one of its allowed formats is // in the request's list of acceptable formats. if (empty($supported_formats) || in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) { - $collection->add($name, $route); + $filtered_collection->add($name, $route); } } - if (!count($collection)) { + if (!count($filtered_collection)) { throw new UnsupportedMediaTypeHttpException(); } - return $collection; + return $filtered_collection; } } diff --git a/core/lib/Drupal/Core/Routing/NestedMatcher.php b/core/lib/Drupal/Core/Routing/NestedMatcher.php deleted file mode 100644 index ff53715..0000000 --- a/core/lib/Drupal/Core/Routing/NestedMatcher.php +++ /dev/null @@ -1,199 +0,0 @@ -matchers[$priority])) { - $this->matchers[$priority] = array(); - } - - $this->matchers[$priority][] = $matcher; - $this->sortedMatchers = array(); - } - - /** - * Sets the final matcher for the matching plan. - * - * @param \Drupal\Core\Routing\FinalMatcherInterface $final - * The matcher that will be called last to ensure only a single route is - * found. - * - * @return \Drupal\Core\Routing\NestedMatcherInterface - * The current matcher. - */ - public function setFinalMatcher(FinalMatcherInterface $final) { - $this->finalMatcher = $final; - - return $this; - } - - /** - * Sets the first matcher for the matching plan. - * - * Partial matchers will be run in the order in which they are added. - * - * @param \Drupal\Core\Routing\InitialMatcherInterface $matcher - * An initial matcher. It is responsible for its own configuration and - * initial route collection - * - * @return \Drupal\Core\Routing\NestedMatcherInterface - * The current matcher. - */ - public function setInitialMatcher(InitialMatcherInterface $initial) { - $this->initialMatcher = $initial; - - return $this; - } - - /** - * Tries to match a request with a set of routes. - * - * If the matcher can not find information, it must throw one of the - * exceptions documented below. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request to match. - * - * @return array - * An array of parameters. - * - * @throws ResourceNotFoundException - * If no matching resource could be found. - * @throws MethodNotAllowedException - * If a matching resource was found but the request method is not allowed. - */ - public function matchRequest(Request $request) { - $collection = $this->initialMatcher->matchRequestPartial($request); - - foreach ($this->getPartialMatchers() as $matcher) { - if ($collection) { - $matcher->setCollection($collection); - } - $collection = $matcher->matchRequestPartial($request); - } - - $attributes = $this->finalMatcher->setCollection($collection)->matchRequest($request); - - return $attributes; - } - - /** - * Sorts the matchers and flattens them. - * - * @return array - * An array of RequestMatcherInterface objects. - */ - public function getPartialMatchers() { - if (empty($this->sortedMatchers)) { - $this->sortedMatchers = $this->sortMatchers(); - } - - return $this->sortedMatchers; - } - - /** - * Sort matchers by priority. - * - * The highest priority number is the highest priority (reverse sorting). - * - * @return \Symfony\Component\Routing\RequestMatcherInterface[] - * An array of Matcher objects in the order they should be used. - */ - protected function sortMatchers() { - $sortedMatchers = array(); - krsort($this->matchers); - - foreach ($this->matchers as $matchers) { - $sortedMatchers = array_merge($sortedMatchers, $matchers); - } - - return $sortedMatchers; - } - - /** - * Sets the request context. - * - * This method is unused. It is here only to satisfy the interface. - * - * @param \Symfony\Component\Routing\RequestContext $context - * The context - */ - public function setContext(RequestContext $context) { - $this->context = $context; - } - - /** - * Gets the request context. - * - * This method is unused. It is here only to satisfy the interface. - * - * @return \Symfony\Component\Routing\RequestContext - * The context - */ - public function getContext() { - return $this->context; - } - -} diff --git a/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php b/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php deleted file mode 100644 index 6ae0995..0000000 --- a/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php +++ /dev/null @@ -1,58 +0,0 @@ -routes = $collection; - - return $this; - } - -} diff --git a/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php b/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php deleted file mode 100644 index 0d180c6..0000000 --- a/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php +++ /dev/null @@ -1,39 +0,0 @@ -getPathWithoutDefaults($route); + $symfony_compiled = parent::compile($route); + // The Drupal-specific compiled information. + $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; - } - } + return new CompiledRoute( + $route, + $fit, + $pattern_outline, + $num_parts, + // These are the Symfony compiled parts. + $symfony_compiled->getStaticPrefix(), + $symfony_compiled->getRegex(), + $symfony_compiled->getTokens(), + $symfony_compiled->getPathVariables(), + $symfony_compiled->getHostnameRegex(), + $symfony_compiled->getHostnameTokens(), + $symfony_compiled->getHostnameVariables(), + $symfony_compiled->getVariables() + ); } /** diff --git a/core/lib/Drupal/Core/Routing/PathMatcher.php b/core/lib/Drupal/Core/Routing/RouteProvider.php similarity index 52% rename from core/lib/Drupal/Core/Routing/PathMatcher.php rename to core/lib/Drupal/Core/Routing/RouteProvider.php index 9b5bd5e..0e4e572 100644 --- a/core/lib/Drupal/Core/Routing/PathMatcher.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -2,21 +2,22 @@ /** * @file - * Definition of Drupal\Core\Routing\PathMatcher. + * Contains Drupal\Core\Routing\RouteProvider. */ namespace Drupal\Core\Routing; +use Symfony\Cmf\Component\Routing\RouteProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Drupal\Core\Database\Connection; +use \Drupal\Core\Database\Connection; /** - * Initial matcher to match a route against a built database, by path. + * A Route Provider front-end for all Drupal-stored routes. */ -class PathMatcher implements InitialMatcherInterface { +class RouteProvider implements RouteProviderInterface { /** * The database connection from which to read route information. @@ -33,6 +34,13 @@ class PathMatcher implements InitialMatcherInterface { protected $tableName; /** + * A cache of already-loaded routes, keyed by route name. + * + * @var array + */ + protected $routes; + + /** * Constructs a new PathMatcher. * * @param \Drupal\Core\Database\Connection $connection @@ -46,15 +54,31 @@ public function __construct(Connection $connection, $table = 'router') { } /** - * Matches a request against multiple routes. + * Finds routes that may potentially match the request. + * + * This may return a mixed list of class instances, but all routes returned + * must extend the core symfony route. The classes may also implement + * RouteObjectInterface to link to a content document. * - * @param \Symfony\Component\HttpFoundation\Request $request - * A Request object against which to match. + * This method may not throw an exception based on implementation specific + * restrictions on the url. That case is considered a not found - returning + * an empty array. Exceptions are only used to abort the whole request in + * case something is seriously broken, like the storage backend being down. * - * @return \Symfony\Component\Routing\RouteCollection - * A RouteCollection of matched routes. + * Note that implementations may not implement an optimal matching + * algorithm, simply a reasonable first pass. That allows for potentially + * very large route sets to be filtered down to likely candidates, which + * may then be filtered in memory more completely. + * + * @param Request $request A request against which to match. + * + * @return \Symfony\Component\Routing\RouteCollection with all urls that + * could potentially match $request. Empty collection if nothing can + * match. + * + * @todo Should this method's found routes also be included in the cache? */ - public function matchRequestPartial(Request $request) { + public function getRouteCollectionForRequest(Request $request) { // The 'system_path' has language prefix stripped and path alias resolved, // whereas getPathInfo() returns the requested path. In Drupal, the request @@ -97,6 +121,61 @@ public function matchRequestPartial(Request $request) { } /** + * Find the route using the provided route name (and parameters) + * + * @param string $name the route name to fetch + * @param array $parameters the parameters as they are passed to the + * UrlGeneratorInterface::generate call + * + * @return \Symfony\Component\Routing\Route + * + * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException if + * there is no route with that name in this repository + */ + public function getRouteByName($name, $parameters = array()) { + $routes = $this->getRoutesByNames(array($name), $parameters); + if (empty($routes)) { + throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); + } + + return reset($routes); + } + + /** + * Find many routes by their names using the provided list of names + * + * Note that this method may not throw an exception if some of the routes + * are not found. It will just return the list of those routes it found. + * + * This method exists in order to allow performance optimizations. The + * simple implementation could be to just repeatedly call + * $this->getRouteByName() + * + * @param array $names the list of names to retrieve + * @param array $parameters the parameters as they are passed to the + * UrlGeneratorInterface::generate call. (Only one array, not one for + * each entry in $names. + * + * @return \Symfony\Component\Routing\Route[] iterable thing with the keys + * the names of the $names argument. + */ + public function getRoutesByNames($names, $parameters = array()) { + + $routes_to_load = array_diff($names, array_keys($this->routes)); + + $result = $this->connection->query('SELECT name, route FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name IN :names', array(':names' => $routes_to_load)); + $routes = $result->fetchAllKeyed(); + + $return = array(); + foreach ($routes as $name => $route) { + $this->routes[$name] = unserialize($route); + } + + return array_intersect_key($this->routes, $names); + + } + + /** * Returns an array of path pattern outlines that could match the path parts. * * @param array $parts @@ -145,4 +224,5 @@ public function getCandidateOutlines(array $parts) { } return $ancestors; } + } diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php new file mode 100644 index 0000000..75779a2 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -0,0 +1,58 @@ +aliasManager = $alias_manager; + } + + /** + * Implements Symfony\Component\Routing\Generator\UrlGeneratorInterface::generate(); + */ + public function generate($name, $parameters = array(), $absolute = FALSE) { + $path = parent::generate($name, $parameters, $absolute); + + // This method is expected to return a path with a leading /, whereas + // the alias manager has no leading /. + $path = '/' . $this->aliasManager->getPathAlias(trim($path, '/')); + + return $path; + } + +} diff --git a/core/lib/Drupal/Core/Routing/UrlMatcher.php b/core/lib/Drupal/Core/Routing/UrlMatcher.php new file mode 100644 index 0000000..ae5fc7d --- /dev/null +++ b/core/lib/Drupal/Core/Routing/UrlMatcher.php @@ -0,0 +1,36 @@ +routes = $collection; + $context = new RequestContext(); + $context->fromRequest($request); + $this->setContext($context); + return $this->match('/' . $request->attributes->get('system_path')); + } + +} diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php index 6808020..874ad13 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php @@ -59,9 +59,13 @@ public function testRead() { // checked in serialization tests. $this->assertEqual($data['uuid'][LANGUAGE_DEFAULT][0]['value'], $entity->uuid(), 'Entity UUID is correct'); - // Try to read the entity with an unsupported media format. + // Try to read the entity with an unsupported mime format. + // Because the matcher checks mime type first, then method, this will hit + // zero viable routes on the method. If the mime matcher wasn't working, + // we would still find an existing GET route with the wrong format. That + // means this is a valid functional test for mime-matching. $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat'); - $this->assertResponse(415); + $this->assertResponse(405); // Try to read an entity that does not exist. $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/vnd.drupal.ld+json'); diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php deleted file mode 100644 index c6b28cc..0000000 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php +++ /dev/null @@ -1,112 +0,0 @@ - 'Chain matcher tests', - 'description' => 'Confirm that the chain matcher is working correctly.', - 'group' => 'Routing', - ); - } - - /** - * Confirms that the expected exception is thrown. - */ - public function testMethodNotAllowed() { - - $chain = new ChainMatcher(); - - $method_not_allowed = new MockMatcher(function(Request $request) { - throw new MethodNotAllowedException(array('POST')); - }); - - try { - $chain->add($method_not_allowed); - $chain->matchRequest(Request::create('my/path')); - } - catch (MethodNotAllowedException $e) { - $this->pass('Correct exception thrown.'); - } - catch (Exception $e) { - $this->fail('Incorrect exception thrown: ' . get_class($e)); - } - } - - /** - * Confirms that the expected exception is thrown. - */ - public function testRequestNotFound() { - - $chain = new ChainMatcher(); - - $resource_not_found = new MockMatcher(function(Request $request) { - throw new ResourceNotFoundException(); - }); - - try { - $chain->add($resource_not_found); - $chain->matchRequest(Request::create('my/path')); - } - catch (ResourceNotFoundException $e) { - $this->pass('Correct exception thrown.'); - } - catch (Exception $e) { - $this->fail('Incorrect exception thrown: ' . get_class($e)); - } - } - - /** - * Confirms that the expected exception is thrown. - */ - public function testRequestFound() { - - $chain = new ChainMatcher(); - - $method_not_allowed = new MockMatcher(function(Request $request) { - throw new MethodNotAllowedException(array('POST')); - }); - - $resource_not_found = new MockMatcher(function(Request $request) { - throw new ResourceNotFoundException(); - }); - - $found_data = new MockMatcher(function(Request $request) { - return array('_controller' => 'foo'); - }); - - try { - $chain->add($method_not_allowed); - $chain->add($resource_not_found); - $chain->add($found_data); - $request = Request::create('my/path'); - $attributes = $chain->matchRequest($request); - $this->assertEqual($attributes['_controller'], 'foo', 'Correct attributes returned.'); - } - catch (Exception $e) { - $this->fail('Exception thrown when a match should have been successful: ' . get_class($e)); - } - } - -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/FirstEntryFinalMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/FirstEntryFinalMatcherTest.php deleted file mode 100644 index c44a492..0000000 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/FirstEntryFinalMatcherTest.php +++ /dev/null @@ -1,112 +0,0 @@ - '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']->getOption('_name'), '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']->getOption('_name'), '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']->getOption('_name'), 'route_a', 'The correct matching route was found.'); - $this->assertEqual($attributes['_controller'], 'foo', 'The correct controller was found.'); - $this->assertEqual($attributes['value'], 'poink', 'Optional placeholder value used default.'); - } -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php deleted file mode 100644 index 8055743..0000000 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php +++ /dev/null @@ -1,105 +0,0 @@ - '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']->getOption('_name'), 'route_a', 'The correct matching route was found.'); - } - - /** - * Confirms that the HttpMethod matcher throws an exception for no-route. - */ - public function testNoRouteFound() { - $matcher = new HttpMethodMatcher(); - - // Remove the sample route that would match any method. - $routes = $this->fixtures->sampleRouteCollection(); - $routes->remove('route_d'); - - $matcher->setCollection($routes); - - try { - $routes = $matcher->matchRequestPartial(Request::create('path/one', 'DELETE')); - $this->fail(t('No exception was thrown.')); - } - catch (Exception $e) { - $this->assertTrue($e instanceof MethodNotAllowedException, 'The correct exception was thrown.'); - } - - } -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php index 05f8d42..6061d56 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MimeTypeMatcherTest.php @@ -7,9 +7,7 @@ namespace Drupal\system\Tests\Routing; -use Drupal\Core\Routing\FirstEntryFinalMatcher; use Drupal\Core\Routing\MimeTypeMatcher; -use Drupal\Core\Routing\NestedMatcher; use Drupal\simpletest\UnitTestBase; use Symfony\Component\HttpFoundation\Request; @@ -47,52 +45,34 @@ function __construct($test_id = NULL) { public function testFilterRoutes() { $matcher = new MimeTypeMatcher(); - $matcher->setCollection($this->fixtures->sampleRouteCollection()); + $collection = $this->fixtures->sampleRouteCollection(); // Tests basic JSON request. $request = Request::create('path/two', 'GET'); $request->headers->set('Accept', 'application/json, text/xml;q=0.9'); - $routes = $matcher->matchRequestPartial($request); - $this->assertEqual(count($routes->all()), 4, 'The correct number of routes was found.'); + $routes = $matcher->filter($collection, $request); + $this->assertEqual(count($routes), 4, 'The correct number of routes was found.'); $this->assertNotNull($routes->get('route_c'), 'The json route was found.'); $this->assertNull($routes->get('route_e'), 'The html route was not found.'); // Tests JSON request with alternative JSON MIME type Accept header. $request = Request::create('path/two', 'GET'); $request->headers->set('Accept', 'application/x-json, text/xml;q=0.9'); - $routes = $matcher->matchRequestPartial($request); - $this->assertEqual(count($routes->all()), 4, 'The correct number of routes was found.'); + $routes = $matcher->filter($collection, $request); + $this->assertEqual(count($routes), 4, 'The correct number of routes was found.'); $this->assertNotNull($routes->get('route_c'), 'The json route was found.'); $this->assertNull($routes->get('route_e'), 'The html route was not found.'); // Tests basic HTML request. $request = Request::create('path/two', 'GET'); $request->headers->set('Accept', 'text/html, text/xml;q=0.9'); - $routes = $matcher->matchRequestPartial($request); - $this->assertEqual(count($routes->all()), 4, 'The correct number of routes was found.'); + $routes = $matcher->filter($collection, $request); + $this->assertEqual(count($routes), 4, 'The correct number of routes was found.'); $this->assertNull($routes->get('route_c'), 'The json route was not found.'); $this->assertNotNull($routes->get('route_e'), 'The html 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 MimeTypeMatcher()); - $matcher->setFinalMatcher(new FirstEntryFinalMatcher()); - - $request = Request::create('/path/two', 'GET'); - $request->headers->set('Accept', 'text/html, text/xml;q=0.9'); - - $attributes = $matcher->matchRequest($request); - $this->assertEqual($attributes['_route']->getOption('_name'), 'route_e', 'The correct matching route was found.'); - } - - /** * Confirms that the MimeTypeMatcher matcher throws an exception for no-route. */ public function testNoRouteFound() { @@ -105,12 +85,10 @@ public function testNoRouteFound() { $routes->remove('route_c'); $routes->remove('route_d'); - $matcher->setCollection($routes); - try { $request = Request::create('path/two', 'GET'); $request->headers->set('Accept', 'application/json, text/xml;q=0.9'); - $routes = $matcher->matchRequestPartial($request); + $routes = $matcher->filter($routes, $request); $this->fail(t('No exception was thrown.')); } catch (UnsupportedMediaTypeHttpException $e) { diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockAliasManager.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockAliasManager.php new file mode 100644 index 0000000..6f5add4 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MockAliasManager.php @@ -0,0 +1,92 @@ +defaultLanguage; + + $this->aliases[$path][$language] = $alias; + $this->systemPaths[$alias][$language] = $path; + } + + /** + * Implements \Drupal\Core\Path\AliasManagerInterface::getSystemPath(). + */ + public function getSystemPath($path, $path_language = NULL) { + $language = $path_language ?: $this->defaultLanguage; + return $this->systemPaths[$path][$language]; + } + + /** + * Implements \Drupal\Core\Path\AliasManagerInterface::getPathAlias(). + */ + public function getPathAlias($path, $path_language = NULL) { + $language = $path_language ?: $this->defaultLanguage; + $this->lookedUp[$path] = 1; + return $this->aliases[$path][$language]; + } + + /** + * Implements \Drupal\Core\Path\AliasManagerInterface::getPathLookups(). + */ + public function getPathLookups() { + return array_keys($this->lookedUp); + } + + /** + * Implements \Drupal\Core\Path\AliasManagerInterface::preloadPathLookups(). + */ + public function preloadPathLookups(array $path_list) { + // Not needed. + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockController.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockController.php index fe09a7b..bc9d093 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/MockController.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MockController.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\system\Tests\Routing\MockController. + * Contains Drupal\system\Tests\Routing\MockController. */ namespace Drupal\system\Tests\Routing; @@ -14,6 +14,9 @@ */ class MockController extends ContainerAware { + /** + * Does nothing; this is just a fake controller method. + */ public function run() {} } diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php index 71611d4..dc796b8 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php @@ -9,15 +9,9 @@ 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 { @@ -26,10 +20,19 @@ class MockMatcher implements RequestMatcherInterface { */ protected $matcher; - public function __construct(Closure $matcher) { + /** + * Constructs a MockMatcher object. + * + * @param \Closure $matcher + * An anonymous function that will be used for the matchRequest() method. + */ + public function __construct(\Closure $matcher) { $this->matcher = $matcher; } + /** + * Implements \Symfony\Component\Routing\Matcher\RequestMatcherInterface::matchRequest(). + */ public function matchRequest(Request $request) { $matcher = $this->matcher; return $matcher($request); diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php deleted file mode 100644 index 1592cbf..0000000 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php +++ /dev/null @@ -1,59 +0,0 @@ -routes = $routes; - } - - /** - * Matches a request against multiple routes. - * - * @param Request $request - * A Request object against which to match. - * - * @return RouteCollection - * A RouteCollection of matched routes. - */ - public function matchRequestPartial(Request $request) { - // For now for testing we'll just do a straight string match. - - $path = $request->getPathInfo(); - - $return = new RouteCollection(); - - foreach ($this->routes as $name => $route) { - if ($route->getPattern() == $path) { - $return->add($name, $route); - } - } - - return $return; - } - - -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockRouteProvider.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockRouteProvider.php new file mode 100644 index 0000000..8cdf57e --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MockRouteProvider.php @@ -0,0 +1,70 @@ +routes = $routes; + } + + /** + * Implements \Symfony\Cmf\Component\Routing\RouteProviderInterface::getRouteCollectionForRequest(). + * + * Not implemented at present as it is not needed. + */ + public function getRouteCollectionForRequest(Request $request) { + + } + + /** + * Implements \Symfony\Cmf\Component\Routing\RouteProviderInterface::getRouteByName(). + */ + public function getRouteByName($name, $parameters = array()) { + $routes = $this->getRoutesByNames(array($name), $parameters); + if (empty($routes)) { + throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); + } + + return reset($routes); + } + + /** + * Implements \Symfony\Cmf\Component\Routing\RouteProviderInterface::getRoutesByName(). + */ + public function getRoutesByNames($names, $parameters = array()) { + $routes = array(); + foreach ($names as $name) { + $routes[] = $this->routes->get($name); + } + + return $routes; + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php deleted file mode 100644 index de29538..0000000 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php +++ /dev/null @@ -1,65 +0,0 @@ - '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']->getOption('_name'), 'route_a', 'The correct matching route was found.'); - } -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouteProviderTest.php similarity index 81% rename from core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php rename to core/modules/system/lib/Drupal/system/Tests/Routing/RouteProviderTest.php index 6769107..310a4e6 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouteProviderTest.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\system\Tests\Routing\PartialMatcherTest. + * Contains Drupal\system\Tests\Routing\RouteProviderTest. */ namespace Drupal\system\Tests\Routing; @@ -13,16 +13,16 @@ use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Drupal\simpletest\UnitTestBase; -use Drupal\Core\Routing\PathMatcher; +use Drupal\Core\Routing\RouteProvider; use Drupal\Core\Database\Database; use Drupal\Core\Routing\MatcherDumper; use Exception; /** - * Basic tests for the UrlMatcherDumper. + * Basic tests for the RouteProvider. */ -class PathMatcherTest extends UnitTestBase { +class RouteProviderTest extends UnitTestBase { /** * A collection of shared fixture data for tests. @@ -33,8 +33,8 @@ class PathMatcherTest extends UnitTestBase { public static function getInfo() { return array( - 'name' => 'Path matcher tests', - 'description' => 'Confirm that the path matching library is working correctly.', + 'name' => 'Route Provider tests', + 'description' => 'Confirm that the default route provider is working correctly.', 'group' => 'Routing', ); } @@ -57,11 +57,11 @@ public function tearDown() { public function testCandidateOutlines() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection); + $provider = new RouteProvider($connection); $parts = array('node', '5', 'edit'); - $candidates = $matcher->getCandidateOutlines($parts); + $candidates = $provider->getCandidateOutlines($parts); $candidates = array_flip($candidates); @@ -77,7 +77,7 @@ public function testCandidateOutlines() { */ function testExactPathMatch() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection, 'test_routes'); + $provider = new RouteProvider($connection, 'test_routes'); $this->fixtures->createTables($connection); @@ -89,7 +89,7 @@ function testExactPathMatch() { $request = Request::create($path, 'GET'); - $routes = $matcher->matchRequestPartial($request); + $routes = $provider->getRouteCollectionForRequest($request); foreach ($routes as $route) { $this->assertEqual($route->getPattern(), $path, 'Found path has correct pattern'); @@ -101,7 +101,7 @@ function testExactPathMatch() { */ function testOutlinePathMatch() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection, 'test_routes'); + $provider = new RouteProvider($connection, 'test_routes'); $this->fixtures->createTables($connection); @@ -113,14 +113,14 @@ function testOutlinePathMatch() { $request = Request::create($path, 'GET'); - $routes = $matcher->matchRequestPartial($request); + $routes = $provider->getRouteCollectionForRequest($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->assertEqual(count($routes), 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.'); } @@ -130,7 +130,7 @@ function testOutlinePathMatch() { */ function testOutlinePathMatchTrailingSlash() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection, 'test_routes'); + $provider = new RouteProvider($connection, 'test_routes'); $this->fixtures->createTables($connection); @@ -142,14 +142,14 @@ function testOutlinePathMatchTrailingSlash() { $request = Request::create($path, 'GET'); - $routes = $matcher->matchRequestPartial($request); + $routes = $provider->getRouteCollectionForRequest($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->assertEqual(count($routes), 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.'); } @@ -159,7 +159,7 @@ function testOutlinePathMatchTrailingSlash() { */ function testOutlinePathMatchDefaults() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection, 'test_routes'); + $provider = new RouteProvider($connection, 'test_routes'); $this->fixtures->createTables($connection); @@ -177,15 +177,14 @@ function testOutlinePathMatchDefaults() { $request = Request::create($path, 'GET'); try { - $routes = $matcher->matchRequestPartial($request); + $routes = $provider->getRouteCollectionForRequest($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->assertEqual(count($routes), 1, 'The correct number of routes was found.'); $this->assertNotNull($routes->get('poink'), 'The first matching route was found.'); } catch (ResourceNotFoundException $e) { @@ -198,7 +197,7 @@ function testOutlinePathMatchDefaults() { */ function testOutlinePathMatchDefaultsCollision() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection, 'test_routes'); + $provider = new RouteProvider($connection, 'test_routes'); $this->fixtures->createTables($connection); @@ -217,15 +216,14 @@ function testOutlinePathMatchDefaultsCollision() { $request = Request::create($path, 'GET'); try { - $routes = $matcher->matchRequestPartial($request); + $routes = $provider->getRouteCollectionForRequest($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->assertEqual(count($routes), 1, 'The correct number of routes was found.'); $this->assertNotNull($routes->get('poink'), 'The first matching route was found.'); } catch (ResourceNotFoundException $e) { @@ -238,7 +236,7 @@ function testOutlinePathMatchDefaultsCollision() { */ function testOutlinePathMatchDefaultsCollision2() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection, 'test_routes'); + $provider = new RouteProvider($connection, 'test_routes'); $this->fixtures->createTables($connection); @@ -257,14 +255,14 @@ function testOutlinePathMatchDefaultsCollision2() { $request = Request::create($path, 'GET'); try { - $routes = $matcher->matchRequestPartial($request); + $routes = $provider->getRouteCollectionForRequest($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->assertEqual(count($routes), 1, 'The correct number of routes was found.'); $this->assertNotNull($routes->get('narf'), 'The first matching route was found.'); } catch (ResourceNotFoundException $e) { @@ -277,7 +275,7 @@ function testOutlinePathMatchDefaultsCollision2() { */ function testOutlinePathNoMatch() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection, 'test_routes'); + $provider = new RouteProvider($connection, 'test_routes'); $this->fixtures->createTables($connection); @@ -290,13 +288,12 @@ function testOutlinePathNoMatch() { $request = Request::create($path, 'GET'); try { - $routes = $matcher->matchRequestPartial($request); + $routes = $provider->getRouteCollectionForRequest($request); $this->fail(t('No exception was thrown.')); } catch (Exception $e) { $this->assertTrue($e instanceof ResourceNotFoundException, 'The correct exception was thrown.'); } - } /** @@ -304,7 +301,7 @@ function testOutlinePathNoMatch() { */ function testSystemPathMatch() { $connection = Database::getConnection(); - $matcher = new PathMatcher($connection, 'test_routes'); + $provider = new RouteProvider($connection, 'test_routes'); $this->fixtures->createTables($connection); @@ -315,7 +312,7 @@ function testSystemPathMatch() { $request = Request::create('/path/one', 'GET'); $request->attributes->set('system_path', 'path/two'); - $routes = $matcher->matchRequestPartial($request); + $routes = $provider->getRouteCollectionForRequest($request); foreach ($routes as $route) { $this->assertEqual($route->getPattern(), '/path/two', 'Found path has correct pattern'); diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php index 4fc7a11..6e808e7 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php @@ -10,7 +10,6 @@ use Symfony\Component\Routing\Route; use Drupal\simpletest\UnitTestBase; -use Drupal\Core\Database\Database; /** * Basic tests for the Route. diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php new file mode 100644 index 0000000..0e9f13e --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php @@ -0,0 +1,76 @@ + 'UrlGenerator', + 'description' => 'Confirm that the UrlGenerator is functioning properly.', + 'group' => 'Routing', + ); + } + + function setUp() { + parent::setUp(); + + $routes = new RouteCollection(); + $routes->add('test_1', new Route('/test/one')); + $routes->add('test_2', new Route('/test/two/{narf}')); + $provider = new MockRouteProvider($routes); + + $this->aliasManager = new MockAliasManager(); + $this->aliasManager->addAlias('test/one', 'hello/world'); + + $context = new RequestContext(); + $context->fromRequest(Request::create('/some/path')); + + $generator = new UrlGenerator($provider, $this->aliasManager); + $generator->setContext($context); + + $this->generator = $generator; + } + + /** + * Confirms that generated routes will have aliased paths. + */ + public function testAliasGeneration() { + $url = $this->generator->generate('test_1'); + + $this->assertEqual($url, '/hello/world', 'Correct URL generated including alias.'); + } + + /** + * Confirms that generated routes will have aliased paths. + */ + public function testAliasGenerationWithParameters() { + $this->aliasManager->addAlias('test/two/5', 'goodbye/cruel/world'); + + $url = $this->generator->generate('test_2', array('narf' => '5')); + + $this->assertEqual($url, '/goodbye/cruel/world', 'Correct URL generated including alias and parameters.'); + } + +} diff --git a/core/vendor/symfony-cmf/routing/Symfony/Cmf/Component/Routing/ProviderBasedGenerator.php b/core/vendor/symfony-cmf/routing/Symfony/Cmf/Component/Routing/ProviderBasedGenerator.php index 1c353f8..6492667 100644 --- a/core/vendor/symfony-cmf/routing/Symfony/Cmf/Component/Routing/ProviderBasedGenerator.php +++ b/core/vendor/symfony-cmf/routing/Symfony/Cmf/Component/Routing/ProviderBasedGenerator.php @@ -6,7 +6,6 @@ use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Generator\UrlGenerator; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Log\LoggerInterface; use Symfony\Cmf\Component\Routing\RouteProviderInterface;