美文网首页
Lumen路由实现

Lumen路由实现

作者: 叶小许 | 来源:发表于2018-12-23 20:55 被阅读0次

一、Lumen路由的使用

在了解实现之前,我们先了解其使用的方法以及其作用也是很重要的。Lumen路由对象是在构建Application对象的时候声明的Router对象。Router对象包含几个常用的方法。

$app->get($uri, $callback);//get方法路由
$app->post($uri, $callback);//post方法路由
$app->put($uri, $callback);//put方法路由
$app->patch($uri, $callback);//patch方法路由
$app->delete($uri, $callback);//delete方法路由
$app->option($uri, $callback);//option方法路由

这是最基础的用法。其中$uri是路径,$callback可以是一个闭包,可以是一个字符串,也可以是一个数组,当是一个数组的时候,我们可以为当前路由设置相关的属性,包括设置路由中间件,路由命名,指定相关的动作等等。
举个例子:

//当我们使用get方法访问http://localhost/的时候,会打印出hello world
$app->get('/', function () use ($app) {
    return 'hello world';
});
//当我们使用get方法访问http://localhost/user的时候,会访问到UserController下的index方法(这里默认的命名空间是App\Http\Controllers)
$app->get('/user', ['UserController@index']);

其中,还有一个常用的group方法

//这个方法是为了当一组路由中出现了一些公共属性的时候,可以统一定义这些路由的属性,其中$callback必须是闭包类型
$app->group($attributes, $callback);

二、添加路由的实现

(1) 路由方法的原型

在第一部分里面,我们所说的get,post,put等一些列http动作的方法,他们的原型都是路由中的addRoute方法。比如get方法

public function get($uri, $action)
{
    $this->addRoute('GET', $uri, $action);

    return $this;
}

从中我们可以看到,它实际上调用的是路由对象中的addRoute方法。其他的几个http动作的相关的方法,都类似。

(2) addRoute方法的作用

简单来说,addRoute方法的功能就是把我们当前配置的路由以及其相关的属性,都整合到一个路由数组中。

public function addRoute($method, $uri, $action)
{
    $action = $this->parseAction($action);

    $attributes = null;

    if ($this->hasGroupStack()) {
        $attributes = $this->mergeWithLastGroup([]);
    }
    //这里还有其他实现
    ……
}

addRoute方法第一行,就是解析了我们定义的路由的行为。即第一小节中的我们定义路由的方法中的$callback做一个解析。我们来看看,parseAction方法具体做了什么。

protected function parseAction($action)
{
    if (is_string($action)) {
        return ['uses' => $action];
    } elseif (! is_array($action)) {
        return [$action];
    }

    if (isset($action['middleware']) && is_string($action['middleware'])) {
        $action['middleware'] = explode('|', $action['middleware']);
    }

    return $action;
}

parseAction方法中我们可以看到,当我们定义的路由动作是一个字符串的时候,也就是我们很常用的$app->get(‘/’, ‘Controller@action’)这种形式的时候,会封装成一个数组形式[‘uses’ => ‘Controller@action’]。这种形式其实就是在当前没有定义任何有关路由属性的时候的一个简写。当定义的动作不是字符串和不是数组的时候,会把当前的对象封装到一个数组中。当当前定义的路由动作是一个数组的时候,会把当前的路由中间件拆成一个数组。总而言之,当通过该方法解析后,我们得到的就是一个路由属性的数组。

addRoute方法中的前几行,还有一个hasGroupStack的方法,这个方法是干嘛的呢?我们来看看这个方法的内部实现

//判断当前是否定义了公共属性
public function hasGroupStack()
{
    return ! empty($this->groupStack);
}

从实现中我们可以看出来,这个方式就是只是简单的判断了一下当前对象中的groupStack属性是否为空,那么这个属性是在哪里操作的呢?这就引出了我们的group方法中的作用了。我们来看看group方法的实现

//定义路由下的公共属性
public function group(array $attributes, \Closure $callback)
{
    if (isset($attributes['middleware']) && is_string($attributes['middleware'])) {
        $attributes['middleware'] = explode('|', $attributes['middleware']);
    }

    $this->updateGroupStack($attributes);

    call_user_func($callback, $this);

    array_pop($this->groupStack);
}

//更新当前的公共属性
protected function updateGroupStack(array $attributes)
{
    if (! empty($this->groupStack)) {
        $attributes = $this->mergeWithLastGroup($attributes);
    }

    $this->groupStack[] = $attributes;
}

