laravel 的请求会组装成 $request 对象,然后会依次经过中间件(前置部分),最终到达 url 指向的控制器方法,然后把返回数据组装为 $response 对象返回,再依次经过中间件 (后置部分),最终返回。
其实有两大部分:
1.laravel 如何管理中间件
2.laravel 如何通过 url 匹配到路由,找到目标方法
第一部分,laravel 通过管道来管理中间件
laravel 的中间件的管理都是通过管道来实现的,把注册的中间件数组传递到管道中,管道类会按照我们的顺序执行这些中间件。
实例化 App\Http\Kernel::class
我们知道,框架执行是通过 Kernell->hand () 方法开始的。看看 kernel 的实例化
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;
$router->middlewarePriority = $this->middlewarePriority;//优先级中间件,用于中间件排序
foreach ($this->middlewareGroups as $key => $middleware) {//中间件组
$router->middlewareGroup($key, $middleware);
}
foreach ($this->routeMiddleware as $key => $middleware) {//路由中间件
$router->aliasMiddleware($key, $middleware);
}
}
实例化 http 核心类,就是把 middlewarePriority,middlewareGroups,aliasMiddleware 注册到路由类的属性中。所以这些中间件的执行都是要再路由解析后执行的。
管道通过 Illuminate\Pipeline\Pipeline 类来实现。
框架执行是通过 Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter () 来实现控制器访问
return (new Pipeline($this->app)) //传入$app实例化管道类
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
这个就是启动后,传入 $request,通过管道的调度,执行各种中间件后返回 $response 实例。
1.send()
public function send($passable)
{
$this->passable = $passable; //就是把$request对象挂到属性passable中
return $this;
}
2.through()
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();//这个就是中间件数组
return $this;
}
我们看看这时候的中间件有哪些?$this->middleware,这个就是全局中间件,定义在 Illuminate\Foundation\Http\Kernel 类的 middleware 属性中。
3.then (), 这个才是执行的关键
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
array_reduce (参数 1,参数 2,参数 3) 是 php 的自带的函数,参数 1 是一个数组,参数 2 是一个函数,参数 3 选填。需要去查看手册了解清楚这个函数的原理。
4.carry () 就是把中间件封装成闭包
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if (is_callable($pipe)) {
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
[$name, $parameters] = $this->parsePipeString($pipe);
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
$parameters = [$passable, $stack];
}
$response = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $response instanceof Responsable
? $response->toResponse($this->getContainer()->make(Request::class))
: $response;
};
};
}
array_reduce 就是把中间件的闭包嵌套起来。可以参考一下这一篇 https://learnku.com/articles/38189#reply127271
简单来说:
array_reduce( [a,b], 'carry', fun);
比如有中间件数组为 [a,b] 两个个中间件实例,参数 3 为闭包 fun, carry () 方法会得到三个闭包函数 funA,funB。fun 会在 funA 肚子里面,funA 会在 funB 肚子里面。这就是函数嵌套的关系。array_reduce 返回的是 funB。执行 funB 的时候,运行到 $next (),就是调用 funA。所以 fun 是在这个嵌套的最底层。
嵌套中最底层的函数,就是 then 的参数
$this->prepareDestination($destination),//这个就是我们路径要访问的控制器的闭包执行,
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable); //这个就是执行控制器方法,$passable就是$request
};
}
1. 先看看这个 $destination,就是执行我们目标控制器方法,返回 $response
App\Http\Kernel 类的 dispatchToRouter 方法
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);//路由类调用dispatch,就是执行我们的目标方法
};
}
第二部分,路由类通过 request 匹配到路由,执行对应的控制器方法
先解释三个类
Illuminate\Routing\Router 路由类,就是门面Route::调用的那个类,负责路由实现的对外接口
Illuminate\Routing\Route 路由规则类,我们用Route::get(),就会生成一个路由规则对象,相当于一个url路径,就会有一个路由规则实例。路由匹配的时候,就是要找到对应的路由规则类。
Illuminate\Support\Collection 集合类,其实是laravel的一个工具类,可以把数组转为集合,然后使用集合类封装的方法处理各个元素。
路由类 Illuminate\Routing\Router
public function dispatch(Request $request)
{
$this->currentRequest = $request;//把request赋值给属性currentRequest
return $this->dispatchToRoute($request);
}
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));//通过request,找到匹配的路由规则对象
}
1. 通过 request, 找到匹配的路由规则对象
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
//$this->routes就是Illuminate\Routing\RouteCollection类,在路由实例化的时候注入进来的
所以说真正执行 match 操作的是 Illuminate\Routing\RouteCollection 类,看一下 match 方法
public function match(Request $request)
{
$routes = $this->get($request->getMethod()); //通过请求方法,得到所有的路由规则,比如get
$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;
}
//$routes就是get/post下的所有路由规则对象组成的数组
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
//把$routes规则数组转为集合
[$fallbacks, $routes] = collect($routes)->partition(function ($route) {
return $route->isFallback;
});
// first()方法返回集合中通过指定条件的第一个元素:
return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {
return $value->matches($request, $includingMethod);
});
}
最终匹配是根据路由规则类的 matches 方法来做的,如果匹配上就返回 true
路由规则类其实是 Illuminate\Routing\Route, 也就是说 $routes 数组的元素是 Illuminate\Routing\Route 类的实例,一条路由规则就是一个实例。
2. 路由规则类的介绍 Illuminate\Routing\Route
我们知道,laravel 的路由规则都需要我们在 routes 目录下定义,比如 routes\web.php
Route::group(['prefix'=>'article'], function(){
Route::get('index', 'ArticleController@index');
Route::post('create', 'ArticleController@create');
Route::get('edit/{article}', 'ArticleController@edit');
Route::get('show/{article}', 'ArticleController@show');
});
这时候就会生成 4 个路由规则对象,保存在 Illuminate\Routing\Router 的属性中,比如上面讲的 $routes 路由规则数组,因为我是通过 GET 访问,打印出来就是是这样的
Collection {#306 ▼
#items: array:14 [▼
"_debugbar/open" => Route {#129 ▶}
"_debugbar/clockwork/{id}" => Route {#130 ▶}
"api/user" => Route {#180 ▶}
"article/index" => Route {#182 ▶}
"article/edit/{article}" => Route {#184 ▶}
"article/show/{article}" => Route {#185 ▶}
]
}
当然因为我安装了 debugbar 包,所以还有一些其他的路由规则注册进来了,但是还是可以看到有三个 article 的路由规则对象。每个路由规则对象都包含了对应的 uri,method,controller,路由参数等等。具体如何生成路由规则对象,并且注册到路由属性中,可以看 Route::get () 方法。
我们可以看一下一个路由规则对象有哪些属性
比如 Route::get ('index', 'ArticleController@index') 这个语句生成的路由规则对象
Route {#182 ▼
+uri: "article/index"
+methods: array:2 [▶]
+action: array:6 [▶]
+isFallback: false
+controller: null
+defaults: []
+wheres: []
+parameters: []
+parameterNames: []
#originalParameters: []
+computedMiddleware: null
+compiled: CompiledRoute {#324 ▶}
#router: Router {#26 ▶}
#container: Application {#2 ▶}
}
3. 循环所有的路由规则对象,用路由规则对象的 matches 来判断是否匹配上
Illuminate\Routing\Route 路由规则对象的 matches 方法
public function matches(Request $request, $includingMethod = true)
{
$this->compileRoute();//路由规则的正则编译
foreach ($this->getValidators() as $validator) {
if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}
if (! $validator->matches($this, $request)) {
return false;
}
}
return true;
}
//通过RouteCompiler类编译路由规则实例
protected function compileRoute()
{
if (! $this->compiled) {//一个路由规则实例只编译一次,编译完成会标识
$this->compiled = (new RouteCompiler($this))->compile(); //编译成功后返回正则编译对象
}
return $this->compiled;
}
3.1 路由规则的正则编译是通过 Symfony 框架来实现,最终得到一个正则编译对象
还是比较复杂的,原理就是通过正则表达式来判断路由规则实例是否匹配上,这里就不展开细节了,可以看一下这个博客 https://learnku.com/articles/5426/laravel-http-routing-uri-regular-compilation
不过可以看看一下这个正则编译后返回的对象 $this->compiled,路由规则是 Route::get ('index', 'ArticleController@index')
CompiledRoute {#309 ▼
-variables: []
-tokens: array:1 [▶]
-staticPrefix: "/_debugbar/open"
-regex: "#^/_debugbar/open$#sDu"
-pathVariables: []
-hostVariables: []
-hostRegex: null
-hostTokens: []
}
返回一个 Symfony\Component\Routing\CompiledRoute 对象。
3.2 四个验证器验证路由规则是否匹配
public static function getValidators()
{
if (isset(static::$validators)) {
return static::$validators;
}
return static::$validators = [
new UriValidator, new MethodValidator,
new SchemeValidator, new HostValidator,
];
}
这四个路由验证器类在 Illuminate\Routing\Matching\ 目录下,他们将分别使用 matches 来验证路由是否匹配上,只要有一个验证不通过,就表示不匹配。
//UriValidator验证器
public function matches(Route $route, Request $request)
{
$path = $request->path() === '/' ? '/' : '/'.$request->path();
return preg_match($route->getCompiled()->getRegex(), rawurldecode($path));
}
//MethodValidator验证器
public function matches(Route $route, Request $request)
{
return in_array($request->getMethod(), $route->methods());
}
//SchemeValidator验证器
public function matches(Route $route, Request $request)
{
if ($route->httpOnly()) {
return ! $request->secure();
} elseif ($route->secure()) {
return $request->secure();
}
return true;
}
//HostValidator验证器
public function matches(Route $route, Request $request)
{
if (is_null($route->getCompiled()->getHostRegex())) {
return true;
}
return preg_match($route->getCompiled()->getHostRegex(), $request->getHost());
}
其中 UriValidator 验证,HostValidator 验证都需要正则编译对象来实现。
4. 得到匹配的路由规则对象,执行路由类的 runRoute 方法
Illuminate\Routing\Router
$this->runRoute($request, $this->findRoute($request));//$this->findRoute($request)就是返回匹配上的路由规则对象
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {//向request绑定路由规则对象
return $route;
});
$this->events->dispatch(new Events\RouteMatched($route, $request));//监听RouteMatched事件
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
先看看如何运行方法
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()
);
});
}
我们开头说过,只有全局中间件,才是在路由解析前放入到管道中的,而我们的路由中间件,中间件组,只有执行到这里时才会加入到管道中的。
5. 如何得到路由解析后的中间件
Kernell 实例化的时候,已经把所有的路由中间件,中间件组注册到路由类的属性中,我们只要匹配需要执行的中间件即可。
Illuminate\Routing\Router
public function gatherRouteMiddleware(Route $route)
{
$middleware = collect($route->gatherMiddleware())->map(function ($name) {
return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups);
})->flatten();
return $this->sortMiddleware($middleware);//把得到的中间件实例排序
}
首先去对应的路由规则类获取中间件信息(比如这个路由绑定的中间件别名,中间件组的 key)
Illuminate\Routing\Route
public function gatherMiddleware()
{
if (! is_null($this->computedMiddleware)) {
return $this->computedMiddleware;
}
$this->computedMiddleware = [];
return $this->computedMiddleware = array_unique(array_merge(//数据有两个来源
$this->middleware(), $this->controllerMiddleware()
), SORT_REGULAR);
}
路由规则中间件信息源头一 $this->middleware ()
Illuminate\Routing\Route
public function middleware($middleware = null)
{
if (is_null($middleware)) {//每有传参数时
return (array) ($this->action['middleware'] ?? []);
}
if (is_string($middleware)) {//把参数转为数组
$middleware = func_get_args();
}
$this->action['middleware'] = array_merge(//有传参数时
(array) ($this->action['middleware'] ?? []), $middleware
);
return $this;
}
这个路由规则的 middleware ($middleware) 的方法有两个作用:
没传参数时,返回 $this->action ['middleware'] 属性的值
有参数传入时,会把参数整合到 $this->action ['middleware'] 属性中
我们知道,每一条路由都会生成一个路由规则对象,路由规则对象生成的时候,如果是在 web.php 的路由,会向这个路由规则传入‘web’,如果路由定义在 api.php,这里就会传参数 'api'。
当我们定义路由规则 middleware (‘test’),例如
Route::get('/user', 'Home\UserController@user')->middleware('test');
就会向这个路由规则传入 'test'
路由规则中间件信息来源二 $this->controllerMiddleware ()
Illuminate\Routing\Route
public function controllerMiddleware()
{
if (! $this->isControllerAction()) {
return [];
}
return $this->controllerDispatcher()->getMiddleware(
$this->getController(), $this->getControllerMethod()
);
}
综合上述两个来源,如果访问 web.php 中的路由 Route::get ('/user', 'Home\UserController@user')->middleware ('test'),
$route->gatherMiddleware () 会返回 ['web','test'] 数组。通过 MiddlewareNameResolver::resolve 就得到了对应的中间件实例了。
6. 再次通过管道把中间件封装成闭包嵌套起来。
Illuminate\Routing\Router
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
我们看到,嵌套最底层的就是我们控制器的方法,$route->run (),终于找到你了,就是路由规则对象的 run 方法
通过路由规则对象的 run 方法执行
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();
}
}
//执行controller
protected function runController()
{
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
laravel 执行 controller 也是通过 controllerDispatcher 类来执行的,先看看需要什么参数
7.1 通过路由规则对象,从容器获取目标控制器对象
Illuminate\Routing\Router
public function getController()
{
if (! $this->controller) {
$class = $this->parseControllerCallback()[0];
$this->controller = $this->container->make(ltrim($class, '\\'));
}
return $this->controller;
}
7.1 通过路由规则对象,得到目标方法名
protected function getControllerMethod()
{
return $this->parseControllerCallback()[1];
}
7.3 获取控制器分发器
public function controllerDispatcher()
{
if ($this->container->bound(ControllerDispatcherContract::class)) {
return $this->container->make(ControllerDispatcherContract::class);
}
return new ControllerDispatcher($this->container);
}
8 通过控制器分发器执行目标
Illuminate\Routing\ControllerDispatcher
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(//通过反射获取参数
$route->parametersWithoutNulls(), $controller, $method
);
if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}
return $controller->{$method}(...array_values($parameters));//这里返回的是方法的返回值
}
泪奔了,终于看到控制器调用方法了。不过还有一个问题,我们的目标方法的参数如果是对象,我们还要解析出来。
8.1 通过反射准备目标方法的参数
protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
if (! method_exists($instance, $method)) {
return $parameters;
}
return $this->resolveMethodDependencies(
$parameters, new ReflectionMethod($instance, $method)
);
}
8.2 把控制器 return 的内容封装为 response 对象
Illuminate\Routing\Router,我们再看看这个方法,$route->run (), 返回值是控制器 的 return 内容,还需要 prepareResponse 进行处理。
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
public function prepareResponse($request, $response)
{
return static::toResponse($request, $response);
}
//根据方法return内容的数据类型,组装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);//数组,json等等
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);//字符串
}
if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
$response->setNotModified();
}
return $response->prepare($request);
}
我们简单分析一下,如果我们的方法返回字符串,数组,模型对象,response 对象有什么区别
1. 控制器返回字符串
$response = new Response($response);//参数$response是字符串
Response {#404 ▼
+headers: ResponseHeaderBag {#366 ▶}
#content: "aaaa" //字符串内容
#version: "1.0"
#statusCode: 200
#statusText: "OK"
#charset: null
+original: "aaaa"
+exception: null
}
2. 如果是数组或者对象
public function setData($data = [])
{
try {
$data = json_encode($data, $this->encodingOptions);//会把data进行json_encode
} catch (\Exception $e) {
if ('Exception' === \get_class($e) && 0 === strpos($e->getMessage(), 'Failed calling ')) {
throw $e->getPrevious() ?: $e;
}
throw $e;
}
if (JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException(json_last_error_msg());
}
return $this->setJson($data);//json_encode后挂到属性data中
}
public function setContent($content)
{
if (null !== $content && !\is_string($content) && !is_numeric($content) && !\is_callable([$content, '__toString'])) {
throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', \gettype($content)));
}
$this->content = (string) $content; //把属性data的值写入到属性content
return $this;
}
JsonResponse {#404 ▼
#data: "["aa",["bb"]]" //对数组,对象镜像json_encode
#callback: null
#encodingOptions: 0
+headers: ResponseHeaderBag {#366 ▶}
#content: "["aa",["bb"]]" //对数组,对象镜像json_encode
#version: "1.0"
#statusCode: 200
#statusText: "OK"
#charset: null
+original: array:2 [▶]
+exception: null
}
所以说最后 $response 保存内容都在 content 属性中,如果是数组,或者对象,会进行 json_encod 处理。