生命周期 6--一个 request 是如何匹配到具体路由的源码分析
导语
前面已经分析了request对象是如何通过全局中间件过滤后到达路由器。
接下来的大致步骤
- request是如何匹配具体路由。
- 如何根据request对象的信息获取路由和控制器中间件的。
- 如何穿透这些中间件的。
- 穿过后,是如何进入控制器与应用程序交互的(一般写程序只写这么一点。。)。
- 交互后,返回结果又是如何再次通过路由和控制器中间件和全局中间件的。
- 最后的得到response对象。
接下来的书写风格
- 限于时间关系,只说大致思路,不会深入太多。
- 不解释属性含义,代码中有,自行查阅即可。
- 不严格区分方法是从子类和父类执行,除非子类和父类中有相同方法。
- 能够通过注释讲清楚的,优先写注释。
- 方法所属命名空间,不影响理解的都会忽略,自行查阅或debug即可。
几个英文单词
- route: 单个路由
- routes: 多个路由(RouteCollection)
- router: 路由器(放置routes的容器)
如何匹配路由
代码分析起点选择在vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:176
,你我的可能略有差异,如下:
$this->router->dispatch($request);
上面这句之所以没有写上return,是因为限于篇幅和时间限制,本文远远分析不到执行return的地方。
dispatch方法如下:
/**
* Dispatch the request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function dispatch(Request $request)
{
//使用request对象设置kernel的currentRequest属性
$this->currentRequest = $request;
//将request发送到一个路由(哪个?)并返回response
return $this->dispatchToRoute($request);
}
dispatchToRoute方法如下:
/**
* Dispatch the request to a route and return the response.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function dispatchToRoute(Request $request)
{
//通过request对象找到一个匹配的路由,并执行此路由并返回结果。
return $this->runRoute($request, $this->findRoute($request));
}
本文重点来了,如何找到路由
$this->findRoute($request)
findRoute方法如下:
/**
* Find the route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*/
protected function findRoute($request)
{
//找到匹配request的第一个路由,如果找不到抛出NotFoundHttpException
$this->current = $route = $this->routes->match($request);
//设置route对象缓存
$this->container->instance(Route::class, $route);
//返回路由供后面run
return $route;
}
match方法如下:
/**
* Find the first route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function match(Request $request)
{
//通过request的method来获取router中的该方法类型的routes
$routes = $this->get($request->getMethod());
// First, we will see if we can find a matching route for this current request
// method. If we can, great, we can just return it so that it can be called
// by the consumer. Otherwise we will check for routes with another verb.
//从上述的routes中,再通过route对象的matches方法来获取匹配
//request的具体路由route
$route = $this->matchAgainstRoutes($routes, $request);
//将获得的route绑定到request,并返回给findRoute方法。
if (! is_null($route)) {
return $route->bind($request);
}
// If no route was found we will now check if a matching route is specified by
// another HTTP verb. If it is we will need to throw a MethodNotAllowed and
// inform the user agent of which HTTP verb it should use for this route.
//如果上面没找到对应route,那么会检查其他的HTTP动词动词
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {
return $this->getRouteForMethods($request, $others);
}
//如果还找不到,就会抛出NotFoundHttpException
throw new NotFoundHttpException;
}
routes对象的matchAgainstRoutes方法就是神奇匹配的地方:
/**
* Determine if a route in the array matches the request.
*
* @param array $routes
* @param \Illuminate\Http\Request $request
* @param bool $includingMethod
* @return \Illuminate\Routing\Route|null
*/
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
//将传入的routes按照是否有Fallback路由分为两部分。
//注意是Fallback而非callback。。
[$fallbacks, $routes] = collect($routes)->partition(function ($route) {
return $route->isFallback;
});
//然后遍历合并后的routes,对每个route,都使用其matches方法来判断
//request和路由是否完全匹配!
return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
return $value->matches($request, $includingMethod);
});
}
route对象的matches方法:
/**
* Determine if the route matches given request.
*
* @param \Illuminate\Http\Request $request
* @param bool $includingMethod
* @return bool
*/
public function matches(Request $request, $includingMethod = true)
{
//将当前路由(route对象)编译为一个Symfony CompiledRoute instance
//并赋值给route对象的compiled属性,后面会多次用到这个属性。
$this->compileRoute();
//然后是获取当前路由的validators,默认有4个。
//UriValidator, MethodValidator,
//SchemeValidator, HostValidator,
//然后遍历验证器数组,每个验证器处理一个点。
foreach ($this->getValidators() as $validator) {
if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}
//只要有一个验证器没有通过,返回false到前面的match方法中
if (! $validator->matches($this, $request)) {
return false;
}
}
//全部通过,返回true
return true;
}
最后,将找到的route对象返回到dispatchToRoute方法中。
$this->runRoute($request, $this->findRoute($request))
下一步,似乎就可以runRoute了。
runRoute方法
/**
* Return the response for the given route.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Routing\Route $route
* @return mixed
*/
protected function runRoute(Request $request, Route $route)
{
//设置request对象的路由解析器
$request->setRouteResolver(function () use ($route) {
return $route;
});
//触发注册过的路由匹配事件!
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
触发注册过的路由匹配事件!
$this->events->dispatch(new Events\RouteMatched($route, $request))
默认是没有注册这个事件的,但可以在appServiceProvide的boot方法或自己的serviceProvider中的boot方法中自定义,比如在vendor/hieu-le/active/src/ActiveServiceProvider.php
中。
public function boot()
{
// Update the instances each time a request is resolved and a route is matched
$instance = app('active');
if (version_compare(Application::VERSION, '5.2.0', '>=')) {
app('router')->matched( //就是这里
function (RouteMatched $event) use ($instance) {
$instance->updateInstances($event->route, $event->request);
}
);
} else {
app('router')->matched(
function ($route, $request) use ($instance) {
$instance->updateInstances($route, $request);
}
);
}
}
然后就是如何根据request对象的信息获取路由和控制器中间件的(下一篇文章再写)
小结
- serviceProvider的register方法都是用来注册类到serviceContainer中的或者起别名的。
- 其他工作比如添加中间件,添加事件监听器,加载配置信息,环境变量,视图文件等等就放到boot方法中。毕竟有了类,才能使用其中的方法来干事情。
- serviceContainer和各个组件之间往往通过互为属性的关系来达到数据共享。有时也可以通过明确引用传递参数的方式。
- 以上代码分析都比较浅,如果深入研究下去,可以点亮更多技能,是否有用另说。
- 闭包回调非常重要,几乎所有的难点都集中在这上面。
- 看源码就是在学PHP的各种高级用法,开拓思路,同时理解了框架原理,以后用起来会得心应手。
- 读源码时,先看接口方法,再看实现类中的属性和方法,然后再看执行源码过程。执行源码时,先看是哪个对象,然后执行哪个方法或者赋值给哪个属性。
本作品采用《CC 协议》,转载必须注明作者和本文链接
写的相当精彩。尤其最后总结中这句 “看源码就是在学PHP的各种高级用法,开拓思路,同时理解了框架原理,以后用起来会得心应手”,相当精妙。