讀 Slim 框架代碼(2)

URL 路由

URL 路由在前一篇讀 Slim 框架代碼(1)中已經有涉及。這一篇更詳細介紹一下 Slim 框架中對 URL 路由的處理。php

基本路由

用 Slim 建立一條 URL 路由的方法:數組

$app = new \Slim\Slim();
$app->{HTTP_METHOD}('/book/:id', function($id){
                // 根據書籍 $id 顯示數據
            });

其中的 {HTTP_METHOD}() 能夠是 get()post()put()delete() 或者是 options() 中的任意一種(GETHEAD 方法都由 $app->get() 函數執行)。都是傳入兩個參數,第一個是匹配的 URI 的模式,第二個是匹配後執行的回調函數。app

如今來看 Slim 源代碼中這個函數的執行(以 $app->get() 爲例):框架

  • in Slim.php
public function get()
{
    $args = func_get_args();

    return $this->mapRoute($args)->via(\Slim\Http\Request::METHOD_GET, \Slim\Http\Request::METHOD_HEAD);
}

在這裏會取到 $app->get() 的全部參數而後傳到 $app->mapRoute(),這裏會返回一個 Route 對象,接着再執行這個 Route 對象的 via() 方法,把 GETHEAD 做爲參數傳入。函數

爲了方便解析,先把 mapRoute 和 via 涉及的全部源代碼列出來:post

<!--more-->this

  • in Slim.php:
protected function mapRoute($args)
{
    $pattern = array_shift($args);
    $callable = array_pop($args);
    $route = new \Slim\Route($pattern, $callable);
    $this->router->map($route);
    if (count($args) > 0) {
        $route->setMiddleware($args);
    }

    return $route;
}
...

public function call()
{
    try {
        ...
        $matchedRoutes = $this->router->getMatchedRoutes($this->request->getMethod(), $this->request->getResourceUri());
        foreach ($matchedRoutes as $route) {
            try {
                $this->applyHook('slim.before.dispatch');
                $dispatched = $route->dispatch();
                $this->applyHook('slim.after.dispatch');
                if ($dispatched) {
                    break;
                }
            } catch (\Slim\Exception\Pass $e) {
                continue;
            }
        }
        if (!$dispatched) {
            $this->notFound();
        }
        $this->applyHook('slim.after.router');
        $this->stop();
    } catch (\Slim\Exception\Stop $e) {
        ...
    } catch (\Exception $e) {
        ...
    }
}
  • in Router.php:
public function map(\Slim\Route $route)
{
    list($groupPattern, $groupMiddleware) = $this->processGroups();

    $route->setPattern($groupPattern . $route->getPattern());
    $this->routes[] = $route;

    foreach ($groupMiddleware as $middleware) {
        $route->setMiddleware($middleware);
    }
}
...
public function getMatchedRoutes($httpMethod, $resourceUri, $reload = false)
{
    if ($reload || is_null($this->matchedRoutes)) {
        $this->matchedRoutes = array();
        foreach ($this->routes as $route) {
            if (!$route->supportsHttpMethod($httpMethod) && !$route->supportsHttpMethod("ANY")) {
                continue;
            }

            if ($route->matches($resourceUri)) {
                $this->matchedRoutes[] = $route;
            }
        }
    }

    return $this->matchedRoutes;
}
  • in Route.php
public function __construct($pattern, $callable)
{
    $this->setPattern($pattern);
    $this->setCallable($callable);
    $this->setConditions(self::getDefaultConditions());
}
...
public function via()
{
    $args = func_get_args();
    $this->methods = array_merge($this->methods, $args);

    return $this;
}
...
public function dispatch()
{
    foreach ($this->middleware as $mw) {
        call_user_func_array($mw, array($this));
    }

    $return = call_user_func_array($this->getCallable(), array_values($this->getParams()));
    return ($return === false)? false : true;
}

