Skip to content

Commit

Permalink
Adds a classic routing strategy.
Browse files Browse the repository at this point in the history
  • Loading branch information
jails committed Feb 18, 2016
1 parent 8164b4a commit 1146765
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 0 deletions.
125 changes: 125 additions & 0 deletions src/DataGenerator/Classic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
namespace FastRoute\DataGenerator;

use FastRoute\DataGenerator;
use FastRoute\BadRouteException;
use FastRoute\Route;

class Classic implements DataGenerator {
protected $staticRoutes = [];
protected $methodToRegexToRoutesMap = [];

public function addRoute($httpMethod, $routeData, $handler) {
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($httpMethod, $routeData, $handler);
} else {
$this->addVariableRoute($httpMethod, $routeData, $handler);
}
}

public function getData() {
if (empty($this->methodToRegexToRoutesMap)) {
return [$this->staticRoutes, []];
}

return [$this->staticRoutes, $this->methodToRegexToRoutesMap];
}

private function isStaticRoute($routeData) {
return count($routeData) == 1 && is_string($routeData[0]);
}

private function addStaticRoute($httpMethod, $routeData, $handler) {
$routeStr = $routeData[0];

if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
throw new BadRouteException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$routeStr, $httpMethod
));
}

if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
if ($route->matches($routeStr)) {
throw new BadRouteException(sprintf(
'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
$routeStr, $route->regex, $httpMethod
));
}
}
}

$this->staticRoutes[$httpMethod][$routeStr] = $handler;
}

private function addVariableRoute($httpMethod, $routeData, $handler) {
list($regex, $variables) = $this->buildRegexForRoute($routeData);

if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
throw new BadRouteException(sprintf(
'Cannot register two routes matching "%s" for method "%s"',
$regex, $httpMethod
));
}

$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
$httpMethod, $handler, $regex, $variables
);
}

private function buildRegexForRoute($routeData) {
$regex = '';
$variables = [];
foreach ($routeData as $part) {
if (is_string($part)) {
$regex .= preg_quote($part, '~');
continue;
}

list($varName, $regexPart) = $part;

if (isset($variables[$varName])) {
throw new BadRouteException(sprintf(
'Cannot use the same placeholder "%s" twice', $varName
));
}

if ($this->regexHasCapturingGroups($regexPart)) {
throw new BadRouteException(sprintf(
'Regex "%s" for parameter "%s" contains a capturing group',
$regexPart, $varName
));
}

$variables[$varName] = $varName;
$regex .= '(' . $regexPart . ')';
}

return [$regex, $variables];
}

private function regexHasCapturingGroups($regex) {
if (false === strpos($regex, '(')) {
// Needs to have at least a ( to contain a capturing group
return false;
}

// Semi-accurate detection for capturing groups
return preg_match(
'~
(?:
\(\?\(
| \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \]
| \\\\ .
) (*SKIP)(*FAIL) |
\(
(?!
\? (?! <(?![!=]) | P< | \' )
| \*
)
~x',
$regex
);
}
}
80 changes: 80 additions & 0 deletions src/Dispatcher/Classic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace FastRoute\Dispatcher;

use FastRoute\Dispatcher;

class Classic implements Dispatcher{
protected $staticRouteMap;
protected $variableRouteData;

public function __construct($data) {
list($this->staticRouteMap, $this->variableRouteData) = $data;
}

public function dispatch($httpMethod, $uri) {
if (isset($this->staticRouteMap[$httpMethod][$uri])) {
$handler = $this->staticRouteMap[$httpMethod][$uri];
return [self::FOUND, $handler, []];
} else if ($httpMethod === 'HEAD' && isset($this->staticRouteMap['GET'][$uri])) {
$handler = $this->staticRouteMap['GET'][$uri];
return [self::FOUND, $handler, []];
}

$varRouteData = $this->variableRouteData;
if (isset($varRouteData[$httpMethod])) {
$result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
} else if ($httpMethod === 'HEAD' && isset($varRouteData['GET'])) {
$result = $this->dispatchVariableRoute($varRouteData['GET'], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}

// Find allowed methods for this URI by matching against all other HTTP methods as well
$allowedMethods = [];

foreach ($this->staticRouteMap as $method => $uriMap) {
if ($method !== $httpMethod && isset($uriMap[$uri])) {
$allowedMethods[] = $method;
}
}

foreach ($varRouteData as $method => $routeData) {
if ($method === $httpMethod) {
continue;
}

$result = $this->dispatchVariableRoute($routeData, $uri);
if ($result[0] === self::FOUND) {
$allowedMethods[] = $method;
}
}

// If there are no allowed methods the route simply does not exist
if ($allowedMethods) {
return [self::METHOD_NOT_ALLOWED, $allowedMethods];
} else {
return [self::NOT_FOUND];
}
}

protected function dispatchVariableRoute($routeData, $uri) {
foreach ($routeData as $regex => $route) {
if (!preg_match('~^' . $regex. '$~', $uri, $matches)) {
continue;
}
$vars = [];
$i = 0;
foreach ($route->variables as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $route->handler, $vars];
}

return [self::NOT_FOUND];
}
}
23 changes: 23 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
namespace FastRoute;

if (!function_exists('FastRoute\simpleDispatcher')) {
/**
* @param callable $routeDefinitionCallback
* @param array $options
*
* @return Dispatcher
*/
function classicDispatcher(callable $routeDefinitionCallback, array $options = []) {
$options += [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\Classic',
'dispatcher' => 'FastRoute\\Dispatcher\\Classic',
'routeCollector' => 'FastRoute\\RouteCollector',
];

/** @var RouteCollector $routeCollector */
$routeCollector = new $options['routeCollector'](
new $options['routeParser'], new $options['dataGenerator']
);
$routeDefinitionCallback($routeCollector);

return new $options['dispatcher']($routeCollector->getData());
}

/**
* @param callable $routeDefinitionCallback
* @param array $options
Expand Down

0 comments on commit 1146765

Please sign in to comment.