生命周期 11--从生命周期看 Dingo API 是如何接管 Laravel 路由的

前言

在Dingo API视频中老师有一句:“Dingo接管了API路由”,让我思索了许久。。今天终于想清楚了。。

要明白这句话,首先得对laravel生命周期有一定的理解。

如果不熟悉生命周期,建议阅读我小结的如下系列文章(并点赞 :smile:)。

我们知道,访问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 协议》,转载必须注明作者和本文链接
日拱一卒
本帖由系统于 5年前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 11

我觉得讲的有点问题

if ($this->validator->validateRequest($request)) 

这句话 我加了断点,非api的端点也进入了,我理解的是,dingo接管了所有的路由

置于再何时分叉,还要继续加断点

5年前 评论

@jake_zou 这个我怎么说呢?你运行的url是什么,点开源码看看就明白了。生命周期走一遍,不同的方式都走一遍你就明白了。

5年前 评论
kinyou

****一会debug看看, 谢谢楼主的分享!

5年前 评论

@hustnzj
我运行的url是 /
然后 $this->validator->validateRequest($request) 发现可以通过的

5年前 评论

@jake_zou / 我这里是直接不通过的。你配置文件里是怎么配的?

验证代码都在\Dingo\Api\Http\RequestValidator::validateRequest

    public function validateRequest(IlluminateRequest $request)
    {
        $passed = false;

        foreach ($this->validators as $validator) {
            $validator = $this->container->make($validator);

            if ($validator instanceof Validator && $validator->validate($request)) {
                $passed = true;
            }
        }

        // The accept validator will always be run once any of the previous validators have
        // been run. This ensures that we only run the accept validator once we know we
        // have a request that is targeting the API.
        if ($passed) {
            $this->container->make(Accept::class)->validate($request);
        }

        return $passed;
    }

你可以再debug看看是哪里出了问题?

5年前 评论

我是5.7 版本的,我这边确实是通过的,而且不一定是出了问题

5年前 评论

@jake_zou 我也是5.7版本,看看自己的配置文件吧,不一定出了问题?那你debug过程写出来?
环境配置,一个变量都会有影响,差别很大,你要不debug,如何证明不一定出了问题?

5年前 评论

@hustnzj 大神,我就是debug的,否则我怎么知道走了那个函数呢?

5年前 评论

@jake_zou 过程和配置文件。。

5年前 评论

@hustnzj
配置文件真不太方便,我回头找一下原因,再发帖通知你

5年前 评论

@jake_zou 幸好老师这里刚说明了,供参考:问答:dingo 扩展包视频里说 dingo 接管了全部的路由。这句话怎么理解?

估计你还是没有点进去看如何验证的。你要是把验证代码看完了,就知道我说的配置文件的哪些内容了。验证就是两部分,域名和前缀。

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
93
粉丝
85
喜欢
153
收藏
121
排名:71
访问:11.4 万
私信
所有博文
社区赞助商