Laravel 从 $request 到 $response 的过程解析

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方法

  1. 通过路由规则对象的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处理。

本作品采用《CC 协议》,转载必须注明作者和本文链接
用过哪些工具?为啥用这个工具(速度快,支持高并发...)?底层如何实现的?
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 2

复杂度不低

4年前 评论

@swing07 laravel确实用了很多工具或者思想来处理流程,特别是管道处理中间件部分,一开始不是很好理解。

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!