lumen 的路由实现

文章目录

  • lumen 的路由实现
    • 前言
    • 代码分析
      • 路由的注册
      • 路由的分发
      • 注入依赖
    • request、response 对象解析
      • requset 处理
      • response 处理
        • 处理PsrResponseInterface
        • 处理SymfonyResponse
        • 处理BinaryFileResponse
    • 后记

前言

最近公司项目中使用的框架换成了 lumen , 想到以前面试的时候,面试官都喜欢问一些框架的底层的逻辑,也提到过,laravel 的controller 路由事怎么实现的。
       虽然对于它的路由配置已经很是熟悉了,但是对它的底层原理却很陌生,所以下决心把路由的完整的注册过程,以及分发过程给捋一遍。

代码分析

首先看看我们一般使用时候的配置:
bootstrap/app.php

/** api */
$app->group(['namespace' => 'App\Http\Controller\Api','prefix' =>'/api'], function ($app) {require __DIR__ . '/../routes/api.php';
});

route/api.php

/** api */
$app->group(['prefix' => '/xx', 'middleware' => ['param.xx', 'xx']], function () use ($app) {$app->get('/user/login', 'UserController@login');$app->get('/user/record', 'UserController@userRecord');
}

具体的路由配置不详细解释,可以参考 https://learnku.com/docs/lumen/5.3/routing/1880 。
大概写完上面两步,然后我们就去建 UserController 控制器,在控制器里面建 login 和 userRecord 方法,然后访问URI资源为 /api/xx/user/login (或 /api/user/record)就可以看到我们写的东西了

但是如何通过这两个URI资源访问到这个里面呢?相信很多忙于写业务代码的同行们所忽略的,下面是我对路由的注册和分发的一些理解,如果有什么理解不对的地方,请各位大神斧正 [手动抱拳]

路由的注册

根据上面的两段代码,我们来看看lumen的代码具体是怎么执行的,具体类 vendor/laravel/lumen-framework/src/Concerns/RoutesRequests.php
首先调用了路由的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);
}

group方法中,首先将middleware属性非数组的形式拆分成数组的形式,然后调用updateGroupStack方法

protected function updateGroupStack(array $attributes)
{if (! empty($this->groupStack)) {$attributes = $this->mergeWithLastGroup($attributes);}$this->groupStack[] = $attributes;
}

使用新增加的属性更新当前路由对象中的groupStack属性。更新的时候,是与当前groupStack中最后一个元素进行合并,然后将合并后的属性添加到groupStack的尾部(代码参考mergeWithLastGroup方法,这里就不贴出来了)。一般来说groupStack属性都为空,存在这种情况只会在group嵌套的时候发生,子group会拥有父group的所有属性(我们前面的两段代码则属于子父group嵌套的),兄弟group之间,他们的属性之间没有任何关系(在最后调用了array_pop方法)。当更新好当前的groupStack后,会立即调用当前group所定义的闭包,在这个闭包中我们通常就是调用相关的路由方法,或者定义子group。这样,在当前group中所定义的路由,都会拥有group所定义的路由属性。

嵌套groupgroupStack属性值

array (size=2)0 => array (size=2)'namespace' => string 'App\Http\Controller\Api' 'prefix' => string '/api' 1 => array (size=3)'middleware' => array (size=2)0 => string 'param.xx' 1 => string 'xx' 'prefix' => string 'api/xx' 'namespace' => string 'App\Http\Controller\Api'

未嵌套groupgroupStack属性值


$this->groupStack 组信息中的属性,包含中间件,命名空间,路由前缀,路由后缀,as(别名?)

