1. 程式人生 > 其它 >自己實現一個簡單的php路由器

自己實現一個簡單的php路由器

自己實現一個簡單的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

參考資料

  1. [翻譯]為MVC框架構建路由
  2. bramus/router