Laravel 從 $request 到 $response 的過程解析

bytecc發表於2020-01-19

laravel的請求會組裝成$request物件,然後會依次經過中介軟體(前置部分),最終到達url指向的控制器方法,然後把返回資料組裝為$response物件返回,再依次經過中介軟體(後置部分),最終返回。

其實有兩大部分:

1.laravel如何管理中介軟體
2.laravel如何透過url匹配到路由,找到目標方法

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,就是執行我們的目標方法
        };
    }

先解釋三個類

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 協議》,轉載必須註明作者和本文連結
用過哪些工具?為啥用這個工具(速度快,支援高併發...)?底層如何實現的?

相關文章