array (size=3)'prefix' => string 'api''suffix' => string '''middleware' => array (size=2)0 => string 'param.valid' 1 => string 'logdb.send' 'namespace' => string 'App\Http\Controller\Api'

接着看我们经常所使用的 get,post,put,delete,option 等一系列 http 动作的方法,他们的原型都是路由中的 addRoute 方法,来添加路由

public function addRoute($method, $uri, $action)
{// 把 action 解析为数组形式$action = $this->parseAction($action);//包含中间件,命名空间,前缀$attributes = null;if ($this->hasGroupStack()) {$attributes = $this->mergeWithLastGroup([]);}if (isset($attributes) && is_array($attributes)) {if (isset($attributes['prefix'])) {$uri = trim($attributes['prefix'], '/').'/'.trim($uri, '/');}if (isset($attributes['suffix'])) {$uri = trim($uri, '/').rtrim($attributes['suffix'], '/');}$action = $this->mergeGroupAttributes($action, $attributes);}$uri = '/'.trim($uri, '/');if (isset($action['as'])) {$this->namedRoutes[$action['as']] = $uri;}if (is_array($method)) {foreach ($method as $verb) {$this->routes[$verb.$uri] = ['method' => $verb, 'uri' => $uri, 'action' => $action];}} else {$this->routes[$method.$uri] = ['method' => $method, 'uri' => $uri, 'action' => $action];}
}

首先调用parseAction把我们定义的行为action解析成数组形式,从parseAction方法中可以看出我们定义行为action也可以写成['uses' => 'UseController@login', 'middleware' => ['param.xx']]

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

然后判断是否存在group,存在的话需要把组的属性,赋给当前的行为,并且对行为action合并当前组的属性,再判断是否存在别名,存在别名则加入到nameRoutes中,最后把路由放进routes

$this->nameRoutes 别名路由中的信息

array (size=1)'user.login' => string '/api/xx/user/login'

$this->routes 路由数组中的属性,method + uri 作为键

'POST/api/user/login' => array (size=3)'method' => string 'POST' 'uri' => string '/api/user/login' 'action' => array (size=1)'uses' => string 'App\Http\Controllers\Api\UserController@login' 'middleware' => array (size=2)0 => string 'param.valid'1 => string 'logdb.send'
'GET/api/user/record' => array (size=3)'method' => string 'GET' 'uri' => string '/api/user/record''action' => array (size=1)'uses' => string 'App\Http\Controllers\Api\UserController@userRecord''middleware' => array (size=1)0 => string 'logdb.send'

到这里基本上就知道,lumen里面的路由是如何注册的了。它把所有的路由都放在属性routes

路由的分发
public function run($request = null)
{$response = $this->dispatch($request);if ($response instanceof SymfonyResponse) {$response->send();} else {echo (string) $response;}if (count($this->middleware) > 0) {$this->callTerminableMiddleware($response);}
}

路由注册完成之后,调用了$app->run(),运行程序并发送response,首先调用dispatch分发获取到的请求,然后输出结果信息,最后判断是否有中间件,调用结束进程的中间件

首先看看路由分发dispatch方法

public function dispatch($request = null)
{list($method, $pathInfo) = $this->parseIncomingRequest($request);try {return $this->sendThroughPipeline($this->middleware, function () use ($method, $pathInfo) {if (isset($this->routes[$method.$pathInfo])) {return $this->handleFoundRoute([true, $this->routes[$method.$pathInfo]['action'], []]);}return $this->handleDispatcherResponse($this->createDispatcher()->dispatch($method, $pathInfo));});} //处理异常...
}

首先解析了request请求,获取到methodpathInfo,调用sendThroughPipeline方法使用给定的回调函数,通过管道发送request请求,存在中间件的时候,需要使用管道(管道后面再说吧),不存在则直接调用传入的函数

protected function sendThroughPipeline(array $middleware, Closure $then)
{if (count($middleware) > 0 && ! $this->shouldSkipMiddleware()) {return (new Pipeline($this))->send($this->make('request'))->through($middleware)->then($then);}return $then();
}

回调函数中处理的是:存在method + pathInfo的路由,直接调用handleFoundRoute处理转发的路由,否则调用handleDispatcherResponse(根据动态路由来确定,是否存在路由信息?)

先看看直接找到的路由信息的处理方式

protected function handleFoundRoute($routeInfo)
{$this->currentRoute = $routeInfo;$this['request']->setRouteResolver(function () {return $this->currentRoute;});$action = $routeInfo[1];// Pipe through route middleware...if (isset($action['middleware'])) {$middleware = $this->gatherMiddlewareClassNames($action['middleware']);return $this->prepareResponse($this->sendThroughPipeline($middleware, function () {return $this->callActionOnArrayBasedRoute($this['request']->route());}));}return $this->prepareResponse($this->callActionOnArrayBasedRoute($routeInfo));
}

这里的核心处理方法就是调用callActionOnArrayBasedRoute处理请求,调用这个方法,判断是否存在uses,存在的话调用callControllerAction,不存在的话,调用执行绑定的回调函数

protected function callActionOnArrayBasedRoute($routeInfo)
{$action = $routeInfo[1];if (isset($action['uses'])) {return $this->prepareResponse($this->callControllerAction($routeInfo));}foreach ($action as $value) {if ($value instanceof Closure) {$closure = $value->bindTo(new RoutingClosure);break;}}try {return $this->prepareResponse($this->call($closure, $routeInfo[2]));} catch (HttpResponseException $e) {return $e->getResponse();}
}

这个方法里面再执行行为绑定的类方法或者回调函数

执行业务方法的核心方法callControllerAction

protected function callControllerAction($routeInfo)
{$uses = $routeInfo[1]['uses'];if (is_string($uses) && ! Str::contains($uses, '@')) {$uses .= '@__invoke';}list($controller, $method) = explode('@', $uses);if (! method_exists($instance = $this->make($controller), $method)) {throw new NotFoundHttpException;}if ($instance instanceof LumenController) {return $this->callLumenController($instance, $method, $routeInfo);} else {return $this->callControllerCallable([$instance, $method], $routeInfo[2]);}
}

其实路由走到最后都会调用两个核心的方法,调用类方法,或者执行回调的函数,call以及callClasscallClass最后也是会调用call方法

//调用给定的函数方法或者类方法,并注入其中的依赖
public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
{if (static::isCallableWithAtSign($callback) || $defaultMethod) {return static::callClass($container, $callback, $parameters, $defaultMethod);}return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {return call_user_func_array($callback, static::getMethodDependencies($container, $callback, $parameters));});
}//根据字符串调用类方法
protected static function callClass($container, $target, array $parameters = [], $defaultMethod = null)
{$segments = explode('@', $target);$method = count($segments) == 2? $segments[1] : $defaultMethod;if (is_null($method)) {throw new InvalidArgumentException('Method not provided.');}return static::call($container, [$container->make($segments[0]), $method], $parameters);
}
注入依赖

下面是获取依赖的参数的方法,使用反射类获取方法的参数,根据参数来赋值或实例化不同的参数

//获取注入的参数
protected static function getMethodDependencies($container, $callback, array $parameters = [])
{$dependencies = [];foreach (static::getCallReflector($callback)->getParameters() as $parameter) {static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);}return array_merge($dependencies, $parameters);
}
//获取类的反射信息
protected static function getCallReflector($callback)
{//...return is_array($callback)? new ReflectionMethod($callback[0], $callback[1]): new ReflectionFunction($callback);
}
//添加依赖的参数信息
protected static function addDependencyForCallParameter($container, $parameter,array &$parameters, &$dependencies)
{if (array_key_exists($parameter->name, $parameters)) {$dependencies[] = $parameters[$parameter->name];unset($parameters[$parameter->name]);} elseif ($parameter->getClass()) {$dependencies[] = $container->make($parameter->getClass()->name);} elseif ($parameter->isDefaultValueAvailable()) {$dependencies[] = $parameter->getDefaultValue();}
}

方法最后执行的就是我们写的业务代码了,路由分析到这里大概就差不多了,还有动态路由什么的这里没有具体看

request、response 对象解析

在一个http生命周期中,我们最关心的还是输入request,以及输出response,下面简单的看看lumen里面的输入和输出

requset 处理

在前面parseIncomingRequest方法中,我们看到了对request的赋值:Request::capture();,我们来看看这个方法里面捕获了些什么,具体赋值应该是在:vendor/symfony/http-foundation/Request.php:460没有具体分析

public static function capture()
{//开启方法参数覆盖?static::enableHttpMethodParameterOverride();//创建一个 SymfonyRequest 的requestreturn static::createFromBase(SymfonyRequest::createFromGlobals());
}
//根据 SymfonyRequest的实例创建一个 Illuminate的实例 request
public static function createFromBase(SymfonyRequest $request)
{if ($request instanceof static) {return $request;}$content = $request->content;$request = (new static)->duplicate($request->query->all(), $request->request->all(), $request->attributes->all(),$request->cookies->all(), $request->files->all(), $request->server->all());$request->content = $content;$request->request = $request->getInputSource();return $request;
}
response 处理

在看路由分发的时候,最后一直是调用prepareResponse这个方法的,方法里面有三个if判断,分别处理PsrResponseInterfaceSymfonyResponseBinaryFileResponse

public function prepareResponse($response)
{if ($response instanceof PsrResponseInterface) {$response = (new HttpFoundationFactory)->createResponse($response);} elseif (! $response instanceof SymfonyResponse) {$response = new Response($response);} elseif ($response instanceof BinaryFileResponse) {$response = $response->prepare(Request::capture());}return $response;
}
处理PsrResponseInterface
public function createResponse(ResponseInterface $psrResponse)
{$response = new Response($psrResponse->getBody()->__toString(),$psrResponse->getStatusCode(),$psrResponse->getHeaders());$response->setProtocolVersion($psrResponse->getProtocolVersion());foreach ($psrResponse->getHeader('Set-Cookie') as $cookie) {$response->headers->setCookie($this->createCookie($cookie));}return $response;
}
处理SymfonyResponse

前两个最后都是处理成Symfony\Component\HttpFoundation\Response

$response = new Response($response);use Symfony\Component\HttpFoundation\Response as BaseResponse;class Response extends BaseResponse
{...
处理BinaryFileResponse

有兴趣的去看看吧,我没有深入研究
vendor/symfony/http-foundation/Response.php:265

后记

有些地方可能写的不是特别清晰,大家自己最照着源码大概看一看吧!有什么可以指教的,欢迎联系我。QQ:1109563194

lumen 的路由具体实现相关推荐

  1. lumen 项目根目录_在Lumen路由中使用嵌套路由群组

    前段时间写的古诗词文api使用了,Dingo/api,tymondesigns/jwt-auth. 为了更加方便,而不是局限于Dingo/api框架中,我使用spatie/laravel-fracta ...

  2. Lumen 9.x 对路由限流的正确姿势【别乱抄代码了】

    希望大家找到正确的操作姿势,不要直接网上生搬,其实框架内内置好了一些方法的. 配置中间件 中间件类 首先去 Laravel 拿到中间件源码:ThrottleRequests.php,然后在 app/H ...

  3. Lumen、Laravel开发问题记录

    前言:个人杂项 2018.5.17 14:55 1. lumne 连接Sqlite时,一直报错: Call to a member function connection() on null 解决方法 ...

  4. laravel excel迁移到lumen

    1.简介 Laravel Excel 在 Laravel 5 中集成 PHPOffice 套件中的 PHPExcel ,从而方便我们以优雅的.富有表现力的代码实现Excel/CSV文件的导入和 导出  ...

  5. 开始使用Lumen吧,3分钟搞定登陆认证

    用户注册 我们在 Controller/Controller.php 添加 succeed 和 faied 公用接口数据返回方法 通过 status_code 来区分失败和成功 namespace A ...

  6. php rest api lumen,使用Lumen框架创建 REST API 实例教程

    Lumen是一个基于Laravel的微框架,主要用于小型应用和微服务,专注于性能和速度的优化,该框架一个重要的应用就是构建 REST API. 为什么用Lumen构建REST API · Lumen访 ...

  7. php rest api lumen,lumen Rest API 起步

    lumen Rest API 起步 修改项目文件 .env DB_DATABASE= DB_USERNAME= DB_PASSWORD= bootstrap/app.php $app->with ...

  8. 基于 lumen 的微服务架构实践

    lumen 为速度而生的 Laravel 框架 官网的介绍很简洁,而且 lumen 确实也很简单,我在调研了 lumen 相关组件(比如缓存,队列,校验,路由,中间件和最重要的容器)之后认为已经能够满 ...

  9. phpyii框架倒叙_快速入门php框架(Lumen thinkphp Yii)

    打算从三个比较普遍常用的框架带领那些初入门的小伙伴(老鸟勿喷)快速上手一个框架,期间会分享一些自己的编码习惯,和代码思路,这三个框架分别是thinkphp(简单的轻量级国产框架).Lumen(为速度而 ...

最新文章

  1. LeedCode: 计算器专场
  2. HDU ACM 1267 下沙的沙子有几粒?-gt;DP
  3. cudnn v4安装
  4. hihocoder #1465 : 后缀自动机五·重复旋律8
  5. 冲突域 广播域简单解释
  6. leetcode- 225 Implement Stack using Queues
  7. Ubuntu18.04配置Jupyter
  8. Oracle 单实例数据库安装和real application clusters数据库安装的区别
  9. 独立站电商广告和营销洞察
  10. Mocha: 58同城 App 基于卡片的线上 AB 测(线上卡片动态换)
  11. ubuntu 安装pyqt IDE使用eric 辛酸史
  12. 最优化理论与算法(袁亚湘)学习笔记---最优性条件和最优化算法的基本结构
  13. secondary namenode详解
  14. Audio Hijack for Mac(音频录制软件)
  15. 课本剧剧本和计算机专业相关,《滥竽充数》课本剧剧本
  16. v-model是什么?怎么使用?
  17. 港科百创|【未磁科技】勇夺百万大奖,2021年度总决赛圆满收官!香港科大-越秀集团百万奖金国际创业大赛年度总决赛成功举办!...
  18. 蚂蚁p8多少股票_蚂蚁金服上市了,小编不想努力了。
  19. JavaScript一些优雅小技巧不得不知
  20. 微信群创意活动_一小群制造商将大创意转变为用户社区

热门文章

  1. 小乌龟克隆报错:git add not exit cleanly
  2. 串口通信(串口助手发送数据给单片机,单片机原封不动发给串口助手)
  3. Yolov5进阶之一摄像头实时采集识别
  4. 川大和哈工大计算机学院,哈尔滨工业大学计算机科学与技术学院
  5. 网页不能自动播放视频、音频的解决方案
  6. 骨灰级的魔兽伤害计算(包括物理和…
  7. python语言的语法_Python第一章基本语言语法
  8. matlab tek示波器,SIMULINK示波器参数设置_matlab中对示波器进行设置
  9. 暴雪战网怎么修改服务器,战网更改地区的图文教程
  10. isbn书号查询php代码,php根据isbn书号查询amazon网站上的图书信息的示例_PHP教程