什么是中間鍵
對于一個Web應用來說,在一個請求真正處理前,我們可能會對請求做各種各樣的判斷,然后才可以讓它繼續傳遞到更深層次中。而如果我們用if else
這樣子來,一旦需要判斷的條件越來越來多,會使得代碼更加難以維護,系統間的耦合會增加,而中間件就可以解決這個問題。我們可以把這些判斷獨立出來做成中間件,可以很方便的過濾請求。
Laravel中的中間件:
在Laravel中,中間件的實現其實是依賴于管道Illuminate\Pipeline\Pipeline
這個類實現的,我們先來看看觸發中間件的代碼。很簡單,就是處理后把請求轉交給一個閉包就可以繼續傳遞了。
public function handle($request, Closure $next) {
//對請求做的一些事
return $next($request);
}
中間件內部實現原理
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request,
$route->run($request)
);
});
可以看到,中間件執行過程調用了三個方法。再來看看這三個方法的代碼:
send方法
public function send($passable){
$this->passable = $passable;
return $this;
}
send方法就是設置了需要在中間件中流水處理的對象,在這里就是HTTP請求實例。
through方法
public function through($pipes){
$this->pipes = is_array($pipes) ? $pipes :func_get_args();
return $this;
}
through方法就是設置一下需要經過哪些中間件處理。
then方法
public function then(Closure $destination){
//接受一個閉包作為參數,然后經過getInitialSlice包裝,而getInitialSlice返回的其實也是一個閉包
$firstSlice = $this->getInitialSlice($destination);
//反轉中間件數組,主要是利用了棧的特性
$pipes = array_reverse($this->pipes);
//call_user_func 就是執行了一個array_reduce返回的閉包
return call_user_func(
//array_reduce來用回調函數處理數組。其實 arrary_reduce 就是包裝閉包然后移交給call_user_func來執行
array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
);
}
由于aray_reduce
的第二個參數需要一個函數,我們這里重點看看getSlice()
方法的源碼
protected function getSlice(){
return function ($stack, $pipe) { //這里$stack
return function ($passable) use ($stack, $pipe) {
if ($pipe instanceof Closure) {
return call_user_func($pipe, $passable, $stack);
} else {
list($name, $parameters) = $this->parsePipeString($pipe);
return call_user_func_array([$this->container->make($name), $this->method],
array_merge([$passable, $stack],
$parameters)
);
}
};
};
}
看到可能會很頭暈,閉包返回閉包的。簡化一下就是getSlice()
返回一個函數A
,而函數A
又返回了函數B
。為什么要返回兩個函數呢?因為我們中間在傳遞過程中是用$next($request)
來傳遞對象的,而$next($request)
這樣的寫法就表示是執行了這個閉包,這個閉包就是函數A
,然后返回函數B
,可以給下一個中間件繼續傳遞。
再來簡化一下代碼就是:
//這里的$stack其實就是閉包,第一次遍歷的時候會傳入$firstSlice這個閉包,
//以后每次都會傳入下面的那個function; 而$pipe就是每一個中間件
array_reduce($pipes, function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
};
}, $firstSlice);
再來看這一段代碼:
/*判斷是否為閉包,這里就是判斷中間件形式是不是閉包,是的話直接執行并且傳入$passable[請求實例]和$stack[傳遞給下一個中間件的閉包],
并且返回*/
if ($pipe instanceof Closure) {
return call_user_func($pipe, $passable, $stack);
//不是閉包的時候就是形如這樣Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode執行
} else {
//解析,把名稱返回,這個$parameters看了許久源碼還是看不懂,應該是和參數相關,不過不影響我們的分析
list($name, $parameters) = $this->parsePipeString($pipe);
//從容器中解析出中間件實例并且執行handle方法
return call_user_func_array([$this->container->make($name), $this->method],
//$passable就是請求實例,而$stack就是傳遞的閉包
array_merge([$passable, $stack], $parameters)
);
}
再看一張圖片:
一次迭代傳入上一次的閉包和需要執行的中間件,由于反轉了數組,基于棧先進后出的特性,所以中間件3
第一個被包裝,中間件1就在最外層了。要記得,arrary_reduc
他不執行中間件代碼,而是包裝中間件。
看到這里應該明白了,array_reduce
最后會返回func3
,那么call_user_func(func3,$this->passable)
實際就是:
return call_user_func($middleware[0]->handle, $this->passable, func2);
而我們的中間件中的handle
代碼是:
public function handle($request, Closure $next) {
return $next($request);
}
這里就相當于return func2($request)
,這里的$request
就是經過上一個中間件處理過的。所以正果中間件的過程就完了,理解起來會有點繞,只要記得最后是由最外面的call_user_func
來執行中間件代碼的.
創建中間鍵
首先,通過Artisan命令建立一個中間件。
php artisan make:middleware [中間件名稱]
通過 Artisan 命令 make:middleware:
php artisan make:middleware CheckAge
這個命令會在 app/Http/Middleware
目錄下創建一個新的中間件類CheckAge
,在這個中間件中,我們只允許提供的 age
大于 200 的請求訪問路由,否則,我們將用戶重定向到 home
:
<?php
namespace App\Http\Middleware;
use Closure;
class CheckAge{
/**
* 返回請求 [過濾器]
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next) {
if ($request->input('age') <= 200) {
return redirect('home');
}
return $next($request);
}
}
正如你所看到的,如果 age <= 200,中間件會返回一個 HTTP 重定向到客戶端;否則,請求會被傳遞下去。將請求往下傳遞可以通過調用回調函數 $next
并傳入 $request
。
理解中間件的最好方式就是將中間件看做 HTTP 請求到達目標動作之前必須經過的“層”,每一層都會檢查請求并且可以完全拒絕它。
中間件之前/之后
一個中間件是請求前還是請求后執行取決于中間件本身。比如,以下中間件會在請求處理前執行一些任務:
<?php
namespace App\Http\Middleware;
use Closure;
class BeforeMiddleware
{
public function handle($request, Closure $next)
{
// 執行動作
return $next($request);
}
}
而下面這個中間件則會在請求處理后執行其任務:
<?php
namespace App\Http\Middleware;
use Closure;
class AfterMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);
// 執行動作
return $response;
}
}
注冊中間件
全局中間件
如果你想要中間件在每一個 HTTP 請求期間被執行,只需要將相應的中間件類設置到 app/Http/Kernel.php
的數組屬性 $middleware
中即可。
protected $middleware = [
//這是自帶的例子
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
//這是我注冊的中間件
\App\Http\Middleware\TestMiddleware::class,
];
分配中間件到路由
如果你想要分配中間件到指定路由,首先應該在 app/Http/Kernel.php
文件中分配給該中間件一個key,默認情況下,該類的 $routeMiddleware
屬性包含了 Laravel 自帶的中間件,要添加你自己的中間件,只需要將其追加到后面并為其分配一個key
,例如:
// 在 App\Http\Kernel 類中...
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
中間件在 HTTP Kernel 中被定義后,可以使用 middleware 方法將其分配到路由:
Route::get('admin/profile', function () {
//
})->middleware('auth');
使用數組分配多個中間件到路由:
Route::get('/', function () {
//
})->middleware('first', 'second');
分配中間件的時候還可以傳遞完整的類名:
use App\Http\Middleware\CheckAge;
Route::get('admin/profile', function () {
//
})->middleware(CheckAge::class);
中間件組
有時候你可能想要通過指定一個鍵名的方式將相關中間件分到同一個組里面,從而更方便將其分配到路由中,這可以通過使用 HTTP Kernel
的 $middlewareGroups
屬性實現。
Laravel 自帶了開箱即用的 web 和 api 兩個中間件組以分別包含可以應用到 Web UI
和 API 路由
的通用中間件:
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'auth:api',
],
];
中間件組可以被分配給路由和控制器動作,使用和單個中間件分配同樣的語法。再次申明,中間件組的目的只是讓一次分配給路由多個中間件的實現更加方便:
Route::get('/', function () {
//
})->middleware('web');
Route::group(['middleware' => ['web']], function () {
//
});
注:默認情況下,
RouteServiceProvider
自動將中間件組 web 應用到routes/web.php
文件。
中間件參數
中間件還可以接收額外的自定義參數,例如,如果應用需要在執行給定動作之前驗證認證用戶是否擁有指定的角色,可以創建一個 CheckRole
來接收角色名作為額外參數。
額外的中間件參數會在 $next
參數之后傳入中間件:
<?php
namespace App\Http\Middleware;
use Closure;
class CheckRole
{
/**
* 運行請求過濾器
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $role
* @return mixed
* translator http://laravelacademy.org
*/
public function handle($request, Closure $next, $role)
{
if (! $request->user()->hasRole($role)) {
// Redirect...
}
return $next($request);
}
}
中間件參數可以在定義路由時通過 :
分隔中間件名和參數名來指定,多個中間件參數可以通過,
分隔:
Route::put('post/{id}', function ($id) {
//
})->middleware('role:editor');
中間件代碼分析
中間件在請求階段會調用自己的handle()
方法
同時中間件也可以在響應階段使用,這時,會掉用它的terminate()
方法。
所以,當需要在響應發出后使用中間件只需要重寫terminate()
方法即可。
<?php
namespace App\Http\Middleware;
use Closure;
class TestMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
public function terminate($request, $response)
{
//這里是響應后調用的方法
}
}
handle()方法
handle()
方法有兩個參數
$request
--->請求信息,里面包含了輸入,URL,上傳文件等等信息。
$next
--->閉包函數。將接下來需要執行的邏輯裝載到了其中。
返回值:
通過上文對參數的描述可以了解到:
當我們在中間件中return $next($request)
;時,相當與把請求傳入接下來的邏輯中。
同時,中間件也可以返回重定向,不運行之前的邏輯。
例如,希望將頁面重定向到'/welcome'
的頁面return redirect('welcome')
。
注意,這里是重定向到"/welcome"這個地址的
route
而不是"welcome"這個頁面(view)。
terminate()方法
參數
$request
--->請求信息,里面包含了輸入,URL,上傳文件等等信息。
$response
-->響應消息,包含了邏輯處理完成后傳出到的響應消息。
因為terminate()
方法只是在響應后進行一些處理所以沒有返回值。
終止中間件
有時候中間件可能需要在 HTTP 響應
發送到瀏覽器之后做一些工作。比如,Laravel 內置的session
中間件會在響應發送到瀏覽器之后將 Session 數據寫到存儲器中,為了實現這個功能,需要定義一個終止中間件并添加 terminate
方法到這個中間件:
<?php
namespace Illuminate\Session\Middleware;
use Closure;
class StartSession
{
public function handle($request, Closure $next)
{
return $next($request);
}
public function terminate($request, $response)
{
// 存儲session數據...
}
}
terminate
方法將會接收請求和響應作為參數。定義了一個終止中間件之后,還需要將其加入到 HTTP kernel
的全局中間件列表中。
當調用中間件上的 terminate
方法時,Laravel 將會從服務容器中取出該中間件的新的實例,如果你想要在調用 handle
和 terminate
方法時使用同一個中間件實例,則需要使用容器的 singleton
方法將該中間件注冊到容器中。