生命周期 11--从生命周期看 Dingo API 是如何接管 Laravel 路由的
前言
在Dingo API视频中老师有一句:“Dingo接管了API路由”,让我思索了许久。。今天终于想清楚了。。
要明白这句话,首先得对laravel生命周期有一定的理解。
如果不熟悉生命周期,建议阅读我小结的如下系列文章(并点赞 :smile:)。
- 生命周期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 协议》,转载必须注明作者和本文链接
推荐文章: