改進
緊接上一篇文章Just for fun——PHP框架之簡單的路由器(1)。
程式碼下載
效率不高原因
對於以下合併的正則
~^(?:
/user/([^/]+)/(d+)
| /user/(d+)
| /user/([^/]+)
)$~x
最終匹配的是分組中的某一個,我們需要的子匹配也是那個分組中的,然而從結果看
preg_match($regex, `/user/nikic`, $matches);
=> [
"/user/nikic", # 完全匹配
"", "", # 第一個(空)
"", # 第二個(空)
"nikic", # 第三個(被使用)
]
這裡是最後一個路由被匹配了,但是其他分組的子匹配也被填充了,這是多餘的。
解決思路
PCRE正則裡?|
也是非捕獲分組,那麼?|
和?:
有什麼區別呢??
區別在於?|
會組號重置,看以下幾個例子就懂了
preg_match(`~(?:(Sat)ur|(Sun))day~`, `Saturday`, $matches)
=> ["Saturday", "Sat", ""] # 最後一個""其實是不存在的,寫在這裡是為了闡釋概念
preg_match(`~(?:(Sat)ur|(Sun))day~`, `Sunday`, $matches)
=> ["Sunday", "", "Sun"]
preg_match(`~(?|(Sat)ur|(Sun))day~`, `Saturday`, $matches)
=> ["Saturday", "Sat"]
preg_match(`~(?|(Sat)ur|(Sun))day~`, `Sunday`, $matches)
=> ["Sunday", "Sun"]
所有我們可以用?|
來代替?:
來減少多餘的子匹配填充,但是這樣一來的話,如何判斷哪個分組被匹配了呢??(因為之前的判斷技巧就失效了)
我們可以這樣,新增一些多餘子匹配
~^(?|
/user/([^/]+)/(d+)
| /user/(d+)()()
| /user/([^/]+)()()()
)$~x
實現
dispatcher.php
<?php
/**
* User: salamander
* Date: 2017/11/12
* Time: 13:43
*/
namespace SalamanderRoute;
class Dispatcher {
/** @var mixed[][] */
protected $staticRoutes = [];
/** @var Route[][] */
private $methodToRegexToRoutesMap = [];
const NOT_FOUND = 0;
const FOUND = 1;
const METHOD_NOT_ALLOWED = 2;
/**
* 提取佔位符
* @param $route
* @return array
*/
private function parse($route) {
$regex = `~^(?:/[a-zA-Z0-9_]*|/{([a-zA-Z0-9_]+?)})+/?$~`;
if(preg_match($regex, $route, $matches)) {
// 區分靜態路由和動態路由
if(count($matches) > 1) {
preg_match_all(`~{([a-zA-Z0-9_]+?)}~`, $route, $matchesVariables);
return [
preg_replace(`~{[a-zA-Z0-9_]+?}~`, `([a-zA-Z0-9_]+)`, $route),
$matchesVariables[1],
];
} else {
return [
$route,
[],
];
}
}
throw new LogicException(`register route failed, pattern is illegal`);
}
/**
* 註冊路由
* @param $httpMethod string | string[]
* @param $route
* @param $handler
*/
public function addRoute($httpMethod, $route, $handler) {
$routeData = $this->parse($route);
foreach ((array) $httpMethod as $method) {
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($method, $routeData, $handler);
} else {
$this->addVariableRoute($method, $routeData, $handler);
}
}
}
private function isStaticRoute($routeData) {
return count($routeData[1]) === 0;
}
private function addStaticRoute($httpMethod, $routeData, $handler) {
$routeStr = $routeData[0];
if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
throw new LogicException(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 LogicException(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) = $routeData;
if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
throw new LogicException(sprintf(
`Cannot register two routes matching "%s" for method "%s"`,
$regex, $httpMethod
));
}
$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
$httpMethod, $handler, $regex, $variables
);
}
public function get($route, $handler) {
$this->addRoute(`GET`, $route, $handler);
}
public function post($route, $handler) {
$this->addRoute(`POST`, $route, $handler);
}
public function put($route, $handler) {
$this->addRoute(`PUT`, $route, $handler);
}
public function delete($route, $handler) {
$this->addRoute(`DELETE`, $route, $handler);
}
public function patch($route, $handler) {
$this->addRoute(`PATCH`, $route, $handler);
}
public function head($route, $handler) {
$this->addRoute(`HEAD`, $route, $handler);
}
/**
* 分發
* @param $httpMethod
* @param $uri
*/
public function dispatch($httpMethod, $uri) {
$staticRoutes = array_keys($this->staticRoutes[$httpMethod]);
foreach ($staticRoutes as $staticRoute) {
if($staticRoute === $uri) {
return [self::FOUND, $this->staticRoutes[$httpMethod][$staticRoute], []];
}
}
$routeLookup = [];
$regexes = [];
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $regex => $route) {
$index = count($route->variables);
if(array_key_exists($index, $routeLookup)) {
$indexNear = $this->getArrNearEmptyEntry($routeLookup, $index);
array_push($regexes, $regex . str_repeat(`()`, $indexNear - $index));
$routeLookup[$indexNear] = [
$this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,
$this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,
];
} else {
$routeLookup[$index] = [
$this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler,
$this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables,
];
array_push($regexes, $regex);
}
}
$regexCombined = `~^(?|` . implode(`|`, $regexes) . `)$~`;
if(!preg_match($regexCombined, $uri, $matches)) {
return [self::NOT_FOUND];
}
list($handler, $varNames) = $routeLookup[count($matches) - 1];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
private function getArrNearEmptyEntry(&$arr, $index) {
while (array_key_exists(++$index, $arr));
return $index;
}
}