group方法中首先也是将middleware属性拆分成数组的形式,然后根据新加的属性更新当前路由对象中的groupStack这个属性。更新的时候,是与当前groupStack中最后一个元素进行合并,然后将合并后的属性添加到groupStack的尾部。这种情况只会在group嵌套的时候发生,子group会拥有父group的所有属性,兄弟group之间,他们的属性之间没有任何关系。当更新好当前的groupStack后,会立即调用当前group所定义的闭包,在这个闭包中我们通常就是调用相关的路由方法,或者定义子group。这样,在当前group中所定义的路由,都会拥有group所定义的路由属性。

当获取完当前分组的属性之后,会将当前分组的属性与解析好的action属性做一个合并,将分组的属性应用到action上,这当前包括middleware,namespace,as等属性。

当当前的路由相关属性处理好之后,就将当前的属性加入到路由数组中其中,在addRoute方法中路由数组的定义如下:

$this->routes[$method.$uri] = ['method' => $method, 'uri' => $uri, 'action' => $action];

三、路由解析

当定义好一系列路由之后,我们的服务有关路由的准备工作就做的差不多了。在index.php方法里面我们可以看到这样一行:

$app->run();

运行到这里我们的服务就开始进入解析阶段了。

在run方法里,有这样一行代码:

$response = $this->dispatch($request);

dispatch的核心代码:

return $this->sendThroughPipeline($this->middleware, function () use ($method, $pathInfo) {
    if (isset($this->router->getRoutes()[$method.$pathInfo])) {
        return $this->handleFoundRoute([true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]);
    }

    return $this->handleDispatcherResponse(
        $this->createDispatcher()->dispatch($method, $pathInfo)
    );
});

在这个方法里,会通过当前的$request对象得到请求的方法$method以及请求的$uri,然后通过第二小节中的定义好的路由数组,找到对应的路由的属性。然后通过当前的路由属性中找到对应的控制器和方法(如果是对象的,直接执行该函数),通过容器来调用控制器中的方法(通过容易解决依赖的问题),得到返回值,然后根据不同的返回值类型做出不同的动作。
这里我只是简单描述了一下流程,真实的流程中,还需要通过应用的中间件,路由中间件才会到达对应的控制器方法中。中间件的验证是通过Lumen中的Pipeline对象(管道对象)来进行验证的。其实现原理其实就是通过array_reduce方法来实现的,这里不详细说明。

细心的同学会发现,如何我定义了一个动态路由,例如我定义一个动态路由如下:

$app->get(‘/user/{id}’, function () use ($app) {
    return ‘hello world’;
});

理论上说:当我访问的$uri/user/1时,也应会输出hello world,但按照上面的流程其实是找不到当前的路由的。

的确,按照我们目前说的流程,肯定是找不到这个路由的。于是就引出了我接下来要说的动态路由的添加。

四、动态路由的添加

动态路由的添加是在解析路由过程中添加的。
注意到dispatch方法中的这一行

return $this->handleDispatcherResponse(
        $this->createDispatcher()->dispatch($method, $pathInfo)
    );

这个方法是在第一次没有找到对应路由之后才会执行的。其中
$this->createDispatcher()就添加动态路由中的过程,我们看到这个方法里的实现

protected function createDispatcher()
{
    return $this->dispatcher ?: \FastRoute\simpleDispatcher(function ($r) {
        foreach ($this->router->getRoutes() as $route) {
            $r->addRoute($route['method'], $route['uri'], $route['action']);
        }
    });
}

我们看到,这个方法就是返回了一个分派器,如果没有指定分派器,则默认使用Lumen当前的分派器。这里框架允许我们自定义分派器。(PS:路由的依赖注入就是通过获取这个自定义的分派器实现的)

我们直接看默认的路由分配器的实现。
simpleDispatcher是一个辅助函数,里面定义了包括路由解析器,数据生成器,路由收集器,以及分派器等所对应的类。

function simpleDispatcher(callable $routeDefinitionCallback, array $options = [])
{
    $options += [
        'routeParser' => 'FastRoute\\RouteParser\\Std',
        'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
        'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
        'routeCollector' => 'FastRoute\\RouteCollector',
    ];

    /** @var RouteCollector $routeCollector */
    $routeCollector = new $options['routeCollector'](
        new $options['routeParser'], new $options['dataGenerator']
    );
    $routeDefinitionCallback($routeCollector);

    return new $options['dispatcher']($routeCollector->getData());
}

simpleDispatcher函数中:

$routeCollector = new $options['routeCollector'](
        new $options['routeParser'], new $options['dataGenerator']
    );
$routeDefinitionCallback($routeCollector);

这两行是添加路由的核心代码。

createDispatcher中闭包中的内容,即调用$routeCollector(路由收集器)的addRoute方法,重新添加路由。我们重点看该方法。

public function addRoute($httpMethod, $route, $handler)
{
    $route = $this->currentGroupPrefix . $route;
    $routeDatas = $this->routeParser->parse($route);
    foreach ((array) $httpMethod as $method) {
        foreach ($routeDatas as $routeData) {
            $this->dataGenerator->addRoute($method, $routeData, $handler);
        }
    }
}

addRoute方法中,首先做的便是解析当前$uriparse的核心代码如下:

//这个地方循环是因为配置的$uri中可以有可选参数,比如我配置的$uri为:/user/{id}[/interes]
这里就相当于定义了两条除了$uri不同之外其他都相同的路由,一条为/user/{id},一条为/user/{id}/interes,具体细节请看这个方法的整个实现。
foreach ($segments as $n => $segment) {
    if ($segment === '' && $n !== 0) {
        throw new BadRouteException('Empty optional part');
    }

    $currentRoute .= $segment;
    $routeDatas[] = $this->parsePlaceholders($currentRoute);
}

其中

$routeDatas[] = $this->parsePlaceholders($currentRoute);

这一行,是最重要的一行代码,这个方法的功能就是把当前$uri中的参数进行分割
举个例子:假设我们定义的路由的$uri/user/{id}/goods/{goods_id}
这个方法返的就是[‘/user/’,[‘id’, ‘[^/]+’],’/goods/’, [‘goods_id’, ‘[^/]+’]]
其中返回的参数中数组类型的即为动态参数,第二个参数是一个正则表达式,这是一个默认的正则表达式,如果是自己配置了参数的匹配规则,则会覆盖默认的正则表达式。

$uri中对应的动态参数及其正则表达式解析出来之后,接着会调用dataGenerator中的addRoute方法添加对应的路由。

public function addRoute($httpMethod, $routeData, $handler)
{
    if ($this->isStaticRoute($routeData)) {
        $this->addStaticRoute($httpMethod, $routeData, $handler);
    } else {
        $this->addVariableRoute($httpMethod, $routeData, $handler);
    }
}

可以看到,在这个方法中动态路由和静态路由的添加方式是不一样的,而判断是不是动态路由的唯一方式就是当前解析的路由的数组数量大于1,如果看到后面,会发现静态路由和动态路由是添加在不同的数组中的。且路由不允许重复添加,且路由之间不允许有包含关系,否则,会报异常。

private function isStaticRoute($routeData)
{
    return count($routeData) === 1 && is_string($routeData[0]);
}

我们重点看addVariableRoute方法。

private function addVariableRoute($httpMethod, $routeData, $handler)
{
    list($regex, $variables) = $this->buildRegexForRoute($routeData);

    if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
        throw new BadRouteException(sprintf(
            'Cannot register two routes matching "%s" for method "%s"',
            $regex, $httpMethod
        ));
    }

    $this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
        $httpMethod, $handler, $regex, $variables
    );
}

addVariableRoute方法中,第一行便是构建路由表达式,提取当前路由表达式的参数。拿上面的例子来说,当执行完$this->buildRegexForRoute($routeData)这个方法后
得到的返回值为[‘/user/([^/]+)/goods/([^/]+)’,[‘id’, ‘goods_id’]],然后构建一个Route对象加入到动态路由的methodToRegexToRoutesMap数组中。

五、动态路由的分派

添加完动态路由之后,我们就可以基于当前的$method$uri去找到对应的路由了

simpleDispatcher函数里有这样一行

return new $options['dispatcher']($routeCollector->getData());

其中$routeCollector->getData()即获取当前的静态路由和动态路由。在获取静态路由的时候,是直接将静态路由的数组返回,而获取动态路由的时候,做了相应的处理。
dataGenerator所属的对象中可以看到有一个generateVariableRouteData方法。我们看看这个方法的具体实现:

private function generateVariableRouteData()
{
    $data = [];
    foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) {
        $chunkSize = $this->computeChunkSize(count($regexToRoutesMap));
        $chunks = array_chunk($regexToRoutesMap, $chunkSize, true);
        $data[$method] = array_map([$this, 'processChunk'], $chunks);
    }
    return $data;
}

在这个方法下,将对应$method下的路由分成若干组,每组有$chunkSize个路由,然后将每组的$chunkSize个路由经过processChunk方法处理成一个正则表达式,以及一个路由的映射数组。

我们可以到对应的类中查看processChunk的实现

protected function processChunk($regexToRoutesMap)
{
    $routeMap = [];
    $regexes = [];
    $numGroups = 0;
    foreach ($regexToRoutesMap as $regex => $route) {
        $numVariables = count($route->variables);
        $numGroups = max($numGroups, $numVariables);

        $regexes[] = $regex . str_repeat('()', $numGroups - $numVariables);
        $routeMap[$numGroups + 1] = [$route->handler, $route->variables];

        ++$numGroups;
    }

    $regex = '~^(?|' . implode('|', $regexes) . ')$~';
    return ['regex' => $regex, 'routeMap' => $routeMap];
}

