Skip to content
Snippets Groups Projects
Commit 02fd4b23 authored by Larry Garfield's avatar Larry Garfield Committed by Alex Bronstein
Browse files

Port the regex path matching from Symfony to our CompiledRoute class so that...

Port the regex path matching from Symfony to our CompiledRoute class so that we can match placeholder-using paths.
parent 820a633c
Branches
Tags
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
......@@ -44,22 +44,25 @@ class CompiledRoute {
/**
* Constructs a new CompiledRoute object.
*
* @param Route $route
* A original Route instance.
* @param int $fit
* The fitness of the route.
* @param string $fit
* The pattern outline for this route.
* @param int $num_parts
* The number of parts in the path.
*/
public function __construct(Route $route, $fit, $pattern_outline, $num_parts) {
* Constructs a new CompiledRoute object.
*
* @param Route $route
* A original Route instance.
* @param int $fit
* The fitness of the route.
* @param string $fit
* The pattern outline for this route.
* @param int $num_parts
* The number of parts in the path.
* @param string $regex
* The regular expression to match placeholders out of this path.
*/
public function __construct(Route $route, $fit, $pattern_outline, $num_parts, $regex) {
$this->route = $route;
$this->fit = $fit;
$this->patternOutline = $pattern_outline;
$this->numParts = $num_parts;
$this->regex = $regex;
}
/**
......@@ -100,6 +103,16 @@ public function getPatternOutline() {
return $this->patternOutline;
}
/**
* Returns the placeholder regex.
*
* @return string
* The regex to locate placeholders in this pattern.
*/
public function getRegex() {
return $this->regex;
}
/**
* Returns the Route instance.
*
......
......@@ -39,7 +39,14 @@ public function setCollection(RouteCollection $collection) {
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));
$path = '/' . $request->attributes->get('system_path');
$route->setOption('compiler_class', '\Drupal\Core\Routing\RouteCompiler');
$compiled = $route->compile();
preg_match($compiled->getRegex(), $path, $matches);
return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $name));
}
}
......
......@@ -15,6 +15,8 @@ class RouteCompiler implements RouteCompilerInterface {
*/
const MAX_PARTS = 9;
const REGEX_DELIMITER = '#';
/**
* Compiles the current route instance.
*
......@@ -26,17 +28,133 @@ class RouteCompiler implements RouteCompilerInterface {
*/
public function compile(Route $route) {
$fit = $this->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);
$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 Route $route
* The route object.
* @param string $pattern
* The pattern for which we want a matching regex.
* @return type
* @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
$firstOptional = INF;
for ($i = count($tokens) - 1; $i >= 0; $i--) {
$token = $tokens[$i];
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
$firstOptional = $i;
} else {
break;
}
}
// compute the matching regexp
$regexp = '';
for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) {
$regexp .= $this->computeRegexp($tokens, $i, $firstOptional);
}
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 $firstOptional The index of the first optional token
*
* @return string The regexp pattern for a single token
*/
private function computeRegexp(array $tokens, $index, $firstOptional) {
$token = $tokens[$index];
if ('text' === $token[0]) {
// Text tokens
return preg_quote($token[1], self::REGEX_DELIMITER);
} else {
// Variable tokens
if (0 === $index && 0 === $firstOptional) {
// 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 >= $firstOptional) {
// 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 - $firstOptional - (0 === $firstOptional ? 1 : 0));
}
}
return $regexp;
}
}
}
/**
* Returns the pattern outline.
*
......
......@@ -46,4 +46,14 @@ public function testDefaultController() {
$this->assertRaw('</html>', 'Page markup was found.');
}
/**
* Confirms that placeholders in paths work correctly.
*/
public function testDefaultControllerPlaceholders() {
$value = $this->randomName();
$this->drupalGet('router_test/test3/' . $value);
$this->assertRaw($value, 'The correct string was returned because the route was successful.');
$this->assertRaw('</html>', 'Page markup was found.');
}
}
......@@ -22,5 +22,8 @@ public function test2() {
return "test2";
}
}
public function test3($value) {
return $value;
}
}
......@@ -19,5 +19,10 @@ function router_test_route_info() {
));
$collection->add('router_test_2', $route);
$route = new Route('router_test/test3/{value}', array(
'_content' => '\Drupal\router_test\TestControllers::test3'
));
$collection->add('router_test_3', $route);
return $collection;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment