diff --git a/core/includes/common.inc b/core/includes/common.inc index 9a99bc3..6ffd528 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -4,6 +4,7 @@ use Drupal\Component\Utility\NestedArray; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Drupal\Core\Cache\CacheBackendInterface; +use Symfony\Component\DependencyInjection\Container; use Drupal\Core\Database\Database; use Drupal\Core\Template\Attribute; @@ -7090,6 +7091,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. + router_rebuild(); menu_router_rebuild(); // Re-initialize the maintenance theme, if the current request attempted to @@ -7101,6 +7103,23 @@ function drupal_flush_all_caches() { } } +function router_rebuild() { + // We need to manually call each module so that we can know which module + // a given item came from. + $callbacks = array(); + + $dumper = drupal_container()->get('router.dumper', Container::NULL_ON_INVALID_REFERENCE); + + if ($dumper) { + foreach (module_implements('route_info') as $module) { + $routes = call_user_func($module . '_route_info'); + drupal_alter('router_info', $routes); + $dumper->addRoutes($routes); + $dumper->dump(array('route_set' => $module)); + } + } +} + /** * Changes the dummy query string added to all CSS and JavaScript files. * diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 2c5d180..19a45cc 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -15,6 +15,8 @@ use Symfony\Component\DependencyInjection\Scope; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Drupal\Core\Database\Database; + /** * Bundle class for mandatory core services. * @@ -52,12 +54,23 @@ class CoreBundle extends Bundle ->setFactoryMethod('getConnection') ->addArgument('slave'); + $container->register('router.dumper', '\Drupal\Core\Routing\MatcherDumper') + ->addArgument(new Reference('database')); + // @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()); diff --git a/core/lib/Drupal/Core/LegacyUrlMatcher.php b/core/lib/Drupal/Core/LegacyUrlMatcher.php index 8828f36..48987a9 100644 --- a/core/lib/Drupal/Core/LegacyUrlMatcher.php +++ b/core/lib/Drupal/Core/LegacyUrlMatcher.php @@ -9,13 +9,14 @@ namespace Drupal\Core; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Symfony\Component\Routing\Matcher\UrlMatcherInterface; +use Symfony\Component\Routing\Matcher\RequestMatcherInterface; +use Symfony\Component\Routing\RequestContextAwareInterface; use Symfony\Component\Routing\RequestContext; /** * UrlMatcher matches URL based on a set of routes. */ -class LegacyUrlMatcher implements UrlMatcherInterface { +class LegacyUrlMatcher implements RequestMatcherInterface, RequestContextAwareInterface { /** * The request context for this matcher. @@ -98,8 +99,8 @@ class LegacyUrlMatcher implements UrlMatcherInterface { * * @api */ - public function match($pathinfo) { - if ($router_item = $this->matchDrupalItem($pathinfo)) { + public function matchRequest(Request $request) { + if ($router_item = $this->matchDrupalItem($request->attributes->get('system_path'))) { $ret = $this->convertDrupalItem($router_item); // Stash the router item in the attributes while we're transitioning. $ret['drupal_menu_item'] = $router_item; diff --git a/core/lib/Drupal/Core/Routing/ChainMatcher.php b/core/lib/Drupal/Core/Routing/ChainMatcher.php new file mode 100644 index 0000000..cf8a66d --- /dev/null +++ b/core/lib/Drupal/Core/Routing/ChainMatcher.php @@ -0,0 +1,159 @@ +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. + * + * @api + */ + 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 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) { + $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 + * The priority of the matcher. Higher number matchers will be checked + * first. + */ + public function add(RequestMatcherInterface $matcher, $priority = 0) { + if (empty($this->matchers[$priority])) { + $this->matchers[$priority] = array(); + } + + $this->matchers[$priority][] = $matcher; + $this->sortedMatchers = array(); + } + + /** + * Sorts the matchers and flattens them. + * + * @return array + * An array of RequestMatcherInterface objects. + */ + public function all() { + if (empty($this->sortedMatchers)) { + $this->sortedMatchers = $this->sortMatchers(); + } + + return $this->sortedMatchers; + } + + /** + * Sort matchers by priority. + * + * The highest priority number is the highest priority (reverse sorting). + * + * @return \Symfony\Component\Routing\RequestMatcherInterface[] + * An array of Matcher objects in the order they should be used. + */ + protected function sortMatchers() { + $sortedMatchers = array(); + krsort($this->matchers); + + foreach ($this->matchers as $matchers) { + $sortedMatchers = array_merge($sortedMatchers, $matchers); + } + + return $sortedMatchers; + } + +} diff --git a/core/lib/Drupal/Core/Routing/CompiledRoute.php b/core/lib/Drupal/Core/Routing/CompiledRoute.php new file mode 100644 index 0000000..d8e93f8 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/CompiledRoute.php @@ -0,0 +1,150 @@ +route = $route; + $this->fit = $fit; + $this->patternOutline = $pattern_outline; + $this->numParts = $num_parts; + } + + /** + * 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 Route instance. + * + * @return Route + * A Route instance + */ + public function getRoute() { + return $this->route; + } + + /** + * Returns the pattern. + * + * @return string The pattern + */ + public function getPattern() { + return $this->route->getPattern(); + } + + /** + * Returns the options. + * + * @return array The options + */ + public function getOptions() { + return $this->route->getOptions(); + } + + /** + * Returns the defaults. + * + * @return array The defaults + */ + public function getDefaults() { + return $this->route->getDefaults(); + } + + /** + * Returns the requirements. + * + * @return array The requirements + */ + public function getRequirements() { + return $this->route->getRequirements(); + } + +} + diff --git a/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php b/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php new file mode 100644 index 0000000..ae2bba0 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/FinalMatcherInterface.php @@ -0,0 +1,34 @@ +routes = $collection; + + return $this; + } + + + public function matchRequest(Request $request) { + // Return whatever the first route in the collection is. + foreach ($this->routes as $name => $route) { + return array_merge($this->mergeDefaults(array(), $route->getDefaults()), array('_route' => $name)); + } + } + + /** + * Get merged default parameters. + * + * @param array $params + * The parameters + * @param array $defaults + * The defaults + * + * @return array + * Merged default parameters + */ + protected function mergeDefaults($params, $defaults) { + $parameters = $defaults; + foreach ($params as $key => $value) { + if (!is_int($key)) { + $parameters[$key] = $value; + } + } + + return $parameters; + } + +} + diff --git a/core/lib/Drupal/Core/Routing/Generator.php b/core/lib/Drupal/Core/Routing/Generator.php new file mode 100644 index 0000000..136940e --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Generator.php @@ -0,0 +1,100 @@ +connection = $connection; + $this->tableName = $table; + } + + /** + * Generates a URL from the given parameters. + * + * If the generator is not able to generate the url, it must throw the RouteNotFoundException + * as documented below. + * + * @param string $name The name of the route + * @param mixed $parameters An array of parameters + * @param Boolean $absolute Whether to generate an absolute URL + * + * @return string The generated URL + * + * @throws RouteNotFoundException if route doesn't exist + * + * @api + */ + public function generate($name, $parameters = array(), $absolute = false) { + $route = $this->connection->select($this->tableName, 'r') + ->fields('r', array('route')) + ->condition('name', $name) + ->execute() + ->fetchField(); + + if (!$route) { + throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); + } + + $route = unserialize($route); + + debug($route); + + $path = $route->getPattern(); + + $url = $this->context->getBaseUrl() . $path; + + return $url; + + } + + /** + * {@inheritdoc} + */ + public function setContext(RequestContext $context) { + $this->context = $context; + } + + /** + * {@inheritdoc} + */ + public function getContext() { + return $this->context; + } + +} + diff --git a/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php b/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php new file mode 100644 index 0000000..b0a1878 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/HttpMethodMatcher.php @@ -0,0 +1,53 @@ +getMethod(); + + $collection = new RouteCollection(); + + foreach ($this->routes->all() as $name => $route) { + // _method could be a |-delimited list of allowed methods, or null. If + // null, we accept any method. + $allowed_methods = array_filter(explode('|', strtoupper($route->getRequirement('_method')))); + if (empty($allowed_methods) || in_array($method, $allowed_methods)) { + $collection->add($name, $route); + } + else { + // Build a list of methods that would have matched. Note that we only + // need to do this if a route doesn't match, because if even one route + // passes then we'll never throw the exception that needs this array. + $possible_methods += $allowed_methods; + } + } + + if (!count($collection->all())) { + throw new MethodNotAllowedException(array_unique($possible_methods)); + } + + return $collection; + } + +} + diff --git a/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php b/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php new file mode 100644 index 0000000..a08cb12 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/InitialMatcherInterface.php @@ -0,0 +1,22 @@ +connection = $connection; + + $this->tableName = $table; + } + + /** + * Adds additional routes to be dumped. + * + * @param RouteCollection $routes + */ + 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 $options array + * $options An array of options + */ + public function dump(array $options = array()) { + $options += array( + 'route_set' => '', + ); + + //$compiled = $this->compileRoutes($this->routes, $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(), + // This is only temporary. We need to strip off the compiled route from + // route object in order to serialize it. Cloning strips off the + // compiled route object. Remove this once + // https://github.com/symfony/symfony/pull/4755 is merged and brought + // back downstream. + 'route' => serialize(clone($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 RouteCollection + * A RouteCollection instance representing all routes currently in the + * dumper. + */ + public function getRoutes() { + return $this->routes; + } + + protected function compileRoutes(RouteCollection $routes, $route_set) { + + // First pass: separate callbacks from paths, making paths ready for + // matching. Calculate fitness, and fill some default values. + $menu = array(); + $masks = array(); + foreach ($routes as $name => $item) { + $path = $item->getPattern(); + $move = FALSE; + + $parts = explode('/', $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; + + $num_placeholders = count(array_filter($parts, function($value) { + return strpos($value, '{') !== FALSE; + })); + + $fit = $this->getFit($path); + + if ($fit) { + $move = TRUE; + } + else { + // If there is no placeholder, it fits maximally. + $fit = (1 << $number_parts) - 1; + } + + $masks[$fit] = 1; + $item += array( + 'title' => '', + 'weight' => 0, + 'type' => MENU_NORMAL_ITEM, + 'module' => '', + '_number_parts' => $number_parts, + '_parts' => $parts, + '_fit' => $fit, + ); + + if ($move) { + $new_path = implode('/', $item['_parts']); + $menu[$new_path] = $item; + $sort[$new_path] = $number_parts; + } + else { + $menu[$path] = $item; + $sort[$path] = $number_parts; + } + } + + // Sort the route list. + array_multisort($sort, SORT_NUMERIC, $menu); + // Apply inheritance rules. + foreach ($menu as $path => $v) { + $item = &$menu[$path]; + + for ($i = $item['_number_parts'] - 1; $i; $i--) { + $parent_path = implode('/', array_slice($item['_parts'], 0, $i)); + if (isset($menu[$parent_path])) { + + $parent = &$menu[$parent_path]; + + // If an access callback is not found for a default local task we use + // the callback from the parent, since we expect them to be identical. + // In all other cases, the access parameters must be specified. + if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) { + $item['access callback'] = $parent['access callback']; + if (!isset($item['access arguments']) && isset($parent['access arguments'])) { + $item['access arguments'] = $parent['access arguments']; + } + } + + // Same for theme callbacks. + if (!isset($item['theme callback']) && isset($parent['theme callback'])) { + $item['theme callback'] = $parent['theme callback']; + if (!isset($item['theme arguments']) && isset($parent['theme arguments'])) { + $item['theme arguments'] = $parent['theme arguments']; + } + } + } + } + if (!isset($item['access callback']) && isset($item['access arguments'])) { + // Default callback. + $item['access callback'] = 'user_access'; + } + if (!isset($item['access callback']) || empty($item['page callback'])) { + $item['access callback'] = 0; + } + if (is_bool($item['access callback'])) { + $item['access callback'] = intval($item['access callback']); + } + + $item += array( + 'access arguments' => array(), + 'access callback' => '', + 'page arguments' => array(), + 'page callback' => '', + 'delivery callback' => '', + 'title arguments' => array(), + 'title callback' => 't', + 'theme arguments' => array(), + 'theme callback' => '', + 'description' => '', + 'position' => '', + 'context' => 0, + 'tab_parent' => '', + 'tab_root' => $path, + 'path' => $path, + 'file' => '', + 'file path' => '', + 'include file' => '', + ); + + // Calculate out the file to be included for each callback, if any. + if ($item['file']) { + $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']); + $item['include file'] = $file_path . '/' . $item['file']; + } + } + + // Sort the masks so they are in order of descending fit. + $masks = array_keys($masks); + rsort($masks); + + return array($menu, $masks); + + + // The old menu_router record structure, copied here for easy referencing. + array( + 'path' => $item['path'], + 'load_functions' => $item['load_functions'], + 'to_arg_functions' => $item['to_arg_functions'], + 'access_callback' => $item['access callback'], + 'access_arguments' => serialize($item['access arguments']), + 'page_callback' => $item['page callback'], + 'page_arguments' => serialize($item['page arguments']), + 'delivery_callback' => $item['delivery callback'], + 'fit' => $item['_fit'], + 'number_parts' => $item['_number_parts'], + 'context' => $item['context'], + 'tab_parent' => $item['tab_parent'], + 'tab_root' => $item['tab_root'], + 'title' => $item['title'], + 'title_callback' => $item['title callback'], + 'title_arguments' => ($item['title arguments'] ? serialize($item['title arguments']) : ''), + 'theme_callback' => $item['theme callback'], + 'theme_arguments' => serialize($item['theme arguments']), + 'type' => $item['type'], + 'description' => $item['description'], + 'position' => $item['position'], + 'weight' => $item['weight'], + 'include_file' => $item['include file'], + ); + } + + /** + * Determines the fitness of the provided path. + * + * @param string $path + * The path whose fitness we want. + * + * @return int + * The fitness of the path, as an integer. + */ + public function getFit($path) { + $fit = 0; + + $parts = explode('/', $path, static::MAX_PARTS); + foreach ($parts as $k => $part) { + if (strpos($part, '{') === FALSE) { + $fit |= 1 << ($slashes - $k); + } + } + + return $fit; + } +} + diff --git a/core/lib/Drupal/Core/Routing/NestedMatcher.php b/core/lib/Drupal/Core/Routing/NestedMatcher.php new file mode 100644 index 0000000..db8a429 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/NestedMatcher.php @@ -0,0 +1,149 @@ +partialMatchers[] = $matcher; + + return $this; + } + + /** + * Sets the final matcher for the matching plan. + * + * @param UrlMatcherInterface $final + * The matcher that will be called last to ensure only a single route is + * found. + * + * @return 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 InitialMatcherInterface $matcher + * An initial matcher. It is responsible for its own configuration and + * initial route collection + * + * @return 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 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->partialMatchers as $matcher) { + if ($collection) { + $matcher->setCollection($collection); + } + $collection = $matcher->matchRequestPartial($request); + } + + $attributes = $this->finalMatcher->setCollection($collection)->matchRequest($request); + + return $attributes; + } + + /** + * Sets the request context. + * + * This method is unused. It is here only to satisfy the interface. + * + * @param 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 RequestContext The context + */ + public function getContext() { + return $this->context; + } + +} + diff --git a/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php b/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php new file mode 100644 index 0000000..cd55d32 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/NestedMatcherInterface.php @@ -0,0 +1,50 @@ +routes = $collection; + + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php b/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php new file mode 100644 index 0000000..1b234e8 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/PartialMatcherInterface.php @@ -0,0 +1,34 @@ +connection = $connection; + $this->tableName = $table; + } + + /** + * 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) { + + $path = $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->tableName}} WHERE pattern_outline IN (:patterns) ORDER BY fit", array( + ':patterns' => $ancestors, + )) + ->fetchAllKeyed(); + + $collection = new RouteCollection(); + foreach ($routes as $name => $route) { + $collection->add($name, unserialize($route)); + } + + if (!count($collection->all())) { + 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); + $length = $number_parts - 1; + $end = (1 << $number_parts) - 1; + $candidates = array(); + + $start = pow($number_parts-1, 2); + + // 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, $start); + + foreach ($masks as $i) { + $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 .= '/'; + } + } + $candidates[] = $current; + } + + return $candidates; + } +} + diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php new file mode 100644 index 0000000..a34e5c8 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php @@ -0,0 +1,83 @@ +getFit($route->getPattern()); + + $pattern_outline = $this->getPatternOutline($route->getPattern()); + + $num_parts = count(explode('/', trim($pattern_outline, '/'))); + + return new CompiledRoute($route, $fit, $pattern_outline, $num_parts); + + } + + /** + * Returns the pattern outline. + * + * The pattern outline is the path pattern but normalized so that all + * placeholders are equal strings. + * + * @param string $path + * The path pattern to normalize to an 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; + } +} + diff --git a/core/lib/Drupal/Core/Routing/Router.php b/core/lib/Drupal/Core/Routing/Router.php new file mode 100644 index 0000000..e7e82b5 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Router.php @@ -0,0 +1,42 @@ +matcher = $matcher; + $this->generator = $generator; + } + + // The following are just to satisfy the interface for now. They'll get + // refactored once we figure out how to break RouterInterface from UrlMatcher. + + public function getRouteCollection() {} + + public function match($pathinfo) {} + + public function generate($name, $parameters = array(), $absolute = false) {} + + public function setContext(RequestContext $context) {} + + public function getContext() {} + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php new file mode 100644 index 0000000..cec4d79 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/ChainMatcherTest.php @@ -0,0 +1,111 @@ + '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/GeneratorTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/GeneratorTest.php new file mode 100644 index 0000000..2e638b0 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/GeneratorTest.php @@ -0,0 +1,133 @@ + 'Generator tests', + 'description' => 'Confirm that the Generator is functioning properly.', + 'group' => 'Routing', + ); + } + + function __construct($test_id = NULL) { + parent::__construct($test_id); + + $this->fixtures = new RoutingFixtures(); + } + + function setUp() { + parent::setUp(); + } + + public function tearDown() { + $this->fixtures->dropTables(Database::getConnection()); + + parent::tearDown(); + } + + /** + * Confirms that we can generate a static link successfully. + */ + function testLinkGenerate() { + $base_path = '/base-path'; + + $connection = Database::getConnection(); + + $this->fixtures->createTables($connection); + $collection = $this->fixtures->sampleRouteCollection(); + + $dumper= new MatcherDumper($connection, 'test_routes'); + $dumper->addRoutes($collection); + $dumper->dump(array('route_set' => 'test')); + + $generator = new Generator($connection, 'test_routes'); + + $context = new RequestContext($base_path, 'GET', 'mydomain'); + + $generator->setContext($context); + $url = $generator->generate('route_e'); + + $this->assertEqual($url, $base_path . '/path/two', 'Correct path generated for static path pattern.'); + } + + /** + * Confirms that we can generate a static link successfully. + */ + /* + function testDynamicLinkGenerate() { + $base_path = '/base-path'; + + $connection = Database::getConnection(); + + $this->fixtures->createTables($connection); + $collection = $this->fixtures->complexRouteCollection(); + + $dumper= new MatcherDumper($connection, 'test_routes'); + $dumper->addRoutes($collection); + $dumper->dump(array('route_set' => 'test')); + + $generator = new Generator($connection, 'test_routes'); + + $context = new RequestContext($base_path, 'GET', 'mydomain'); + + $generator->setContext($context); + $url = $generator->generate('route_b', array('thing' => 'MAGIC')); + + debug($url, 'Generated path'); + + $this->assertEqual($url, $base_path . '/path/MAGIC/one', 'Correct path generated for dynamic path pattern.'); + } + */ + + /** + * Confirms that an invalid route generates an exception. + */ + function testMissingRouteGenerate() { + $connection = Database::getConnection(); + + $this->fixtures->createTables($connection); + + $generator = new Generator($connection, 'test_routes'); + + try { + $generator->generate('non_existent_route'); + $this->fail('No exception thrown for missing route.'); + } + catch (RouteNotFoundException $e) { + $this->pass('Correct exception thrown for missing route.'); + } + catch (Exception $e) { + $this->fail('Incorrect exception type thrown for missing route.'); + } + + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php new file mode 100644 index 0000000..962abfc --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/HttpMethodMatcherTest.php @@ -0,0 +1,109 @@ + '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(); + } + public function setUp() { + parent::setUp(); + } + + /** + * Confirms that the HttpMethod matcher matches properly. + */ + public function testFilterRoutes() { + + $matcher = new HttpMethodMatcher(); + $matcher->setCollection($this->fixtures->sampleRouteCollection()); + + $routes = $matcher->matchRequestPartial(Request::create('path/one', 'GET')); + + $this->assertEqual(count($routes->all()), 4, 'The correct number of routes was found.'); + $this->assertNotNull($routes->get('route_a'), 'The first matching route was found.'); + $this->assertNull($routes->get('route_b'), 'The non-matching route was not found.'); + $this->assertNotNull($routes->get('route_c'), 'The second matching route was found.'); + $this->assertNotNull($routes->get('route_d'), 'The all-matching route was found.'); + $this->assertNotNull($routes->get('route_e'), 'The multi-matching route was found.'); + } + + /** + * Confirms we can nest multiple partial matchers. + */ + public function testNestedMatcher() { + + $matcher = new NestedMatcher(); + + $matcher->setInitialMatcher(new MockPathMatcher($this->fixtures->sampleRouteCollection())); + $matcher->addPartialMatcher(new HttpMethodMatcher()); + $matcher->setFinalMatcher(new FirstEntryFinalMatcher()); + + $request = Request::create('/path/one', 'GET'); + + $attributes = $matcher->matchRequest($request); + + $this->assertEqual($attributes['_route'], 'route_a', 'The correct matching route was found.'); + } + + /** + * Confirms that the HttpMethod matcher throws an exception for no-route. + */ + public function testNoRouteFound() { + $matcher = new HttpMethodMatcher(); + + // Remove the sample route that would match any method. + $routes = $this->fixtures->sampleRouteCollection(); + $routes->remove('route_d'); + + $matcher->setCollection($routes); + + try { + $routes = $matcher->matchRequestPartial(Request::create('path/one', 'DELETE')); + $this->fail(t('No exception was thrown.')); + } + catch (Exception $e) { + $this->assertTrue($e instanceof MethodNotAllowedException, 'The correct exception was thrown.'); + } + + } +} + diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php new file mode 100644 index 0000000..7f6f312 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MatcherDumperTest.php @@ -0,0 +1,144 @@ + 'Dumper tests', + 'description' => 'Confirm that the matcher dumper is functioning properly.', + 'group' => 'Routing', + ); + } + + function __construct($test_id = NULL) { + parent::__construct($test_id); + + $this->fixtures = new RoutingFixtures(); + } + + function setUp() { + parent::setUp(); + } + + /** + * Confirms that the dumper can be instantiated successfuly. + */ + function testCreate() { + $connection = Database::getConnection(); + $dumper= new MatcherDumper($connection); + + $class_name = 'Drupal\Core\Routing\MatcherDumper'; + $this->assertTrue($dumper instanceof $class_name, 'Dumper created successfully'); + } + + /** + * Confirms that we can add routes to the dumper. + */ + function testAddRoutes() { + $connection = Database::getConnection(); + $dumper= new MatcherDumper($connection); + + $route = new Route('test'); + $collection = new RouteCollection(); + $collection->add('test_route', $route); + + $dumper->addRoutes($collection); + + $dumper_routes = $dumper->getRoutes()->all(); + $collection_routes = $collection->all(); + + foreach ($dumper_routes as $name => $route) { + $this->assertEqual($route->getPattern(), $collection_routes[$name]->getPattern(), 'Routes match'); + } + } + + /** + * Confirms that we can add routes to the dumper when it already has some. + */ + function testAddAdditionalRoutes() { + $connection = Database::getConnection(); + $dumper= new MatcherDumper($connection); + + $route = new Route('test'); + $collection = new RouteCollection(); + $collection->add('test_route', $route); + $dumper->addRoutes($collection); + + $route = new Route('test2'); + $collection2 = new RouteCollection(); + $collection2->add('test_route2', $route); + $dumper->addRoutes($collection2); + + // Merge the two collections together so we can test them. + $collection->addCollection(clone $collection2); + + $dumper_routes = $dumper->getRoutes()->all(); + $collection_routes = $collection->all(); + + $success = TRUE; + foreach ($collection_routes as $name => $route) { + if (empty($dumper_routes[$name])) { + $success = FALSE; + $this->fail(t('Not all routes found in the dumper.')); + } + } + + if ($success) { + $this->pass('All routes found in the dumper.'); + } + } + + /** + * Confirm that we can dump a route collection to the database. + */ + public function testDump() { + $connection = Database::getConnection(); + $dumper= new MatcherDumper($connection, 'test_routes'); + + $route = new Route('/test/{my}/path'); + $route->setOption('compiler_class', 'Drupal\Core\Routing\RouteCompiler'); + $collection = new RouteCollection(); + $collection->add('test_route', $route); + + $dumper->addRoutes($collection); + + $this->fixtures->createTables($connection); + + $dumper->dump(array('route_set' => 'test')); + + $record = $connection->query("SELECT * FROM {test_routes} WHERE name= :name", array(':name' => 'test_route'))->fetchObject(); + + $loaded_route = unserialize($record->route); + + $this->assertEqual($record->name, 'test_route', 'Dumped route has correct name.'); + $this->assertEqual($record->pattern, '/test/{my}/path', 'Dumped route has correct pattern.'); + $this->assertEqual($record->pattern_outline, '/test/%/path', 'Dumped route has correct pattern outline.'); + $this->assertEqual($record->fit, 5 /* 101 in binary */, 'Dumped route has correct fit.'); + $this->assertTrue($loaded_route instanceof Route, 'Route object retrieved successfully.'); + + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php new file mode 100644 index 0000000..6cd58fc --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MockMatcher.php @@ -0,0 +1,35 @@ +matcher = $matcher; + } + + public function matchRequest(Request $request) { + $matcher = $this->matcher; + return $matcher($request); + } +} + diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php b/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php new file mode 100644 index 0000000..b545ebe --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/MockPathMatcher.php @@ -0,0 +1,50 @@ +routes = $routes; + } + + /** + * Matches a request against multiple routes. + * + * @param Request $request + * A Request object against which to match. + * + * @return RouteCollection + * A RouteCollection of matched routes. + */ + public function matchRequestPartial(Request $request) { + // For now for testing we'll just do a straight string match. + + $path = $request->getPathInfo(); + + $return = new RouteCollection(); + + foreach ($this->routes as $name => $route) { + if ($route->getPattern() == $path) { + $return->add($name, $route); + } + } + + return $return; + } + + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php new file mode 100644 index 0000000..9c0f5de --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/NestedMatcherTest.php @@ -0,0 +1,69 @@ + '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(); + } + public function setUp() { + parent::setUp(); + } + + /** + * 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.'); + } +} + diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php new file mode 100644 index 0000000..055a1e7 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/PathMatcherTest.php @@ -0,0 +1,157 @@ + '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); + + //debug($candidates); + + $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.'); + } + + /** + * Confirm that an exception is thrown when no matching path is found. + */ + function testOutlinePathNoMatch() { + $connection = Database::getConnection(); + $matcher = new PathMatcher($connection, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, 'test_routes'); + $dumper->addRoutes($this->fixtures->complexRouteCollection()); + $dumper->dump(); + + $path = '/no/such/path'; + + $request = Request::create($path, 'GET'); + + try { + $routes = $matcher->matchRequestPartial($request); + $this->fail(t('No exception was thrown.')); + } + catch (Exception $e) { + $this->assertTrue($e instanceof ResourceNotFoundException, 'The correct exception was thrown.'); + } + + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php new file mode 100644 index 0000000..7ed212b --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouteTest.php @@ -0,0 +1,41 @@ + 'Routes', + 'description' => 'Confirm that route object is functioning properly.', + 'group' => 'Routing', + ); + } + + function setUp() { + parent::setUp(); + } + + 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.'); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php new file mode 100644 index 0000000..a5449af --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php @@ -0,0 +1,33 @@ + 'Integrated Router tests', + 'description' => 'Function Tests for the fully integrated routing system.', + 'group' => 'Routing', + ); + } + + public function testCanRoute() { + $this->drupalGet('router_test/test1'); + $this->assertRaw('test1', 'The correct string was returned because the route was successful.'); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php new file mode 100644 index 0000000..8937c75 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RoutingFixtures.php @@ -0,0 +1,167 @@ +routingTableDefinition(); + $schema = $connection->schema(); + + foreach ($tables as $name => $table) { + $schema->dropTable($name); + $schema->createTable($name, $table); + } + } + + 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; + } + + public function routingTableDefinition() { + + $tables['test_routes'] = array( + 'description' => 'Maps paths to various callbacks (access, page and title)', + 'fields' => array( + 'name' => array( + 'description' => 'Primary Key: Machine name of this route', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'pattern' => array( + 'description' => 'The path pattern for this URI', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'pattern_outline' => array( + 'description' => 'The pattern', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'route_set' => array( + 'description' => 'The route set grouping to which a route belongs.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'access_callback' => array( + 'description' => 'The callback which determines the access to this router path. Defaults to user_access.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'access_arguments' => array( + 'description' => 'A serialized array of arguments for the access callback.', + 'type' => 'blob', + 'not null' => FALSE, + ), + 'fit' => array( + 'description' => 'A numeric representation of how specific the path is.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'number_parts' => array( + 'description' => 'Number of parts in this router path.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'small', + ), + 'route' => array( + 'description' => 'A serialized Route object', + 'type' => 'text', + ), + ), + 'indexes' => array( + 'fit' => array('fit'), + 'pattern_outline' => array('pattern_outline'), + 'route_set' => array('route_set'), + ), + 'primary key' => array('name'), + ); + + return $tables; + } +} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index c06b8fd..748d40c 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1341,6 +1341,63 @@ function system_schema() { 'primary key' => array('filename'), ); + $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( @@ -1938,6 +1995,76 @@ function system_update_8018() { } /** + * Create the new routing table. + */ +function system_update_8019() { + + $tables['router'] = array( + 'description' => 'Maps paths to various callbacks (access, page and title)', + 'fields' => array( + 'name' => array( + 'description' => 'Primary Key: Machine name of this route', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'pattern' => array( + 'description' => 'The path pattern for this URI', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'pattern_outline' => array( + 'description' => 'The pattern', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'route_set' => array( + 'description' => 'The route set grouping to which a route belongs.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'fit' => array( + 'description' => 'A numeric representation of how specific the path is.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'route' => array( + 'description' => 'A serialized Route object', + 'type' => 'text', + ), + 'number_parts' => array( + 'description' => 'Number of parts in this router path.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'small', + ), + ), + 'indexes' => array( + 'fit' => array('fit'), + 'pattern_outline' => array('pattern_outline'), + 'route_set' => array('route_set'), + ), + 'primary key' => array('name'), + ); + + $schema = Database::getConnection()->schema(); + + $schema->dropTable('router'); + + $schema->createTable('router', $tables['router']); +} + + +/** * @} End of "defgroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php new file mode 100644 index 0000000..1476e46 --- /dev/null +++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php @@ -0,0 +1,17 @@ + '\Drupal\router_test\TestControllers::test1' + )); + $collection->add('router_test_1', $route); + + return $collection; +}