这里在建立routeMap的时候,是以当前路由中捕获分组中的最大值以及当前路由需要捕获的分组数量中取最大值,如果当前路由需要捕获的分组少于当前路由中捕获分组中的最大值,不足的部分以空的捕获数组填充。这样做的目的是为了在路由匹配的过程中能过通过捕获的数量快速定位到某个路由。可以看到,这几个路由的正则匹配处理之后的大正则表达式中是一个或的关系。

解释起来有点绕,我举个例子:
假设动态路由数组中有五个动态路由分别为:

$regex                         $vars
R1: /a/([^/]+)/b/([^/]+)  [‘a1’, ‘a2’]
R2:/aa/([^/]+)/bb/([^/]+)/cc/([^/]+)/([^/]+) [‘aa1’,’bb2’,’cc1’,’cc2’]

R3:/f/([^/]+)/i/([^/]+)/j/([^/]+)/k/([^/]+) [‘f1’,’i1’,’j1’,’k1’]
R4:/ff/([^/]+)/ii/([^/]+) [‘ff1’, ‘ii1’]

R5:/c/([^/]+) [‘c1’]

两两为一组:
则前两组的routeMap[3]为R1routeMap[5]R2
$regex为~^(?| /a/([^/]+)/b/([^/]+) | /aa/([^/]+)/bb/([^/]+)/cc/([^/]+)/([^/]+))$~
中间两组的routeMap[5]R3routeMap[6]R4
$regex为~^(?|/aa/([^/]+)/bb/([^/]+)/cc/([^/]+)/([^/]+) | /ff/([^/]+)/ii/([^/]+)()()())$~
最后dispatch方法,dispatch方法会首先找静态路由,找到即返回,然后才找动态路由。我们重点看dispatch方法。

$result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri);
if ($result[0] === self::FOUND) {
    return $result;
}

该方法调用了dispatchVariableRoute方法,我们看看具体实现:

protected function dispatchVariableRoute($routeData, $uri)
{
    foreach ($routeData as $data) {
        if (!preg_match($data['regex'], $uri, $matches)) {
            continue;
        }

        list($handler, $varNames) = $data['routeMap'][count($matches)];

        $vars = [];
        $i = 0;
        foreach ($varNames as $varName) {
            $vars[$varName] = $matches[++$i];
        }
        return [self::FOUND, $handler, $vars];
    }

    return [self::NOT_FOUND];
}

该方法通过遍历该方法(指的是get,posthttp动作)下的分组的路由信息,若某分组下有匹配,则通过匹配的参数快速找到对应的具体的Route对象,然后通过分组提取当前匹配中动态参数的值。找到对应的路由信息之后,就和第三小节中找到路由后的处理过程是一样的了。

相关文章

  • Lumen路由实现

    一、Lumen路由的使用 在了解实现之前,我们先了解其使用的方法以及其作用也是很重要的。Lumen路由对象是在构建...

  • Lumen 坑:路由前缀的一个悬而未决的 bug

    在 Lumen 的路由里面发现了这样一个问题。真的是时间挺长的 bug 了…… 以上路由注册在运行时会抛出类似「C...

  • Lumen5.1 使用Mail邮件且找回密码功能

    发邮件 Lumen5.1 使用Mail邮件 找回密码 路由 控制器 找回密码并发送邮件 重置密码 短信发送功能 发...

  • 小程序Lumen API开发

    Lumen API开发 Laravel和Lumen的区别:Lumen轻量级框架,集合了Laravel的优美语法,支...

  • lumen实现KafkaMQ生产

    条件1:安装zookeeper 条件2:安Kafka 条件3:安RdKafka lumen代码 查看消费 理应如图:

  • 路由

    路由map 路由视图 路由导航 实现简单路由 import VueRouter from 'vue-router'...

  • 学习 Lumen 用户认证 (一)

    好久没写 PHP 代码了,尤其是 Lumen,我是 Lumen 的忠实用户,自从面世开始,我就将 Lumen 作为...

  • ios路由 FFRouter

    FFRouter路由 为啥路由 已经到了必须实现路由功能的时候,看了很多大神实现路由的方式(非常感谢?),尤其是Y...

  • luman如何搭建swoole

    1,首先搭建lumen框架,使用composer命令(https://lumen.laravel-china.or...

  • Android组件化专题 - 路由框架进阶模块间的业务通信

    上一篇文章,讲解了路由框架实现的原理,并实现了基本的路由框架 页面路由的跳转Android组件化专题 - 路由框架...

网友评论

      本文标题:Lumen路由实现

      本文链接:https://www.haomeiwen.com/subject/rsankqtx.html