Laravel 从 $request 到 $response 的过程解析二(必读)

2020-02-19 23:12:44

参考地址 Laravel 从 $request 到 $response 的过程解析

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 处理。


  • 2019-09-16 22:56:52

    java.lang.NoSuchMethodError:SpringJAR包版本冲突错误解决方法

    查询了相关资料,大部分都说引起的原因是JAR包错误或JAR包冲突,查看了配置文件,并没有发现错误之处,因为用的是maven工程,这让我想到了maven的JAR引用的传递性,应该是引用的JAR包中传递引用了相同的包,没有排除。通过mvn dependency:tree 命令查看当前工程引用的依赖JAR树

  • 2019-09-17 17:19:42

    strapi 先进的 headless cms

    为什么叫cms而不是框架,主要是因为他提供了一个程序员操作面板,看起来像是一个综合的后台管理,很容易误解具体项目的后台管理,他更像是一个数据库管理面板顺便完成api的自动开发操作。

  • 2019-09-17 17:20:59

    Headless CMS 的介绍

    在本文中,我们将了解Headless CMS,我们将了解它的优点以及何时使用方便。此外,我们将列举实际的主要限制。为了更好地理解HCMS如何在幕后工作,我将解释如何设计和构建RawCMS,一个带有Oauth2的Aspnet.Core Headless CMS,扩展插件系统,业务逻辑支持。该解决方案可在GitHub上获得,并作为演示版在docker hub上发布。

  • 2019-09-17 17:21:58

    Headless CMS 详细介绍

    什么是 Headless CMS? 为什么 Headless CMS 带有真正的革命性?因为它严格的将内容和格式分离,使我们回归到内容管理的本源。这种变化必然会带来一些不确定性。因此,在开始您的第一个 CMS 项目之前,了解 Headless CMS 概念至关重要。因为它和传统的 CMS 有着本质的区别。