最近公司项目中使用的框架换成了 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所定义的路由属性。
嵌套group
的groupStack
属性值
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'
未嵌套group
的groupStack
属性值
$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
请求,获取到method
、pathInfo
,调用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
以及callClass
,callClass
最后也是会调用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();
}
}
方法最后执行的就是我们写的业务代码了,路由分析到这里大概就差不多了,还有动态路由什么的这里没有具体看
在一个http
生命周期中,我们最关心的还是输入request
,以及输出response
,下面简单的看看lumen
里面的输入和输出
在前面parseIncomingRequest
方法中,我们看到了对request
的赋值:Request::capture();
,我们来看看这个方法里面捕获了些什么,具体赋值应该是在:vendor/symfony/http-foundation/Request.php:460
没有具体分析
public static function capture()
{
//开启方法参数覆盖?
static::enableHttpMethodParameterOverride();
//创建一个 SymfonyRequest 的request
return 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;
}
在看路由分发的时候,最后一直是调用prepareResponse
这个方法的,方法里面有三个if
判断,分别处理PsrResponseInterface
、SymfonyResponse
、BinaryFileResponse
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;
}
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;
}
前两个最后都是处理成Symfony\Component\HttpFoundation\Response
类
$response = new Response($response);
use Symfony\Component\HttpFoundation\Response as BaseResponse;
class Response extends BaseResponse
{
...
有兴趣的去看看吧,我没有深入研究
vendor/symfony/http-foundation/Response.php:265
有些地方可能写的不是特别清晰,大家自己最照着源码大概看一看吧!有什么可以指教的,欢迎联系我。QQ:1109563194