ホームページ > バックエンド開発 > PHPチュートリアル > Laravelミドルウェアのコア解釈(ミドルウェア)

Laravelミドルウェアのコア解釈(ミドルウェア)

小云云
リリース: 2023-03-20 06:06:02
オリジナル
5175 人が閲覧しました


この記事では主に、Laravel のミドルウェア (Middleware) の核となる解釈を共有します。Laravel のミドルウェア (Middleware) は、アプリケーションに入る HTTP リクエスト オブジェクト (Request) をフィルターし、HTTP 応答オブジェクト (Response) を改善する役割を果たします。アプリケーションの機能を離れて、リクエストをレイヤーごとにフィルタリングし、複数のミドルウェアを適用することで応答を徐々に改善できます。これにより、プログラムの分離が実現されます。ミドルウェアがない場合、これらの手順をコントローラー内で完了する必要があり、これによりコントローラーが肥大化することは間違いありません。

簡単な例を挙げると、電子商取引プラットフォームでは、ユーザーはプラットフォーム上で買い物をする通常のユーザーか、ストアを開いた後の販売者ユーザーのいずれかになります。これら 2 つのタイプのユーザーのユーザー システムは多くの場合同じです。販売者ユーザーのみがアクセスできるコントローラーでは、販売者ユーザーの ID 認証を完了するために 2 つのミドルウェアを適用するだけで済みます:

class MerchantController extends Controller{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('mechatnt_auth');
    }
}
ログイン後にコピー

auth でユニバーサル ユーザー認証を実行しました。成功後、HTTP リクエストは、merchant_auth ミドルウェアに送信され、両方のミドルウェアが渡された後、目的のコントローラー メソッドに入ることができます。ミドルウェアを使用すると、これらの認証コードを対応するミドルウェアに抽出でき、必要に応じて複数のミドルウェアを自由に組み合わせてHTTPリクエストをフィルタリングできます。

もう 1 つの例は、Laravel がすべてのルーティング アプリケーションに自動的に提供する VerifyCsrfToken ミドルウェアです。HTTP リクエストがアプリケーションに入り、VerifyCsrfToken ミドルウェアを通過すると、トークンが検証されます。クロスサイト リクエスト フォージェリを防止します。HTTP レスポンスは、アプリケーションを終了する前に適切な Cookie をレスポンスに追加します。 (laravel5.5以降、CSRFミドルウェアはWebルーティングのみに自動適用されるようになりました) VerifyCsrfToken中间件,在HTTP Requst进入应用走过VerifyCsrfToken中间件时会验证Token防止跨站请求伪造,在Http Response 离开应用前会给响应添加合适的Cookie。(laravel5.5开始CSRF中间件只自动应用到web路由上)

上面例子中过滤请求的叫前置中间件,完善响应的叫做后置中间件。用一张图可以标示整个流程:
Laravelミドルウェアのコア解釈(ミドルウェア)

上面概述了下中间件在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里绑定了IlluminateContractsHttpKernel接口的实现类AppHttpKernel所以$kernel实际上是AppHttpKernel类的对象。
解析出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)

上記の例では、リクエストをフィルタリングするものをプレミドルウェア、レスポンスを改善するものをポストミドルウェアと呼びます。画像を使用してプロセス全体をマークできます:

Laravelミドルウェアのコア解釈(ミドルウェア)"/上記は、laravelにおけるミドルウェアの役割と、どのような種類のコードをコントローラーからミドルウェアに移動する必要があるかを概説しています。独自のlaravelミドルウェアを定義して使用する方法については、こちらをご覧ください。 公式ドキュメントを参照してください。 ここでは主にLaravelでミドルウェアを実装する方法を見ていきます。ミドルウェアの設計にはデコレータと呼ばれる設計パターンが適用されます。 Laravel は Application をインスタンス化した後、サービス コンテナから Http Kernel オブジェクトを解析します。クラス名からも、Http Kernel が Laravel の HTTP リクエストと応答を担当するコアであることがわかります。

/**
 * 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());
}
ログイン後にコピー

index.php を見ると、HTTP カーネルが bootstrap/app.php</code にあるためサービス コンテナから解析されていることがわかります。 > <code>IlluminateContractsHttpKernel インターフェースの実装クラス AppHttpKernel はそれにバインドされているため、$kernel は実際には AppHttpKernel クラスのオブジェクトです。

HTTP カーネルを解析した後、Laravel はアプリケーションに入るリクエスト オブジェクトを HTTP カーネルのハンドル メソッドに渡します。ハンドル メソッドは、アプリケーションに流入するリクエスト オブジェクトを処理し、応答オブジェクトを返します。

           
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(&#39;request&#39;, $request);        return $this->router->dispatch($request);
    };
}
ログイン後にコピー

ミドルウェア フィルタリング アプリケーションのプロセスは $this->sendRequestThroughRouter($request) で発生します: 🎜🎜🎜🎜 🎜🎜 🎜🎜🎜rrreえー🎜 最初このメソッドの半分はアプリケーションを初期化します。この部分については、サービス プロバイダーについて説明した前の記事で詳しく説明しています。 LaravelはPipelineオブジェクトを介してリクエストオブジェクトを送信し、PipelineではHTTPカーネルで定義されたミドルウェアの前処理やダイレクトクロージャ処理を経て、リクエストオブジェクトがコントローラのアクションに到達し、レスポンスオブジェクトを取得します。 🎜🎜Pipeline の次のメソッドを見てください: 🎜
mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )
array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。
callback ( mixed $carry , mixed $item )carry携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial。item 携带了本次迭代的值。
ログイン後にコピー
🎜 上記の関数はわかりにくいように見えますが、まず array_reduce のコールバック関数パラメーターの説明を見てみましょう: 🎜🎜
$destination = function ($request) {    $this->app->instance(&#39;request&#39;, $request);    return $this->router->dispatch($request);
};

$firstSlice = function ($passable) use ($destination) {    return call_user_func($destination, $passable);
};
ログイン後にコピー
🎜🎜getInitialSlice メソッド、その戻り値。 callbakc 関数に渡される $carray パラメーター。この値は getInitialSlice と Http Kernel のdispatchToRouter の 2 つのメソッドをマージしたものです。$firstSlice の値は次のようになります。 array_reduce:🎜のコールバック時

//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)) {                //解析中间件名称和参数 (&#39;throttle:60,1&#39;)                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(&#39;middleware.disable&#39;) &&                            $this->container->make(&#39;middleware.disable&#39;) === 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ミドルウェアのコア解釈(ミドルウェア)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート