生命周期 11--从生命周期看 Dingo API 是如何接管 Laravel 路由的
前言#
在 Dingo API 视频中老师有一句:“Dingo 接管了 API 路由”,让我思索了许久。。今天终于想清楚了。。
要明白这句话,首先得对 laravel 生命周期有一定的理解。
如果不熟悉生命周期,建议阅读我小结的如下系列文章(并点赞 )。
- 生命周期 1 app 对象解析
- 生命周期 2 kernel 对象解析过程浅析
- 生命周期 3 request 对象创建过程浅析
- 生命周期 4 handle 前期工作
- 生命周期 5 管道流分析
- 生命周期 6 一个 request 是如何匹配到具体路由的源码分析
- 生命周期 7 如何根据 route 对象的信息获取路由和控制器中间件
- 生命周期 8 request 对象是如何通过路由中间件的
- 生命周期 9 如何执行路由对应的方法并返回结果
- 生命周期 10 如何准备待发送 response 对象
我们知道,访问 laravel 应用程序有两种方式:
- 通过浏览器地址栏(普通 web 路由)
- 通过 API 接口(API 路由)
Dingo 接管的路由仅仅是 API 路由,并非全部路由。
下面简要分析一下这两种方式的差别,从而看清楚 "Dingo 是如何接管了 API 的路由" 的。
两种方式的异同点#
先通过一个表格来对比。
特性 | 通过浏览器 | 通过 API |
---|---|---|
访问 url | http://{{host}}/users/7 | http://{{host}}/api/user (user-token 中 id=1) |
路由定义所在文件 | routes/web.php(对 user 使用了资源路由) | routes/api.php(路由直接手写定义) |
引入 composer 自动加载类 | 引入 ComposerLoader 类 | 相同 |
创建 app 对象 | 创建服务容器,并设置全局路径,绑定基础类,注册基本服务,设置核心类的别名 | 相同 |
创建 AppHttpKernel 对象 | 通过服务容器,创建 AppHttpKernel 对象及其依赖的其他对象 | 相同 |
创建 request 对象 | 以 SymfonyRequest 对象为模板创建一个 IlluminateRequest 对象 | 相同 |
AppHttpKernel 的 bootstrap 流程 | 载入环境变量,读取配置文件,设置异常处理,引入 AliasLoader 类,设置 facade 别名,注册各种服务,启动各种服务 | 相同 |
第一个管道流 | IlluminateRequest 对象按正常顺序通过全局中间件过滤后,到达 IlluminateRouter 路由器 | 有差异 |
第二个管道流前 | 中间件:从 Illuminate 路由上绑定的中间件(来自 app/Http/Kernel.php 中的 web 中间件组)和从控制器上设置的中间件。 | 有差异 |
第二个管道流 | 穿过上面的路由 & 控制器中间件 | 有差异 |
与应用程序交互 | 执行控制器方法,返回待处理结果 | 类似 |
准备 response 对象 | 根据返回数据类型,自动选择处理逻辑 | 类似 |
返回 response 对象 -- 第二个管道流 | 进行后置中间件的一些处理后返回 | 类似 |
返回 response 对象 -- 两个管道流之间 | 没有处理 | 有差异 |
返回 response 对象 -- 第一个管道流 | 中间件没有后置处理代码,直接返回 | 相同 |
差异源码分析#
第一个管道流#
在第一个全局中间件 \Dingo\Api\Http\Middleware\Request
的 handle 方法中,就被接管了。
public function handle($request, Closure $next)
{
try {
if ($this->validator->validateRequest($request)) {
$this->app->singleton(LaravelExceptionHandler::class, function ($app) {
return $app[ExceptionHandler::class];
});
$request = $this->app->make(RequestContract::class)->createFromIlluminate($request);
$this->events->fire(new RequestWasMatched($request, $this->app));
return $this->sendRequestThroughRouter($request);
}
} catch (Exception $exception) {
$this->exception->report($exception);
return $this->exception->handle($exception);
}
return $next($request);
}
if ($this->validator->validateRequest($request)) {
这句就进行了判断,如果是 API 接口调用,那么就会转到 dingo 上去执行了,所有的一切变化从此开始。
可以看到,根据 IlluminateRequest 对象创建了 DingoRequest 对象,并代替 IlluminateRequest 对象,进入新的管道流过滤。
新的第一个管道流:
protected function sendRequestThroughRouter(HttpRequest $request)
{
$this->app->instance('request', $request);
return (new Pipeline($this->app))->send($request)->through($this->middleware)->then(function ($request) {
return $this->router->dispatch($request);
});
}
此时通过的中间件是在 \Dingo\Api\Provider\LaravelServiceProvider
的 boot 方法中读取 AppHttpKernel 的全局中间件数组,并绑定到 \Dingo\Api\Http\Middleware\Request
对象中,然后被读取到的。
vendor/dingo/api/src/Provider/LaravelServiceProvider.php:22
public function boot()
{
...
$kernel = $this->app->make(Kernel::class);
$this->app[Request::class]->mergeMiddlewares(
$this->gatherAppMiddleware($kernel)
);
此时创建了 \Dingo\Api\Http\Middleware\Request
对象,并调用其 mergeMiddlewares 将 AppHttpKernel 的全局中间件数组提取处理附加到自己身上。
第二个管道流前#
在 \Dingo\Api\Http\Middleware\Request::sendRequestThroughRouter
,
return $this->router->dispatch($request);
在 \Dingo\Api\Routing\Router::dispatch
中:
public function dispatch(Request $request)
{
...
try {
$response = $this->adapter->dispatch($request, $request->version());
} catch (Exception $exception) {
...
}
return $this->prepareResponse($response, $request, $request->format());
}
在 \Dingo\Api\Routing\Adapter\Laravel::dispatch
中:
public function dispatch(Request $request, $version)
{
if (! isset($this->routes[$version])) {
throw new UnknownVersionException;
}
$routes = $this->mergeOldRoutes($version);
$this->router->setRoutes($routes);
$router = clone $this->router;
$response = $router->dispatch($request);
unset($router);
return $response;
}
路由器已经变成了 DingoRouter,request
变成了 DingoRequest 对象。
在路由器上还增加了一个适配器(adapter),通过适配器来发送 request 到路由。路由还有了版本的区别。
将 IlluminateRouter 中的所有路由和 API 的路由进行合并得到新的路由集合。并将此集合赋给 IlluminateRouter。
克隆一个新的 IlluminateRouter,然后由它去发送 DingoRequest 对象,从而进入了第二个管道流。也就是说第二个管道流是通过 IlluminateRouter 来完成的。
第二个管道流#
找到 request 的匹配路由都跟第一种方式一致。
但在收集中间件时有明细差异,原因在于此时路由的中间件没有通过 web 中间件组,而是通过了 api 中间件组(且 api 中间件组非 app/Http/Kernel.php 中的,而是定义在 routes/api.php 中的)。
代码见 \Illuminate\Routing\Router::runRouteWithinStack
...
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
...
这里的 $this->gatherRouteMiddleware($route)
是通过此路由去找附在其上的中间件。
\Illuminate\Routing\Router::gatherRouteMiddleware
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);
}
\Illuminate\Routing\Route::gatherMiddleware
public function gatherMiddleware()
{
...
return $this->computedMiddleware = array_unique(array_merge(
$this->middleware(), $this->controllerMiddleware()
), SORT_REGULAR);
}
关键在于 $this->middleware()
是什么?
\Illuminate\Routing\Route::middleware
public function middleware($middleware = null)
{
if (is_null($middleware)) {
return (array) ($this->action['middleware'] ?? []);
}
...
return $this;
}
由于传入的参数 $middleware
为 null,因此,会返回 $this->action['middleware']
,也就是说当前匹配路由的 action 属性中的 middlware 键值,此值对最后收集到的中间件有巨大影响。
下面分两种情况具体说明一下。
浏览器调用#
如果当前 url 是 "http://{{host}}/users/7"。
那么这个值是在调用 \App\Providers\RouteServiceProvider
的 boot 方法时,调用 \App\Providers\RouteServiceProvider::mapWebRoutes
,最终调用了 \Illuminate\Routing\Router::createRoute
附加上去的。
...
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
);
//由于此url属于web中间件组
if ($this->hasGroupStack()) {
$this->mergeGroupAttributesIntoRoute($route);
}
因此,$this->action['middleware']
有 web
,而通过 MiddlewareNameResolver::resolve
,就会将 web
转换为数个全局中间件的类全名。这样,就得到了一个全类名中间件集合,用于第二个管道流过滤,内容为:
array (
0 => 'App\\Http\\Middleware\\EncryptCookies',
1 => 'Illuminate\\Routing\\Middleware\\SubstituteBindings',
2 => 'Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse',
3 => 'Illuminate\\Session\\Middleware\\StartSession',
4 => 'Illuminate\\View\\Middleware\\ShareErrorsFromSession',
5 => 'App\\Http\\Middleware\\VerifyCsrfToken',
6 => 'App\\Http\\Middleware\\RecordLastActivedTime',
7 => 'App\\Http\\Middleware\\RedirectIfAuthenticated',
)
API 接口调用#
如果当前 url 是 "http://{{host}}/api/user",附加的 user-token 为 id=1 的用户 token。
那么这个值是在调用 \App\Providers\RouteServiceProvider
的 boot 方法时,调用 \App\Providers\RouteServiceProvider::mapApiRoutes
,然后在定义 $api->get('user', 'UsersController@me')
时,调用了 \Dingo\Api\Routing\Router::addRoute
,然后调用 \Dingo\Api\Routing\Adapter\Laravel::addRoute
附加上去的。
public function addRoute($methods, $uri, $action)
{
...
//获得routes/api.php中定义的嵌套group路由的中间件
$action = $this->mergeLastGroupAttributes($action);
//添加一个preparation controller中间件
$action = $this->addControllerMiddlewareToRouteAction($action);
...
return $this->adapter->addRoute((array) $methods, $action['version'], $uri, $action);
}
public function addRoute(array $methods, array $versions, $uri, $action)
{
$this->createRouteCollections($versions);
$route = new Route($methods, $uri, $action);
$route->where($action['where']);
foreach ($versions as $version) {
$this->routes[$version]->add($route);
}
return $route;
}
这样,也得到了一个全类名中间件集合,用于第二个管道流过滤,内容为:
array (
0 => 'Dingo\\Api\\Http\\Middleware\\PrepareController',
1 => 'Liyu\\Dingo\\SerializerSwitch:array',
2 => 'Illuminate\\Routing\\Middleware\\SubstituteBindings',
3 => 'Dingo\\Api\\Http\\Middleware\\RateLimit',
4 => 'Dingo\\Api\\Http\\Middleware\\Auth',
)
结论#
由此可见,路由的中间件都是在定义路由的时候就已经绑定好了。
返回对象#
返回对象在第一个管道流和第二管道流之间时,会通过 DingoRouter 对返回的 DingoResponse 对象,DingoRequest 对象进行处理。\Dingo\Api\Routing\Router::dispatch
public function dispatch(Request $request)
{
...
try {
$response = $this->adapter->dispatch($request, $request->version());
} catch (Exception $exception) {
...
}
return $this->prepareResponse($response, $request, $request->format());
}
\Dingo\Api\Routing\Router::prepareResponse
protected function prepareResponse($response, Request $request, $format)
{
if ($response instanceof IlluminateResponse) {
$response = Response::makeFromExisting($response);
} elseif ($response instanceof JsonResponse) {
$response = Response::makeFromJson($response);
}
if ($response instanceof Response) {
// If we try and get a formatter that does not exist we'll let the exception
// handler deal with it. At worst we'll get a generic JSON response that
// a consumer can hopefully deal with. Ideally they won't be using
// an unsupported format.
try {
$response->getFormatter($format)->setResponse($response)->setRequest($request);
} catch (NotAcceptableHttpException $exception) {
return $this->exception->handle($exception);
}
$response = $response->morph($format);
}
if ($response->isSuccessful() && $this->requestIsConditional()) {
if (! $response->headers->has('ETag')) {
$response->setEtag(sha1($response->getContent()));
}
$response->isNotModified($request);
}
return $response;
}
注意: $response = $response->morph($format);
如果在控制器中使用了 transformer,那么会在这个地方进行处理,从而可以控制 response 的输出格式。
cheatList#
- vendor/laravel/framework/src/Illuminate/Routing/Router.php:471
- vendor/barryvdh/laravel-debugbar/src/ServiceProvider.php:103
- vendor/dingo/api/src/Provider/LaravelServiceProvider.php:30
- vendor/dingo/api/src/Provider/LaravelServiceProvider.php:34
- vendor/dingo/api/src/Http/Middleware/Request.php:91
- vendor/dingo/api/src/Routing/Router.php:503
- vendor/dingo/api/src/Routing/Adapter/Laravel.php:69
小结#
从上面分析,我们可以得知 dingo 主要是通过自已的路由器,改变了 request 的走向,从而影响了整个程序的运行过程。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: