Laravel 底层分析:生命周期——路由器(第三部分)
本篇用于介绍 Laravel 5.6 底层源码
回顾
在 第1节中 ,我们回顾了加载完 index.php
文件之后会发生什么,即使是在加载核心服务和框架之前。我们看到了 Laravel 如何在容器中注册必要的服务提供者,例如路由器,日志记录器和事件调度器。在 第2节中 ,我们回顾了框架如何加载配置,设置错误处理,注册全部服务提供者并且解决 facades 。重新梳理一下你的记忆,这个过程发生在 handle
方法中调用 Kernel 类中的 bootstrap
方法。
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
让我们在第三部分尝试调试这些吧!
路由中间件堆栈(利用管道类,从第一个中间件过滤到最后一个中间件)
首先,我们可以尝试通过计算 shouldSkipMiddleware()
的值何时为 true 来简化这段代码。这个方法 我们假设为 true 如果字符串文字 middleware.disable
绑定到容器,并且为 true (将其视为全局配置)。注意,这个字符串是在您在测试中导入 WithoutMiddleware
特性时设置的。我们不会深入讨论,因为这不在本课的范围内,但是如果你想知道更多, 下面是它的代码 。 现在我们知道空数组只在测试中传递,我们知道我们传递给 through
方法的是什么。下面是我们的全局中间件列表:
array:6 [▼
0 => "Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode"
1 => "Illuminate\Foundation\Http\Middleware\ValidatePostSize"
2 => "App\Http\Middleware\TrimStrings"
3 => "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull"
4 => "App\Http\Middleware\TrustProxies"
// 其他全局中间件,例如来自 laravel-debugbar 库的 InjectDebugbar
]
我们也可以内联 dispatchToRouter
方法,因为它只返回一个逻辑很少到没有逻辑的闭包。现在让我们来看看我们的简化代码:
return (new Pipeline($this->app))
->send($request)
->through($this->middleware)
->then(function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
});
现在我们可以将这段代码分解为几个部分: 创建一个管道,通过堆栈传递对象,最后在请求通过堆栈后解析闭包。
管道
可以把管道理解为类的栈。我们拿到一个对象(例如 request),按需修改它,然后传递到下一个类中,执行与之前栈元素中相同的方法。似曾相识吧?当你在你的 App 命名空间中创建一个中间件时,会有一个 handle
方法,还记得么? 对的,这个方法接收前一个栈元素传递过来的请求并将其传递给该栈的下一个元素($next($request)
)。
使用案例
管道的完美用例是通过中间件运行请求。我们接受一个请求,将它传递到第一个中间件类,修改它,然后在下一个中间件类上做同样的事情。如果一路上有什么失败了,我们只需将客户机推回到堆栈的顶部,显示错误,不要让他通过。
实现
如果我们打开 Pipeline 类,可以看到它的命名空间是 Illuminate\Routing
,同时他继承了 Illuminate\Pipeline
命名空间下的 Pipeline 类。这样做的原因是因为在 Routing Pipeline 中需要做一些修改。在创建类的实例并且将容器传递给构造函数之后,还没有发生任何事情。接下来,我们使用 ->send($passable)
来分配将要通过管道传递的对象,这里我们用请求来举例。然后,我们必须分配对象将被发送时的栈,这里的例子是使用 ->through($pipes)
来传递全局中间件列表。注意,此时我们只是在分配类的属性,还没有发送任何内容。 接下来有趣的是在我们执行 then()
方法的时候,让我们看看那个方法的源码 :
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
请注意 $this->pipes
是中间件列表,$this->passable
是请求,而 $destination
是最终目标,在这个例子中,我们接受了剩余的请求并传递给路由器
该函数实现了通过每个中间件来执行我们的请求,之后评估最终目标(调度到路由器)。我们可以利用PHP自带的 array_reduce 函数 通过闭包的方式彻底迭代中间件数组。由于这个过程的逻辑非常复杂,我们需要非常多的时间来分解每一个组件并描述他的作用,并且我们还有路由需要检查,所以现在让我们进入路由来一探究竟。
Routing
现在我们已经通过了中间件, 我们获得了经过中间件修改后的 request 传递到了 router. 可在此查看 在 ->router->dispatch($request)
方法里 (这个方法被之后的闭包方法所使用), 我们可以看到我们在这此委托了 dispatchToRoute
方法, 之后 dispatchToRoute
方法执行了 runRoute
方法:
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
在我们执行 route 的的具体行为之前, 我们必须先找到和 URI 匹配的 Route.
在 Request object 里的 requestUri
和 method
这两个属性可以帮我们做到这一点. 在 findRoute
方法里, 我们使用 match
方法来匹配在 RouteCollection
找到的route, 然后再映射到 Route 对象, 最后我们绑定这个 Route 对象 到 Container. 注意 RouteCollection
是我们注册 core RoutingServiceProvider (请参考: part 1) 时, 在 constructor 里实例化的一个 Class.
匹配路由
Illuminate\Routing\RouteCollection
中 match
方法的具体内容如下所示:
public function match(Request $request)
{
$routes = $this->get($request->getMethod());
$route = $this->matchAgainstRoutes($routes, $request);
if (! is_null($route)) {
return $route->bind($request);
}
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {
return $this->getRouteForMethods($request, $others);
}
throw new NotFoundHttpException;
}
$this->get($request->getMethod())
获取到所有已定义的路由。例如,如果请求是 GET 类型的,我们就会返回一个包含所有能被 GET 方法访问的路由数组。这个数组的结构为uri => Route-object
。
array:27 [▼
"api/user" => Route {#298 ▶}
"/" => Route {#300 ▶}
"login" => Route {#307 ▶}
"register" => Route {#310 ▶}
"password/reset" => Route {#312 ▶}
"password/reset/{token}" => Route {#314 ▶}
]
然后,我们会使用matchAgainstRoute
方法对$routes
进行匹配,这个方法本质上是循环调用 Route 对象中的 matches 方法来查看是否匹配。然后将匹配到的第一个结果进行返回并且停止循环(利用 Collection@first
方法)。注意我们会先使用partition
方法,然后使用 merge
方法将回退路由放到最后。这是因为我们首先想匹配常规路由,然后才是会退路由。
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
list($fallbacks, $routes) = collect($routes)->partition(function ($route) {
return $route->isFallback;
});
return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
return $value->matches($request, $includingMethod);
});
}
Route
类中的matches
方法解析并执行不同的路由验证:URIValidator
,SchemeValidator
,HostValidator
和 MethodValidator
。这些验证类会尝试对路由按照各自的目的进行验证。如果你想了解更多,可以看看源代码。由于这是一个很复杂的内容,或许我会单独写关于路由的文章再进行阐述。本质上,我们只是将请求映射到编译好的路由,但是我们页需要匹配请求方法(译者注:此处指GET,POST等)。如果找到了匹配的路由,我们将请求和路由绑定并返回。然而,如果没有路由匹配到,我们会使用checkForAlternateVerbs($request)
方法检查 URI 是否存在并且使用了不同的HTTP方法。如果不存在,就会抛出一个异常。
执行路由
现在我们已经找到了路由,我们已经准备好执行了。runRoute
方法就是这个作用。如果我们看一下这个方法的定义,我们会发现:
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
注意我们需要在 Request 类中设置解析路由。当你使用$request->route()
方法时,解析路由会找到指定的路由并返回给你。然后,我们触发匹配路由事件,接着,我们“在堆栈内执行路由”。这句话是什么意思?我们需要看一下源代码进行理解。
路由中间件
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
}
是的,这段代码看起来很熟悉 —— 中间件。我们又创建了一个管道,但是这次我们将请求通过针对这个路由定义好的中间件进行处理。由于我们已经知道管道的工作原理,我们只需查看闭包中的then
方法:
function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
}
调度控制器
这是我们执行路由、分发控制器并返回响应结果到 prepareResponse
的地方
public function run()
{
$this->container = $this->container ?: new Container;
try {
if ($this->isControllerAction()) {
return $this->runController();
}
return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}
看看这个方法,我们可以看到,我们只是试图分发控制器(如果路由被定义为控制器操作),或者执行回调(如果路由被定义为执行闭包),或者抛出一个错误。isControllerAction
检查路由中的 uses
参数是否为字符串(如 Route::get('/', 'SomeController@someMethod')
)。如果是,则执行 runController
方法。如果不是,则执行 runCallable
方法,这个方法执行的是闭包式路由。阅读 runController
,可以看出它确实是这么实现的:
protected function runController()
{
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
很多代码都是在这种情况下产生的,所以我们先不深入。注意:这是自动地依赖注入所有控制器的地方。
因此,本质上,$route->run()
执行为此路由定义的控制器方法并返回一些响应(注意,这是你在控制器操作中返回的响应),然后响应输出会传递给 prepareResponse
方法。
响应
其实 prepareResponse
方法实际上调用了静态的 toResponse
方法,他的目的是创建一个新的 Response
类并准备将响应发回给客户端。让我们瞧瞧:
public static function toResponse($request, $response)
{
if ($response instanceof Responsable) {
$response = $response->toResponse($request);
}
if ($response instanceof PsrResponseInterface) {
$response = (new HttpFoundationFactory)->createResponse($response);
} elseif ($response instanceof Model && $response->wasRecentlyCreated) {
$response = new JsonResponse($response, 201);
} elseif (! $response instanceof SymfonyResponse &&
($response instanceof Arrayable ||
$response instanceof Jsonable ||
$response instanceof ArrayObject ||
$response instanceof JsonSerializable ||
is_array($response))) {
$response = new JsonResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);
}
if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
$response->setNotModified();
}
return $response->prepare($request);
}
请注意,参数中的 $response
变量是从控制器中返回的准确的输出。 这是一个必须的过程,这个方法已经最好了修改响应的准备。我们返回一个 Model 实例?没问题,判断 $response
是否是一个模型,并建立一个 JsonResponse
类以完成。我们返回一个字符串?没问题,那就建立一个正则 Response
类。我们返回任何一个序列化的JSON串?可以将它转化成 JsonResponse
对象来处理。 注意,如果你想返回 Eloquent 模型并且这个资源刚刚创建(仅仅在这个请求中),Larave 将会返回一个 201响应
目前在我们的整个生命周期中唯一剩下的就是检查我们的 Response 对象中的 prepare($request)
方法,并且完成 index.php
中的剩余代码 -- 将响应发送到客户端并且执行任意 terminate 中间件。在下一节中,我们将查看 Response 类并且深入了解 Symfony's Response 组件,而且可能会完成我们的 「生命周期」系列。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。