在 mapRoute 方法中首先會根據傳入的 URI 模式和回掉函數 new 一個 Route 對象,而後吧這個 Route 對象 map 到 $app->router 對象,其實就是執行 Router::map() 函數把這個 Route 對象加到 $app->router->routes 這個數組裏面。在 Router::map() 這個函數中,$groupPattern 和 $groupMiddleware 應該是用來處理路由組的,可是 Slim 文檔中貌似也沒有提到怎麼用,這裏暫時先無論了。在調用 $app->get() 方法的時候也能夠在 URI 模式和回調函數中間插入一些 中間件(Middleware),在 Route 對象中設置一些要執行的中間件,這個在本文稍後一些再具體說明。url

在 mapRoute 執行的最後,會把此時生成的 Route 對象返回,接着會執行 Route::via 方法。這個方法很簡單,就是把 GETHEAD 加到 Route 對象的 method 數組成員裏。spa

最後在執行 $app->run() 方法時會調用到 $app->call() 方法(應爲 Slim 類自己也是一種中間件),這裏調用 $this->router->getMatchedRoutes 方法找出能匹配 $app->get() 的參數的全部 Route 對象,而後執行他們的 dispatch() 方法。(但一般咱們只會爲同一個 URI 的某個 HTTP_METHOD 定義一個回調函數)code

匹配路由參數

上文中調用 $this->router->getMatchedRoutes 方法找出能匹配 $app->get() 的參數的全部 Route 對象時會執行 Route.php 中的 matches() 方法。

  • In Route.php
public function matches($resourceUri)
{
    //Convert URL params into regex patterns, construct a regex for this route, init params
    $patternAsRegex = preg_replace_callback(
        '#:([\w]+)\+?#',
        array($this, 'matchesCallback'),
        str_replace(')', ')?', (string) $this->pattern)
    );
    if (substr($this->pattern, -1) === '/') {
        $patternAsRegex .= '?';
    }

    //Cache URL params' names and values if this route matches the current HTTP request
    if (!preg_match('#^' . $patternAsRegex . '$#', $resourceUri, $paramValues)) {
        return false;
    }
    foreach ($this->paramNames as $name) {
        if (isset($paramValues[$name])) {
            if (isset($this->paramNamesPath[ $name ])) {
                $this->params[$name] = explode('/', urldecode($paramValues[$name]));
            } else {
                $this->params[$name] = urldecode($paramValues[$name]);
            }
        }
    }

    return true;
}
...
protected function matchesCallback($m)
{
    $this->paramNames[] = $m[1];
    if (isset($this->conditions[ $m[1] ])) {
        return '(?P<' . $m[1] . '>' . $this->conditions[ $m[1] ] . ')';
    }
    if (substr($m[0], -1) === '+') {
        $this->paramNamesPath[ $m[1] ] = 1;

        return '(?P<' . $m[1] . '>.+)';
    }

    return '(?P<' . $m[1] . '>[^/]+)';
}

matches() 方法會把模式中的全部 #:([\w]+)\+?# 模式替換成 (?P<' . $m[1] . '>[^/]+) (僅匹配一級目錄)或 (?P<' . $m[1] . '>.+)(匹配每一級目錄),若是有設定某個參數的 condition,會直接用 condition 替換成 (?P<' . $m[1] . '>' . $this->conditions[ $m[1] ] . ')'。這裏 (?P<name>.+) 用來命名分組。

舉個例子,若是 Route 的模式爲 /:id/:name,那它會被替換成 /(?P<id>[^/]+)/(?P<name>[^/]+)。假設咱們訪問的 URL 爲 /ljie/Rocky,那麼在執行完 preg_match('#^' . $patternAsRegex . '$#', $resourceUri, $paramValues) 這條語句後會獲得

$paramValues = array(
    "id" => "ljie",
    "name" => "Rocky"
);

路由中間件

路由中間件是一系列函數,把 Route 對象做爲輸入參數,期間經過 $app = \Slim\Slim::getInstance(); 的到 $app 實例,能夠作一些過濾,驗證及其餘一些公共的邏輯。在 Route.php 中的 dispatch() 方法中會執行這些中間件

public function dispatch()
{
    foreach ($this->middleware as $mw) {
        call_user_func_array($mw, array($this));
    }

    ...
}
相關文章
相關標籤/搜索