读 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));
    }

    ...
}
相关文章
相关标签/搜索