自己实现一个简单的php路由器

路由器的作用是根据客户端发送过来的请求连接,执行相应的操作,然后返回给客户端一个结果。

下面使用php一步步地实现一个简单的路由器,加深理解。

准备工作

在服务器上配置好php的运行环境,然后通过浏览器访问服务器上的php文件,就可以得到该php文件的执行结果。

在平常工作中请求的后端接口都是形如:

  • ​https://www.zhihu.com/api/v4/search/top_search/tabs/hot/items​​;
  • ​https://www.zhihu.com/question/450397900​​;

在请求的连接中看不到请求的是哪一个文件。

实际上,这些接口请求的都是同一个文件,比如​​index.php​​文件,而在所请求文件的代码当中正是通过路由器来对不同的请求连接进行处理。

可以通过修改服务器的配置文件的配置项使请求的连接中不必包含具体的文件名。

我在本地使用的是WAMP环境,在项目根目录使用的.htaccess文件是:

Options +FollowSymLinks
RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]


我的项目结构是:

|--项目根目录
|----index.php
|----.htaccess


关于.htaccess可以看下面的文章:

  1. ​apache的虚拟域名rewrite配置以及.htaccess的使用​
  2. ​25 个有用 Apache ‘.htaccess’ 技巧​

面向过程

根据面向过程的思想写一个简单的接口出来,下面开始在​​index.php​​文件中添加代码。

先是要注册接口连接以及对应的处理函数:

$routerMap = array(
'GET' => array(
0 => array(
'pattern' => '~^/aa/(\w*)$~',
'patternStr' => '/aa/{id}',
'callback' => function ($id) {
echo "this is a test ------ ".$id;
}
),
1 => array(
'pattern' => '~^/user/(\w*)/order/(\w*)$~',
'patternStr' => '/user/{userid}/order/{orderid}',
'callback' => function ($userid,$orderid) {
echo "\$userid ------ ".$userid." \n "."\$orderid -----".$orderid;
}
)
)
);


变量​​$routerMap​​保存了提供给客户端可访问的接口以及对应的处理函数。

​patternStr​​字段保存的是接口模板字符串;​​pattern​​保存的是由接口模板字符串转换成的正则表达式,用来匹配接口,从接口中获取参数值(RESTful API);​​callback​​字段保存的是该接口对应的处理函数。

接下来是获取当前客户端请求的接口,可以通过​​$_SERVER​​获取:

$uri = $_SERVER['REQUEST_URI'];


获取到​​$uri​​之后还不能直接与之前注册的路由匹配,还要做一下处理才行。

原因是此时得到的​​$uri​​可能包含一些我们并不需要的部分,需要给处理掉,比如拼接的查询参数和文件名等。

下面是处理​​$uri​​的代码:

$scriptNameArr = explode('/', $_SERVER['SCRIPT_NAME']);
foreach($scriptNameArr as $v) {
if($v !== '') {
// 将$uri中的文件路径和文件名去掉
$uri = str_replace('/'.$v,'',$uri);
}
}
// 将$uri上拼接的查询字符串去掉
$uriArr = explode('?',$uri);
$uri = $uriArr[0];


获取请求方法,以及该请求方法上注册的路由数组:

$requestMethod = $_SERVER['REQUEST_METHOD'];
$registerRouters = $routerMap[$requestMethod];


最后是遍历​​$registerRouters​​,找到与当前请求接口匹配的模板,分离出请求参数,并执行相应的处理函数:

foreach($registerRouters as $v) {
$matchRes = preg_match_all($v['pattern'], $uri, $matches, PREG_OFFSET_CAPTURE);
var_dump($matches);
if($matchRes) {
$matches = array_slice($matches, 1);
$callbackParam = array_map(function ($item,$index) {
return $item[0][0];
},$matches,array_keys($matches));
$fn = $v['callback'];
}
}

call_user_func_array($fn, $callbackParam);


最后​​index.php​​的完整代码是:

$routerMap = array(
'GET' => array(
0 => array(
'pattern' => '~^/aa/(\w*)$~',
'patternStr' => '/aa/{id}',
'callback' => function ($param) {
echo "this is a test ------ ".$param;
}
),
1 => array(
'pattern' => '~^/user/(\w*)/order/(\w*)$~',
'patternStr' => '/user/{userid}/order/{orderid}',
'callback' => function ($userid,$orderid) {
echo "\$userid ------ ".$userid." \n "."\$orderid -----".$orderid;
}
)
)
);
$scriptNameArr = explode('/', $_SERVER['SCRIPT_NAME']);
foreach($scriptNameArr as $v) {
if($v !== '') {
// 将$uri中的文件路径和文件名去掉
$uri = str_replace('/'.$v,'',$uri);
}
}
// 将$uri上拼接的查询字符串去掉
$uriArr = explode('?',$uri);
$uri = $uriArr[0];

// 获取请求方法
$requestMethod = $_SERVER['REQUEST_METHOD'];
$registerRouters = $routerMap[$requestMethod];

foreach($registerRouters as $v) {
$matchRes = preg_match_all($v['pattern'], $uri, $matches, PREG_OFFSET_CAPTURE);
var_dump($matches);
if($matchRes) {
$matches = array_slice($matches, 1);
$callbackParam = array_map(function ($item,$index) {
return $item[0][0];
},$matches,array_keys($matches));
$fn = $v['callback'];
}
}

call_user_func_array($fn, $callbackParam);


通过浏览器访问​​http://www.my.com/user/123123/order/dddddd?a=123&b=456​​会看到以下结果:

$userid ------ 123123 $orderid -----dddddd


访问​​http://www.my.com/aa/123123​​会看到以下结果:

this is a test ------ 123123


​www.my.com​​是我在本地配置的虚拟域名。

面向对象

上面通过面向过程的编程方式实现了一个简单的路由器,下面对上面面向过程的代码进行适当的抽象和封装。

在项目根目录下创建一个​​router​​文件夹,在​​router​​文件夹下创建一个​​Router.php​​文件,项目结构如下:

|--项目根目录
|--router
|----Router.php
|----index.php
|----.htaccess


​Router.php​​中的代码就是抽象出的路由器类:

class Router
{
private $routerMap = array();

// 路由派发
public function dispatch() {
$requestMethod = $_SERVER['REQUEST_METHOD'];
$routerArr = $this->routerMap[$requestMethod];

$this->handleUri($routerArr);
}

// 注册GET请求的路由
public function get($patternStr, $fn) {
$pattern = $this->routerTemplateToReg($patternStr);
$this->register('GET', $patternStr, $pattern, $fn);
}
// 注册POST请求的路由
public function post($patternStr, $fn) {
$pattern = $this->routerTemplateToReg($patternStr);
$this->register('POST', $patternStr, $pattern, $fn);
}
// 路由注册函数
public function register($method, $patternStr, $pattern, $fn) {
$this->routerMap[$method][] = array(
'pattern' => $pattern,
'patternStr' => $patternStr,
'callback' => $fn
);
}
// 把路由模板转换为正则表达式
private function routerTemplateToReg($patternStr) {
$txt = preg_replace('~{\w*}~','(\w*)',$patternStr);
return '~^'.$txt.'$~';
}
// 遍历注册的路由找到与当前访问服务器的uri相匹配的路由,分离参数,调用对应的处理函数
private function handleUri($routerArr) {
$uri = $this->getCurrentUri();
foreach($routerArr as $v) {
$matchRes = preg_match_all($v['pattern'], $uri, $matches, PREG_OFFSET_CAPTURE);
// var_dump($matches);
if($matchRes) {
$matches = array_slice($matches, 1);
$callbackParam = array_map(function ($item,$index) {
return $item[0][0];
},$matches,array_keys($matches));
$fn = $v['callback'];
}
}

call_user_func_array($fn, $callbackParam);

}
// 获取当前访问服务器的uri
private function getCurrentUri() {
$uri = $_SERVER['REQUEST_URI'];
$scriptNameArr = explode('/', $_SERVER['SCRIPT_NAME']);
foreach($scriptNameArr as $v) {
if($v !== '') {
$uri = str_replace('/'.$v,'',$uri);
}
}
$uriArr = explode('?',$uri);
return $uriArr[0];
}
}


重写​​index.php​​中的代码,如下:

require '/router/Router.php';

$route = new Router();

$route->get('/news/{id}',function ($id) {
echo '$newsid ==== '.$id;
});

$route->dispatch();


通过浏览器访问​​http://www.my.com/news/123123​​,会看到如下结果:

$newsid ==== 123123


虽然抽象出了​​Router​​类,但是注册路由的处理函数时仅仅支持匿名函数,还不够面向对象,下面使用php里的反射类对​​Router​​类进行适当改造,使路由的注册支持传入类名和方法名。

使用反射类

主要是对​​Router​​类的​​handleUri​​的方法进行扩展,代码如下:

private function handleUri($routerArr) {
$uri = $this->getCurrentUri();
foreach($routerArr as $v) {
$matchRes = preg_match_all($v['pattern'], $uri, $matches, PREG_OFFSET_CAPTURE);
// var_dump($matches);
if($matchRes) {
$matches = array_slice($matches, 1);
$callbackParam = array_map(function ($item,$index) {
return $item[0][0];
},$matches,array_keys($matches));
$fn = $v['callback'];
}
}
if (is_callable($fn)) {
call_user_func_array($fn, $callbackParam);
} else {
if(stripos($fn, '@') !== false) {
$arr = explode('@', $fn);
$className = $arr[0];
$methodName = $arr[1];
$reflectClass = new ReflectionClass($className);
$reflectMethod = $reflectClass->getMethod($methodName);
if($reflectMethod->isStatic()) {
forward_static_call_array(array($className, $methodName), $callbackParam);
} elseif($reflectMethod->isPublic()) {
$reflectClassInstance = $reflectClass->newInstanceArgs();
$reflectMethod->invokeArgs($reflectClassInstance, $callbackParam);
}
}
}
}


反射类的api可以参考​​官方文档​​。

改造之后,可以通过如下方式注册路由:

// @符号左侧是类名,右侧是类的方法名
$route->get('/user/{id}','IndexController@goodbye');


修改​​index.php​​文件中的代码:

require '/router/Router.php';
require '/module/Hello/Controller/IndexController.php';

$route = new Router();

$route->get('/news/{id}',function ($id) {
echo '$newsid ==== '.$id;
});


$route->get('/goodbye/{text}','IndexController@goodbye');

$route->dispatch();


上面代码中引入了​​IndexController.php​​文件,项目的目录结构为:

|--项目根目录
|--module
|--Hello
|--Controller
|----IndexController.php
|--router
|----Router.php
|----index.php
|----.htaccess


​IndexController.php​​中的代码为:

class IndexController {

static function goodbye($param) {
echo '再见~!'.$param;
}
}


通过浏览器访问​​http://www.my.com/goodbye/jack​​会看到如下输出:

再见~!jack