Lumen框架經過設置中間件能夠實現業務與非業務邏輯的隔離。在構建api服務的過程當中中間件能夠作不少工做,例如:php
Lumen中間件分爲兩種,都須要在/bootstrap/app.php中進行設置node
$app->middleware([
App\Http\Middleware\ExampleMiddleware::class
]);
複製代碼
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
'log' => App\Http\Middleware\Logger::class
]);
複製代碼
路由中間件須要在路由中(一般在./routes/web.php)進行設置web
$app->group(['middleware' => 'Example'], function () use ($app) {
$app->get('age',['uses'=>'ExampleController@age']);
});
複製代碼
Lumen中間件採用的是裝飾器模式(固然也能夠說成是責任鏈、管道模式)。和node.js框架koa2的洋蔥模型效果同樣。實現的都是對http請求的層層處理,最終返回給用戶響應信息。bootstrap
Lumen框架實現中間件使用到了管道和Closure(匿名函數類),源碼不太好理解,筆者準備了一個小demo幫助你們理解:api
<?php
interface Middleware
{
public function handle(Closure $next);
}
class LogMiddleware implements Middleware
{
public function handle(Closure $next)
{
echo "記錄請求日誌" . '<br/>';
$next();
echo "記錄響應日誌" . '<br/>';
}
}
class ApiMiddleware implements Middleware
{
public function handle(Closure $next)
{
echo "Apimiddleware校驗token" . '<br/>';
$next();
}
}
class RateLimitMiddleware implements Middleware
{
public function handle(Closure $next)
{
echo "校驗流量限制" . '<br/>';
$next();
}
}
function carry($closures, $middleware)
{
return function () use ($closures, $middleware) {
return $middleware->handle($closures);
};
}
function then()
{
$middlewares = [new LogMiddleware(), new ApiMiddleware(), new RateLimitMiddleware()];
$prepare = function () {
echo '<b>用戶處理邏輯,返回響應結果</b>' . '<br/>';
};
$go = array_reduce(array_reverse($middlewares), 'carry', $prepare);
$go();
}
then();
複製代碼
例子中建立了三個Middleware,都實現了Middleware接口。其中handle爲處理邏輯。handle方法接收一個匿名函數類(Closure),handle方法有本身的處理邏輯,這些處理邏輯能夠在匿名函數類執行以前,也能夠在它以後。(這也是洋蔥模型的精要,請求處理通過中間件到邏輯處理函數,返回響應後還會再次通過它)。LogMiddleware在匿名函數類執行以前和以後分別打印了信息;ApiMiddleware和RateLimitMiddleware則分別在匿名函數執行以前打印了信息。數組
咱們直接來看then函數:首先$middleware數組保存了一組中間件;$prepare是一個匿名函數,這裏主要是爲了模擬用戶處理邏輯,對應咱們Lumen中的業務入口Controller中的相關方法,例如:bash
public function getList(Request $r){
...//這裏是業務邏輯
}
複製代碼
接下來的array_reduce是重點,php官方是這樣定義的:閉包
array_reduce() 將回調函數 callback 迭代地做用到 array 數組中的每個單元中,從而將數組簡化爲單一的值。app
array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed
複製代碼
參數框架
array 輸入的 array。
callback callback ( mixed $carry , mixed $item ) : mixed carry 攜帶上次迭代裏的值; 若是本次迭代是第一次,那麼這個值是 initial。
item 攜帶了本次迭代的值。
initial 若是指定了可選參數 initial,該參數將在處理開始前使用,或者當處理結束,數組爲空時的最後一個結果。
看完了官方的定義,咱們就比較好理解demo中的carry函數就是中間件的迭代函數了,$prepare會作爲carry函數首次迭代時的第一個參數;carry函數的返回值使用到了閉包:
return function () use ($closures, $middleware) {
return $middleware->handle($closures);
};
複製代碼
咱們知道函數內部的變量都是局部變量,除非使用global聲明。閉包的使用使得函數內部可使用外部變量。這裏咱們就把上一層中間件的實例和它用到的參數引用了進來。
存儲中間件的數組$middleware爲何要使用array_reverse()函數反轉呢?
由於array_reduce迭代以後,匿名函數所處的位置和數組原來的位置恰好是相反的。通過一次reverse以後,再次通過array_reduce的迭代,造成了一個符合咱們預期的執行鏈路。像洋蔥同樣,一層層的包裹。最後只須要執行then()函數,就能實現咱們上圖所表示的邏輯鏈路了,打印的結果爲:
記錄請求日誌
Apimiddleware校驗token
校驗流量限制
用戶處理邏輯,返回響應結果
記錄響應日誌
複製代碼
若是你們對上述中間件實現過程還不是太明白,建議回過頭來再讀一遍,將demo拷貝到本地運行調試。相信你們明白了這個例子之後,閱讀下面lumen中間件源碼的理解會有一種豁然開朗的感受。
框架的入口文件(./app/public/index.php)很是簡單:
$app = require __DIR__.'/../bootstrap/app.php';
$app->run();
複製代碼
run函數是在Trait中定義的:
trait RoutesRequests
{
protected $middleware = [];
protected $routeMiddleware = [];
......
public function run($request = null)
{
$response = $this->dispatch($request);
if ($response instanceof SymfonyResponse) {
$response->send();
} else {
echo (string) $response;
}
if (count($this->middleware) > 0) {
$this->callTerminableMiddleware($response);
}
}
......
}
複製代碼
框架中的中間件存儲在成員變量$middleware和$routeMiddleware中,咱們接着看dispatch函數:
trait RoutesRequests
{
......
public function dispatch($request = null)
{
//解析路由和請求方法
[$method, $pathInfo] = $this->parseIncomingRequest($request);
try {
//啓動服務註冊
$this->boot();
//經過向中間件分發請求處理獲得響應信息
return $this->sendThroughPipeline($this->middleware, function ($request) use ($method, $pathInfo) {
$this->instance(Request::class, $request);
if (isset($this->router->getRoutes()[$method.$pathInfo])) {
return $this->handleFoundRoute([true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]);
}
return $this->handleDispatcherResponse(
$this->createDispatcher()->dispatch($method, $pathInfo)
);
});
} catch (Throwable $e) {
return $this->prepareResponse($this->sendExceptionToHandler($e));
}
}
......
複製代碼
接下來的sendThroughPipeline實現了中間件處理:
trait RoutesRequests
{
......
protected function sendThroughPipeline(array $middleware, Closure $then)
{
if (count($middleware) > 0 && ! $this->shouldSkipMiddleware()) {
return (new Pipeline($this))
->send($this->make('request'))
->through($middleware)
->then($then);
}
return $then($this->make('request'));
}
......
}
複製代碼
咱們只看有邏輯中間件的狀況,先來看then函數(/vendor/illuminate/pipeline/Pipeline.php):
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
複製代碼
到這裏,讀者大概就明白了,實現原理和咱們上述demo基本是同樣的。只不過這裏的迭代函數更加的複雜一些,咱們來看一下:
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;
};
};
}
複製代碼
注意咱們第二個參數傳入的是:$this->carry();因此實際的迭代函數是第一個return後的匿名對象。這個匿名對象返回的匿名對象(第二個return)接收一個參數$passable,並使用閉包引用了外層$middleware的實例(匿名函數)。
$pipeline($this->passable)中的$this->passable是什麼呢?正是send方法傳入的參數(Request):
class Pipeline implements PipelineContract
{
......
public function send($passable)
{
$this->passable = $passable;
return $this;
}
......
}
複製代碼
那麼$this->carry()中的這個返回值$pipe($passable, $stack);也讓咱們聯想到了中間件中的handle方法了吧:
class ExampleMiddleware
{
public function handle($request, Closure $next)
{
......
return $next($request);
......
}
}
複製代碼
array_reduce中的第三個參數咱們也來簡單看一下吧:
protected function prepareDestination(Closure $destination)
{
return function ($passable) use ($destination) {
return $destination($passable);
};
}
複製代碼
$passable就是Request對象,$destination是傳入的參數,就是dispatch方法中返回值中的第二個參數,是匿名函數:
return $this->sendThroughPipeline($this->middleware, function ($request) use ($method, $pathInfo) {
$this->instance(Request::class, $request);
if (isset($this->router->getRoutes()[$method.$pathInfo])) {
return $this->handleFoundRoute([true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]);
}
return $this->handleDispatcherResponse(
$this->createDispatcher()->dispatch($method, $pathInfo)
);
});
複製代碼
這個匿名函數最終返回的結果是「服務容器」根據$method和$pathInfo到路由中解析出來具體的執行邏輯(哪一個Controller的哪一個方法)實例。也就是$destination($passable)對應的是:
class ExampleController extends Controller
{
public function getList(Request $r)
{
....
}
複製代碼
到這裏,相信讀者都已經明白了Lumen中間件的實現原理.
中間件是API框架必不可少的一個模塊,其目的都是爲了解耦非業務邏輯代碼和業務邏輯代碼,實現效果是相似於洋蔥同樣的請求處理模型;不一樣的語言有不一樣的特性,node.js框架Ko2是採用的回調函數的形式實現了中間件,只不過其封裝的更加優雅並容易使用;Go語言的特性提倡組合大於繼承,Gin框架利用組合方法的方式也實現了相似Ko2框架中間件的模式。本節講了Lumen框架如何實現中間件,基於php的特性,實現起來確實複雜了一些,相信讀完本文,讀者可以對API框架中間件有更深刻的認識!