Laravel中介軟體(Middleware)的核心解讀

小云云
發布: 2023-03-20 06:06:02
原創
5151 人瀏覽過


本文主要和大家分享Laravel中間件(Middleware)的核心解讀,中間件(Middleware)在Laravel中起著過濾進入應用的HTTP請求對象(Request)和完善離開應用的HTTP回應物件(Reponse)的作用, 而且可以透過應用多個中間件來層層過濾請求、逐步完善對應。這樣就做到了程式的解耦,如果沒有中間件那麼我們必須在控制器中來完成這些步驟,這無疑會造成控制器的臃腫。

舉一個簡單的例子,在一個電商平台上用戶既可以是一個普通用戶在平台上購物也可以在開店後是一個賣家用戶,這兩種用戶的用戶體系往往都是一套,那麼在只有賣家使用者才能存取的控制器裡我們只需要應用兩個中間件來完成賣家使用者的身分認證:

     ##      #

class MerchantController extends Controller{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('mechatnt_auth');
    }
}
登入後複製

在auth中間件裡做了通用的用戶認證,成功後HTTP Request會走到merchant_auth中間件裡進行商家用戶資訊的認證,兩個中間件都通過後HTTP Request就能進入到要去的控制器方法中了。利用中間件,我們就能把這些認證代碼抽離到對應的中間件中了,而且可以根據需求自由組合多個中間件來對HTTP Request進行過濾。

再例如Laravel自動給所有路由應用的

VerifyCsrfToken中間件,在HTTP Requst進入應用程式走過VerifyCsrfToken中間件時會驗證Token防止跨站請求偽造,在Http Response 離開應用前會為響應添加合適的Cookie。 (laravel5.5開始CSRF中間件只會自動應用在web路由上)

上面範例中過濾請求的叫前置中間件,完善回應的叫做後置中間件。用一張圖可以標示整個流程:


Laravel中介軟體(Middleware)的核心解讀

上面概述了下中間件在laravel中的角色,以及什麼類型的程式碼應該從控制器挪到中間件裡,至於如何定義並使用自己的laravel 中間件請參考

官方文件

下面我們主要來看Laravel中是怎麼實現中間件的,中間件的設計應用了一種叫做裝飾器的設計模式。

Laravel實例化Application後,會從服務容器解析出Http Kernel對象,透過類別的名字也能看出來Http Kernel就是Laravel裡負責HTTP請求與回應的核心。

           

/**
 * @var \App\Http\Kernel $kernel
 */$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);
登入後複製

index.php裡面可以看到,從服務容器裡解析出Http Kernel,因為在bootstrap/app.php裡綁定了Illuminate\Contracts\Http\Kernel介面的實作類別App\Http\Kernel所以$kernel其實是App\Http\Kernel類別的物件。 解析出Http Kernel後Laravel將進入應用的請求物件傳遞給Http Kernel的handle方法,在handle方法負責處理流入應用程式的請求物件並傳回回應物件。

           

/**
 * Handle an incoming HTTP request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */public function handle($request){    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {        $this->reportException($e);

        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }    $this->app['events']->dispatch(        new Events\RequestHandled($request, $response)
    );    return $response;
}
登入後複製

中間件過濾應用的過程就發生在

$this->sendRequestThroughRouter( $request)裡:

           

/**
 * Send the given request through the middleware / router.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */protected function sendRequestThroughRouter($request){    $this->app->instance('request', $request);

    Facade::clearResolvedInstance('request');    $this->bootstrap();    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}
登入後複製

這個方法的前半部是對Application進行了初始化,在上一篇講解服務提供器的文章裡有對這一部分的詳細講解。 Laravel透過Pipeline(管道)物件來傳輸請求對象,在Pipeline中請求對象依序透過Http Kernel裡定義的中間件的前置操作到達控制器的某個action或直接閉包處理得到回應對象。

看下Pipeline裡這幾個方法:

           
public function send($passable){    $this->passable = $passable;    return $this;
}public function through($pipes){    $this->pipes = is_array($pipes) ? $pipes : func_get_args();    return $this;
}public function then(Closure $destination){
    $firstSlice = $this->getInitialSlice($destination);    
    //pipes 就是要通过的中间件
    $pipes = array_reverse($this->pipes);    //$this->passable就是Request对象    return call_user_func(
        array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
    );
}protected function getInitialSlice(Closure $destination){    return function ($passable) use ($destination) {        return call_user_func($destination, $passable);
    };
}//Http Kernel的dispatchToRouter是Piple管道的终点或者叫目的地protected function dispatchToRouter(){    return function ($request) {        $this->app->instance('request', $request);        return $this->router->dispatch($request);
    };
}
登入後複製

上面的函數看起來比較暈,我們先來看下array_reduce裡對它的callback函數參數的解釋:

mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )
array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。
callback ( mixed $carry , mixed $item )carry携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial。item 携带了本次迭代的值。
登入後複製
getInitialSlice方法,他的回傳值是作為傳遞給callbakc函數的$carray參數的初始值,這個值現在是一個閉包,我把getInitialSlice和Http Kernel的dispatchToRouter這兩個方法合併一下,現在$firstSlice的值為:

           

$destination = function ($request) {    $this->app->instance('request', $request);    return $this->router->dispatch($request);
};

$firstSlice = function ($passable) use ($destination) {    return call_user_func($destination, $passable);
};
登入後複製

接下來我們看看array_reduce的callback:

//Pipeline protected function getSlice(){    return function ($stack, $pipe) {        return function ($passable) use ($stack, $pipe) {            try {
                $slice = parent::getSlice();                return call_user_func($slice($stack, $pipe), $passable);
            } catch (Exception $e) {                return $this->handleException($passable, $e);
            } catch (Throwable $e) {                return $this->handleException($passable, new FatalThrowableError($e));
            }
        };
    };
}//Pipleline的父类BasePipeline的getSlice方法protected function getSlice(){    return function ($stack, $pipe) {        return function ($passable) use ($stack, $pipe) {            if ($pipe instanceof Closure) {                return call_user_func($pipe, $passable, $stack);
            } elseif (! is_object($pipe)) {                //解析中间件名称和参数 ('throttle:60,1')                list($name, $parameters) = $this->parsePipeString($pipe);
                $pipe = $this->container->make($name);
                $parameters = array_merge([$passable, $stack], $parameters);
            } else{
                $parameters = [$passable, $stack];
            }            //$this->method = handle            return call_user_func_array([$pipe, $this->method], $parameters);
        };
    };
}
登入後複製

注:在Laravel5.5版本里 getSlice这个方法的名称换成了carray, 两者在逻辑上没有区别,所以依然可以参照着5.5版本里中间件的代码来看本文。

getSlice会返回一个闭包函数, $stack在第一次调用getSlice时它的值是$firstSlice, 之后的调用中就它的值就是这里返回的值个闭包了:

$stack = function ($passable) use ($stack, $pipe) {            try {
                $slice = parent::getSlice();                return call_user_func($slice($stack, $pipe), $passable);
            } catch (Exception $e) {                return $this->handleException($passable, $e);
            } catch (Throwable $e) {                return $this->handleException($passable, new FatalThrowableError($e));
            }
 };
登入後複製

getSlice返回的闭包里又会去调用父类的getSlice方法,他返回的也是一个闭包,在闭包会里解析出中间件对象、中间件参数(无则为空数组), 然后把$passable(请求对象), $stack和中间件参数作为中间件handle方法的参数进行调用。

上面封装的有点复杂,我们简化一下,其实getSlice的返回值就是:

$stack = function ($passable) use ($stack, $pipe) {                //解析中间件和中间件参数,中间件参数用$parameter代表,无参数时为空数组
               $parameters = array_merge([$passable, $stack], $parameters)               return $pipe->handle($parameters)
};
登入後複製

array_reduce每次调用callback返回的闭包都会作为参数$stack传递给下一次对callback的调用,array_reduce执行完成后就会返回一个嵌套了多层闭包的闭包,每层闭包用到的外部变量$stack都是上一次之前执行reduce返回的闭包,相当于把中间件通过闭包层层包裹包成了一个洋葱。

在then方法里,等到array_reduce执行完返回最终结果后就会对这个洋葱闭包进行调用:

return call_user_func( array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable);
登入後複製

这样就能依次执行中间件handle方法,在handle方法里又会去再次调用之前说的reduce包装的洋葱闭包剩余的部分,这样一层层的把洋葱剥开直到最后。通过这种方式让请求对象依次流过了要通过的中间件,达到目的地Http Kernel 的dispatchToRouter方法。

通过剥洋葱的过程我们就能知道为什么在array_reduce之前要先对middleware数组进行反转, 因为包装是一个反向的过程, 数组$pipes中的第一个中间件会作为第一次reduce执行的结果被包装在洋葱闭包的最内层,所以只有反转后才能保证初始定义的中间件数组中第一个中间件的handle方法会被最先调用。

上面说了Pipeline传送请求对象的目的地是Http Kernel 的dispatchToRouter方法,其实到远没有到达最终的目的地,现在请求对象了只是刚通过了\App\Http\Kernel类里$middleware属性里罗列出的几个中间件:

protected $middleware = [    \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,    \App\Http\Middleware\TrimStrings::class,    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,    \App\Http\Middleware\TrustProxies::class,
];
登入後複製

当请求对象进入dispatchToRouter方法后,请求对象在被Router dispatch派发给路由时会进行收集路由上应用的中间件和控制器里应用的中间件。

public function dispatch(Request $request){    $this->currentRequest = $request;

    $response = $this->dispatchToRoute($request);    return $this->prepareResponse($request, $response);
}public function dispatchToRoute(Request $request){    return $this->runRoute($request, $this->findRoute($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()
                        );
                    });
}
登入後複製

收集完路由和控制器里应用的中间件后,依赖时利用Pipeline对象来传送请求对象通过收集上来的这些中间件然后到达最终的目的地,在这里会执行路由对应的控制器方法生成响应对象,然后响应对象会依次来通过上面应用的所有中间件的后置操作,最终离开应用被发送给客户端。

限于篇幅和为了文章的可读性,收集路由和控制器中间件然后执行路由对应的处理方法的过程我就不在这里详述了,感兴趣的同学可以自己去看Router的源码,本文的目的还是主要为了梳理laravel是如何设计中间件的以及如何执行它们的,希望能对感兴趣的朋友有帮助。

相关推荐:

Laravel中间件实现原理详解

laravel中间件中的Closure $next是什么意思

关于laravel中间件

以上是Laravel中介軟體(Middleware)的核心解讀的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板