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方法
-
透過路由規則物件的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 協議》,轉載必須註明作者和本文連結