さて、ここからが本題なのですが、Middleware の処理をソースコードから追って行く前に、まず簡単に「Middleware ってなんだっけ?」から復習がてらお話しようと思います。
Laravel の Middleware は、リクエストがアプリケーションに届いた時に、リクエストの前後で行われる一連の処理を指定する機能です。
Laravel で提供されている Middleware は以下の 3 種類で、
- システム全体で使用する Middleware=グローバル Middleware
- 特定のルートに対して適用する Middleware=ルート Middleware
- コントローラクラスのコンストラクタで指定する Middleware=コンストラクタ内 Middleware
です。
では、「その実態がどうなっているか?」というと app/Http/Kernel.php に定義されています。
そして、先ほど挙げた middleware(‘auth’)の際に実行されるのは、\App\Http\Middleware\Authenticate::class となります。
app/Http/Kernel.php
/**
* The application's middleware aliases.
*
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
Middleware が呼ばれるまでの道筋
- index.php
- vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
- vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
- 各 Middleware
という流れになります。
先ほど確認した app/Http/Kernel.php の Middleware が呼ばれるタイミングはどこかというと、3 の Pipline.php が各 Middleware を呼び出している実体です。
では 1~4 を順に見て行きます。
index.php
Laravel のメイン処理は framework/public/index.php のごくわずかなコードです。
(この中を深く掘り下げていくとカオスですが)
framework/public/index.php
<?php
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Check If The Application Is Under Maintenance
|--------------------------------------------------------------------------
|
| If the application is in maintenance / demo mode via the "down" command
| we will load this file so that any pre-rendered content can be shown
| instead of starting the framework, which could cause an exception.
|
*/
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| this application. We just need to utilize it! We'll simply require it
| into the script here so we don't need to manually load our classes.
|
*/
require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request using
| the application's HTTP kernel. Then, we will send the response back
| to this client's browser, allowing them to enjoy our application.
|
*/
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Kernel::class);
$response = $kernel->handle(
$request = Request::capture()
)->send();
$kernel->terminate($request, $response);
で、中心のさらに中心である、Kernel について見て行きます。
Kernel.php
$kernel という変数名からも読み取れるように、これが Laravel の心臓です。
さて、このコードの表面だけ見ればものすごく違和感があります。
use Illuminate\Contracts\Http\Kernel;
//中略
$app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Kernel::class);
$response = $kernel->handle(
$request = Request::capture()
)->send();
$kernel->terminate($request, $response);
なぜなら、$kernel を make する際に使っているのは Illuminate\Contracts\Http\Kernel ですが、実際に中を見てみるとこいつは interface で実体を持っていません。
vendor/laravel/framework/src/Illuminate/Contracts/Http/Kernel.php
<?php
namespace Illuminate\Contracts\Http;
interface Kernel
{
//中略
}
では「実体はどれか?」という話になるのですが、その実体は$kernel = $app->make(Kernel::class);の一つ前の処理、$app = require_once DIR.’/../bootstrap/app.php’;をみるとわかります。
その中に、以下のコードを見つけることができます。
bootstrap/app.php
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
つまり、bootstrap/app.php で Illuminate\Contracts\Http\Kernel::class の実体として App\Http\Kernel::class を登録しているため、$kernel = $app->make(Kernel::class);で$kernel が持つインスタンスは App\Http\Kernel になるというわけです。
では、App\Http\Kernel の中を見てみましょう。
App\Http\Kernel
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
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' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
おや…?
Laravel の学習書などでよくみるクラスに行き着きましたね?
そうです。ここが Middleware を登録する場所です。
しかし、index.php によると$kernel は handle というメソッドを持つはずですがこのクラスには見当たりません。パニックパニック。
落ち着いてクラス宣言をみてください。
App\Http\Kernel
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel{
//中略
}
どうやらスーパークラスが存在しているようです。
その実体は Illuminate\Foundation\Http\Kernel
このクラスの中を探すと…
Illuminate\Foundation\Http\Kernel
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
$this->requestStartedAt = Carbon::now();
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Throwable $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new RequestHandled($request, $response)
);
return $response;
}
はい、見つかりましたね。
ここから受け取ったリクエストを捌く全ての処理が始まります。
そしてこのコードを追っていくとどこかしらで Middleware の呼び出しが始まる箇所があるはずです。
本当はもっと丁寧に説明したいのですが、正直 Kernel のコードは膨大なので、Middleware に行き着く道筋だけ説明することにします。
結論としては、$response = $this->sendRequestThroughRouter($request);で Middleware の呼び出しが始まります。
App\Http\Kernel
/**
* 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());
}
new Pipeline($this->app)から始まるメソッドチェーンがそれです。
Pipeline.php
さて、、、つよつよ Laraveler 達から不評の Pipeline.php です。
/**
* Set the object being sent through the pipeline.
*
* @param mixed $passable
* @return $this
*/
public function send($passable)
{
$this->passable = $passable;
return $this;
}
/**
* Set the array of pipes.
*
* @param array|mixed $pipes
* @return $this
*/
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
/**
* Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
if (is_callable($pipe)) {
// If the pipe is a callable, then we will call it directly, but otherwise we
// will resolve the pipes out of the dependency container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
[$name, $parameters] = $this->parsePipeString($pipe);
// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}
$carry = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $this->handleCarry($carry);
} catch (Throwable $e) {
return $this->handleException($passable, $e);
}
};
};
}
順番に見ていきましょう。
まず、send ですが、これは passable に request をセットしています。
次に through ですが、これは pipes に middleware をセットしています。
そして、最後の then で middleware の処理が始まっていくのですが、これがめちゃくちゃわかりにくい処理になってます。
/**
* Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
この処理を順に理解するために、まずは PHP の array_reduce の動きについてつかんでおきたいと思います。
例えば以下のようなコードがあったとします。
$array = [1, 2, 3, 4, 5];
$sum = array_reduce($array, function($carry, $item) {
echo $carry . <br>
echo $item . <br>
return $carry + $item;
}, 0);
echo $sum;
この時の出力結果は以下のようなイメージになります。
0 -> array_reduceの第3引数
1 -> array[0]
1 -> 前回の処理結果
2 -> array[1]
3 -> 前回の処理結果
3 -> array[2]
4 -> 前回の処理結果
6 -> array[3]
5 -> 前回の処理結果
10 -> array[4]
15 -> 最終結果
つまり、carry は前のクロージャーで実行された結果を返し、$item は array の配列を順にもらっているものです。
このことを頭に入れつつ、元のコードに戻りたいと思います。
Pipeline の then
ここで少し整理をしておきます。
array_reduce の第 1 引数に来るのは middleware で、Kernel.php に定義したものが入ってきます。
全ては書かないのですが、例えば以下の順に middleware が入ってきます。
- \App\Http\Middleware\TrustProxies::class
- \Illuminate\Http\Middleware\HandleCors::class
- \App\Http\Middleware\PreventRequestsDuringMaintenance::class
で、一旦 carry というクロージャーは置いておいて、第 3 引数に来るのはこれまた別の実行したい処理です。
(本質ではないので、ここでは仮に destination とラベリングします)
/**
* Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
);
return $pipeline($this->passable);
}
なので、先ほどの例のイメージでいくと、carry に渡っていくものは、
- destination
- \App\Http\Middleware\TrustProxies::class
- \Illuminate\Http\Middleware\HandleCors::class
- \App\Http\Middleware\PreventRequestsDuringMaintenance::class
なのですが、今回は array_reverse がかかっているので、順番としては以下の通りになります。
- destination
- \App\Http\Middleware\PreventRequestsDuringMaintenance::class
- \Illuminate\Http\Middleware\HandleCors::class
- \App\Http\Middleware\TrustProxies::class
ここで、「あれ?middleware って配列に書いた順に処理されていくんじゃ…?しかも destination が最初に処理されたら middleware の意味がないような?」と思うでしょう。
そう、ここが Laravel の middleware が読み手を混乱させる要因なのです。
この疑問を解消するために、carry()の中を見て行きます。
/**
* The method to call on each pipe.
*
* @var string
*/
protected $method = 'handle';
/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
if (is_callable($pipe)) {
// If the pipe is a callable, then we will call it directly, but otherwise we
// will resolve the pipes out of the dependency container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
[$name, $parameters] = $this->parsePipeString($pipe);
// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}
$carry = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $this->handleCarry($carry);
} catch (Throwable $e) {
return $this->handleException($passable, $e);
}
};
};
}
ここでみなさんの脳内メモリを補助します。
先ほどの array_reverse の結果を踏まえると、
- passable = request
- stack = destination
- pipe = \App\Http\Middleware\PreventRequestsDuringMaintenance::class
ということになるので、ここまでみた感じだと「やっぱり destination が最初に動くじゃないか!」となってしまいます。
では、なぜそうならないかを順を追って説明します。
$carry = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
このように pipe(クラス)に this->method のメソッドが存在するかをチェックし、存在していればそのメソッドを呼んでいます。
この method という変数は this なので、”handle”になっています。
みなさんご存知のように、「Laravel の middleware を作ったときに呼ばれるメソッドは handle」なのですが、その理由はここからきています。
では、試しに Route::middleware(‘auth’)でお馴染みの vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php の Middleware についてみてみます。
vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string[] ...$guards
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
わかりましたか?
各 middleware は引数で request と next を受け取り、次の middleware 呼び出しを行います。
つまり再起的に middleware が呼び出されているわけです。
そして、渡ってきた middleware の根に到達して初めて結果を戻し、stacktrace から抜けて行きます。
つまり、
- destination
- \App\Http\Middleware\PreventRequestsDuringMaintenance::class
- \Illuminate\Http\Middleware\HandleCors::class
- \App\Http\Middleware\TrustProxies::class
の順で呼び出しは起こるのですが、処理結果そのものは
- \App\Http\Middleware\TrustProxies::class
- \Illuminate\Http\Middleware\HandleCors::class
- \App\Http\Middleware\PreventRequestsDuringMaintenance::class
- destination
の順に返って行くのです。
以上が、Laravel が Middleware を呼び出している流れでした。
おわりに
デバッガーを使って処理を追いかけていると、一度のリクエストの間に Pipline の then は複数回走ります。
これは冒頭で Middleware には種類があるという話をしたかと思うのですが、グローバル Middleware->ルート Middleware..というようにそれぞれのタイミングで Middleware 達が実行されているからですね。
では、その順番がどう制御されているのか?ということについては詳しく調べていないのですが、おそらく then(Closure $destination)で渡ってくる$destination によって制御されているのでは?と考えています。
(つよつよ Laraveler、知っていたら教えてください)
コメント