路由的功能就是分發請求到不一樣的控制器,基於的原理就是正則匹配。接下來呢,咱們實現一個簡單的路由器,實現的能力是php
[ 'id' => '23' ]
對於註冊的路由,須要分紅兩類(下文提到的$uri是指$_SERVER['REQUEST_URI']去掉查詢字符串的值)nginx
其實這是很明顯的,由於靜態的路由的話,咱們只須要和$uri直接比較相等與否就好了,而對於帶參數路由,譬如/user/{uid},咱們須要在註冊的時候,提取佔位符名,把{**}這一部分替換爲([a-zA-Z0-9_]+)這樣的正則字符串,使用()是由於要作分組捕獲,把佔位符對應的值要取出來。
Dispatcher.php中有兩個數組正則表達式
分別對應靜態路由和帶參數路由,另外要注意,這兩個數組是二維數組,第一維存儲http method,第二維用來存儲正則字符串(靜態路由自身就是這個值,而帶參數路由是把佔位符替換後的值),最後的value是一個Route對象json
這個類很好理解,用來存儲註冊路由的一些信息數組
固然,這個類還能夠多存儲一點信息,譬如帶參數路由最原始的字符串(/user/{uid}),這裏簡單作了composer
第一步很簡單,主要說明第二步
對於三個獨立的正則字符串(定界符是~):函數
~^/user/([^/]+)/(\d+)$~ ~^/user/(\d+)$~ ~^/user/([^/]+)$~
咱們能夠合起來,獲得post
~^(?: /user/([^/]+)/(\d+) | /user/(\d+) | /user/([^/]+) )$~x
?:是非捕獲型分組
這個轉化很簡單,咱們怎麼知道那個正則被匹配了呢??
舉個例子:ui
preg_match($regex, '/user/nikic', $matches); => [ "/user/nikic", # 徹底匹配 "", "", # 第一個(空) "", # 第二個(空) "nikic", # 第三個(被使用) ]
能夠看到,第一個非空的位置就能夠推斷出哪一個路由被匹配了(第一個徹底匹配要剔除),咱們須要一個數組要映射它this
[ 1 => ['handler0', ['name', 'id']], 3 => ['handler1', ['id']], 4 => ['handler2', ['name']], ]
1是由於排除第一個匹配
3是由於第一個路由有兩個佔位符
4是由於第二個路由有一個佔位符
上面的數組,咱們能夠註冊的methodToRegexToRoutesMap這個量造成的,我是這麼寫的
$regexes = array_keys($this->methodToRegexToRoutesMap[$httpMethod]); foreach ($regexes as $regex) { $routeLookup[$index] = [ $this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler, $this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables, ]; $index += count($this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables); }
調用回調函數,返回一個數組,第一個值用來判斷最終有沒有找到
<?php namespace SalamanderRoute; class Route { /** @var string */ public $httpMethod; /** @var string */ public $regex; /** @var array */ public $variables; /** @var mixed */ public $handler; /** * Constructs a route (value object). * * @param string $httpMethod * @param mixed $handler * @param string $regex * @param array $variables */ public function __construct($httpMethod, $handler, $regex, $variables) { $this->httpMethod = $httpMethod; $this->handler = $handler; $this->regex = $regex; $this->variables = $variables; } /** * Tests whether this route matches the given string. * * @param string $str * * @return bool */ public function matches($str) { $regex = '~^' . $this->regex . '$~'; return (bool) preg_match($regex, $str); } }
<?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($httpMethod, $routeData, $handler); } else { $this->addVariableRoute($httpMethod, $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 = []; $index = 1; $regexes = array_keys($this->methodToRegexToRoutesMap[$httpMethod]); foreach ($regexes as $regex) { $routeLookup[$index] = [ $this->methodToRegexToRoutesMap[$httpMethod][$regex]->handler, $this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables, ]; $index += count($this->methodToRegexToRoutesMap[$httpMethod][$regex]->variables); } $regexCombined = '~^(?:' . implode('|', $regexes) . ')$~'; if(!preg_match($regexCombined, $uri, $matches)) { return [self::NOT_FOUND]; } for ($i = 1; '' === $matches[$i]; ++$i); list($handler, $varNames) = $routeLookup[$i]; $vars = []; foreach ($varNames as $varName) { $vars[$varName] = $matches[$i++]; } return [self::FOUND, $handler, $vars]; } }
location / { try_files $uri $uri/ /index.php$is_args$args; # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }
{ "name": "salmander/route", "require": {}, "autoload": { "psr-4": { "SalamanderRoute\\": "SalamanderRoute/" } } }
<?php include_once 'vendor/autoload.php'; use SalamanderRoute\Dispatcher; $dispatcher = new Dispatcher(); $dispatcher->get('/', function () { echo 'hello world'; }); $dispatcher->get('/user/{id}', function ($args) { echo "user {$args['id']} visit"; }); // Fetch method and URI from somewhere $httpMethod = $_SERVER['REQUEST_METHOD']; $uri = $_SERVER['REQUEST_URI']; // 去掉查詢字符串 if (false !== $pos = strpos($uri, '?')) { $uri = substr($uri, 0, $pos); } $routeInfo = $dispatcher->dispatch($httpMethod, $uri); switch ($routeInfo[0]) { case Dispatcher::NOT_FOUND: echo '404 not found'; break; case Dispatcher::FOUND: $handler = $routeInfo[1]; $vars = $routeInfo[2]; $handler($vars); break; }