
この動きを支えているのが、Illuminate\Pipeline\Pipelineクラスです。Laravelのミドルウェアは、単純な配列を順番に実行しているわけではありません。send()、through()、then()というメソッドを使い、複数のミドルウェアを入れ子になったクロージャへ変換してから、ひとつの処理の流れとして実行しています。
特に、Laravelのミドルウェアパイプラインの仕組みを理解するうえで重要なのが、array_reduce()とarray_reverse()によるクロージャの合成です。ここが見えるようになると、Before処理とAfter処理の順番、$next($request)を呼ばない場合の中断、そしてHTTP以外でPipelineを再利用する方法まで、ひとつの仕組みとして捉えられるようになります。
LaravelのミドルウェアはどこでPipelineに渡されるのか
まず、リクエストがアプリケーションへ入ってから、ミドルウェアが実行されるまでの全体像を確認してみましょう。
HTTPリクエストを処理するLaravelのHTTPカーネルでは、おおまかに次のような形でPipelineクラスが利用されます。
(new Pipeline($this->app))->send($request)->through(...)->then(...)
ここでは、ひとつのメソッドの中でいくつかの処理が連続して呼び出されていますが、それぞれの役割は分かれています。
send($request)で、パイプラインを通す対象のリクエストを設定するthrough(...)で、実行するミドルウェアの配列を設定するthen(...)で、すべてのミドルウェアを通過したあとの最終処理を指定する
この段階では、まだミドルウェアが1件ずつ実行されているわけではありません。Pipelineは、受け取ったミドルウェアの一覧と最終処理をもとに、処理を連結したひとつのコールバックを組み立てます。
send()は何を運ぶのか
send()に渡される値は、HTTPリクエストに限定されません。HTTPカーネルから呼び出される場合はリクエストオブジェクトですが、Pipelineクラスそのものは、任意のデータやオブジェクトを処理の流れに通すための仕組みとして設計されています。
HTTPの場合は次のようなイメージです。
- リクエストを受け取る
- 認証やセッションなどのミドルウェアを通す
- ルートやコントローラの処理へ進む
- レスポンスを返す
一方で、HTTP以外では、配列、クエリビルダ、独自のデータオブジェクトなどを対象にできます。この共通性があるため、Pipelineを単なるミドルウェア専用機能として見ると、クラスの役割を少し狭く捉えることになります。
through()は実行順序の材料を設定する
through()には、ミドルウェアのクラス名や処理を表すパイプの配列を渡します。LaravelのHTTP処理では、グローバルミドルウェアやルートに紐づくミドルウェアなどが、アプリケーションの設定に基づいてこの流れへ入ります。
Laravel 11では、従来の構成と比べて、ミドルウェアの設定をbootstrap/app.phpで組み立てる形が目立つようになりました。ただし、設定場所が変わったからといって、ミドルウェアが順番に処理される基本的な考え方まで変わったわけではありません。
ここで注意していただきたいのは、through()へ渡した配列が、そのまま単純なforeachで処理されるわけではないことです。Pipelineは、この配列を後述するクロージャの連鎖へ変換します。
then()がパイプライン全体をひとつの処理へ変換する
Pipelineの中心にあるのがthen()です。このメソッドには、ミドルウェアをすべて通過したあとに実行する宛先、つまりDestination Closureを渡します。
HTTPリクエストの場合、この宛先はルートの実行やコントローラ呼び出しへつながる処理です。ミドルウェアは最終的にこの宛先へ到達することを目指しますが、その途中で認証エラーやリダイレクトを返した場合は、宛先まで進みません。
then()の内部では、概念的に次のような処理が行われます。
array_reduce(array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination))
この1行は、Laravelのミドルウェアパイプラインの仕組みを理解するための重要な手がかりです。実際の実装には例外処理やパイプの種類に応じた分岐などもありますが、ミドルウェアをどのように連鎖させているかを見るには、この形が役立ちます。
この処理で登場する要素を分けて考えてみましょう。
| 要素 | 役割 |
|---|---|
array_reverse() | ミドルウェアの配列を、クロージャへ合成しやすい順番に並べ替える |
array_reduce() | 複数のミドルウェアを、ひとつのコールバックへ順に畳み込む |
carry() | 現在のミドルウェアと、すでに作られた次の処理を結びつける |
prepareDestination() | ミドルウェアをすべて通過したあとの最終処理を用意する |
then() | 合成された処理へ対象データを渡し、パイプラインを実行する |
ここでのポイントは、ミドルウェアが「次のミドルウェアを直接探して呼ぶ」のではなく、Pipelineによって「次の処理を呼び出すクロージャ」が順番に作られていることです。
Laravelのミドルウェアは、配列を上から実行する仕組みというより、次の処理を呼び出すクロージャを重ねていく仕組みとして見ると、実行順序が読みやすくなります。
array_reduce()とクロージャの玉ねぎ構造
Laravelのミドルウェア実行を説明するとき、よく「玉ねぎ構造」という表現が使われます。これは、ミドルウェアが中心にあるコントローラ処理を囲むように重なり、リクエストが内側へ進み、レスポンスが外側へ戻ってくる構造を指しています。
たとえば、ミドルウェアが3つあるとします。
- ミドルウェアA
- ミドルウェアB
- ミドルウェアC
- 最終処理
実行時の構造は、概念的には次のような入れ子になります。
- AがBを呼び出す
- BがCを呼び出す
- Cが最終処理を呼び出す
ただし、AがBのインスタンスを直接管理しているわけではありません。各ミドルウェアのhandle()へ渡される$nextが、次の処理を呼び出すクロージャになっています。
array_reduce()は何をしているのか
array_reduce()は、配列の各要素を順番に処理しながら、ひとつの値へまとめていくPHPの関数です。Pipelineでは、この「配列をひとつの値へ畳み込む」性質を使って、ミドルウェアの配列を最終的なコールバックへ変換します。
単純化すると、最初に最終処理を用意し、その最終処理を次のミドルウェアが包み、さらにその外側を別のミドルウェアが包む、という順に合成します。
たとえば、最終処理をD、ミドルウェアをA、B、Cとすると、最終的には次のような関係になります。
A(B(C(D)))
これは実際にこの文字列が生成されるという意味ではなく、処理の入れ子を表したものです。
ミドルウェアAのhandle()が呼ばれると、Aの処理が始まり、$next($request)によってBへ進みます。Bは同じようにCへ進み、Cが最終処理へリクエストを渡します。その後、最終処理の戻り値がC、B、Aの順に戻っていきます。
なぜarray_reverse()が必要なのか
ミドルウェアの実行順序を考えると、配列の先頭にあるミドルウェアが最初に実行され、最後のミドルウェアが最後に実行されることを期待します。
しかし、array_reduce()で最終処理を起点にクロージャを組み立てる場合、合成の都合上、ミドルウェアを逆順に扱う必要があります。そこでarray_reverse()を使い、配列の後ろから前へ、外側の処理を組み立てていきます。
たとえば、設定上の順番がA、B、Cなら、内部で合成する順序はC、B、Aです。
1. 最終処理Dを用意する
2. CがDを呼ぶクロージャを作る
3. BがCの処理を呼ぶクロージャを作る
4. AがBの処理を呼ぶクロージャを作る
5. Aから始まるひとつの処理として実行する
この組み立て方により、実行時の順番はA、B、C、Dになります。
ここは、ソースコードを読んでいて混乱しやすい部分です。配列を逆順にしているため、「ミドルウェアの実行順も逆になるのではないか」と考えやすいのですが、逆順なのはあくまでクロージャを合成する順番です。入口から見た実行順序まで逆転するわけではありません。
handle()の前後で処理が分かれる理由
Laravelのミドルウェアには、通常次のような形のhandle()メソッドがあります。
handle(Request $request, Closure $next)
このメソッドで重要なのは、$next($request)の前後に処理を書けることです。
$next($request)より前に書いた処理$next($request)そのもの$next($request)の戻り値を受け取ったあとの処理
この3つが、ミドルウェアの中で異なるタイミングに動きます。
Beforeミドルウェアの処理
$next($request)を呼び出す前の処理は、リクエストが次のミドルウェアやコントローラへ進む前に実行されます。
ここでは、たとえば次のような処理を置けます。
- リクエストに対する事前の判定
- ヘッダーや属性の確認
- 認証状態の確認
- リクエスト情報のログ記録
- 後続処理へ進めるかどうかの判断
この段階で条件を満たしていない場合、$next($request)を呼ばずにレスポンスを返せます。すると、後続のミドルウェアやコントローラへ処理は進みません。
Afterミドルウェアの処理
$next($request)の戻り値を受け取ったあとに書いた処理は、後続の処理から戻ってきたレスポンスに対して実行されます。
たとえば、次のような用途が考えられます。
- レスポンスヘッダーの追加
- レスポンス内容の確認
- 実行時間の記録
- ログの出力
- 共通的なレスポンス加工
もちろん、レスポンスの種類によっては安全に変更できない場合もあります。そのため、実際の処理ではレスポンスオブジェクトの型や状態を確認しながら扱う必要があります。
複数ミドルウェアの実行順序
ミドルウェアA、B、Cがこの順で登録されている場合、処理の流れは次のようになります。
1. AのBefore処理
2. BのBefore処理
3. CのBefore処理
4. コントローラなどの最終処理
5. CのAfter処理
6. BのAfter処理
7. AのAfter処理
つまり、リクエストは登録順に内側へ進み、レスポンスは逆順に外側へ戻ります。
| 実行段階 | 呼び出される処理 |
|---|---|
| 1 | Aの$next()前の処理 |
| 2 | Bの$next()前の処理 |
| 3 | Cの$next()前の処理 |
| 4 | コントローラや最終処理 |
| 5 | Cの$next()後の処理 |
| 6 | Bの$next()後の処理 |
| 7 | Aの$next()後の処理 |
この順番は、ミドルウェアのhandle()を上から下へ読むだけでは見えにくいものです。$next()が「次の処理へ進む呼び出し」であると同時に、「戻り値を受け取って自分の処理へ戻る地点」でもあることが、ポイントになります。
実行順序をログで追うと仕組みが見えやすい
ミドルウェアの順番に迷ったときは、まず処理の前後へログを置いてみましょう。これは、ソースコードを読むだけではなく、実際の実行結果から再現性を確認するための方法です。
たとえば、ミドルウェアAの前後にログを追加するとします。
- AのBefore
$next($request)- AのAfter
同じようにBとCにもログを置けば、実際にどの順序で処理されているかを確認できます。
ここで気をつけたいのは、ログの出力場所です。$next()より前に書いたログはリクエストが内側へ進むときに出力され、$next()より後のログは、内側の処理が戻ってきたあとに出力されます。
そのため、次のようなログ結果になります。
- A Before
- B Before
- C Before
- 最終処理
- C After
- B After
- A After
実際のアプリケーションでは、ルートミドルウェア、グローバルミドルウェア、認証ミドルウェアなどが加わるため、想定より多くのログが出ることがあります。特に、同じミドルウェアを複数の場所で登録している場合は、登録経路を一度整理してみてください。
Laravel 11のようにミドルウェア設定の書き方が変わった環境では、bootstrap/app.php側の設定と、ルート定義側の指定を別々に確認する必要があります。どこから追加されたミドルウェアなのかが分からないまま順序だけを追うと、問題の原因を見誤りやすくなります。
$next($request)を呼ばないと何が起きるのか
ミドルウェアの条件分岐でよく使われるのが、途中でレスポンスを返して処理を止める方法です。
たとえば、認証されていないユーザーをログイン画面へリダイレクトする場合、条件に合わなければリダイレクトレスポンスを返します。このとき、$next($request)は呼び出しません。
すると、次のミドルウェアやコントローラは実行されず、そのレスポンスがパイプラインの戻り値になります。これはPipelineのショートサーキット、つまり条件に応じて後続処理を短絡する仕組みです。
流れを整理すると、次のようになります。
1. ミドルウェアがリクエストを受け取る
2. 認証や権限などの条件を確認する
3. 条件を満たしていれば$next($request)を呼ぶ
4. 条件を満たしていなければ、エラー応答やリダイレクトを返す
5. 後続のパイプラインとコントローラは実行されない
ここで、$next($request)を呼ばなくても自動的に後続ミドルウェアが実行される、と考えてはいけません。Pipelineは、現在のミドルウェアが保持する$nextを明示的に呼び出すことで、次の処理へ進みます。
この仕組みは、アクセス制御を安全に実装するために欠かせません。認証に失敗した場合でもコントローラが動いてしまうなら、ミドルウェアによる保護が成立しないためです。
ショートサーキットで注意したいこと
ただし、途中で処理を止めるミドルウェアを追加すると、後続の処理だけでなく、外側のミドルウェアのAfter処理にも影響します。
たとえば、Aの中からBを呼び出し、Bが条件不成立でレスポンスを返したとします。この場合、Bより内側の処理は実行されません。しかし、AはBからレスポンスを受け取るため、Aの$next()後の処理へ戻ることがあります。
この違いを理解していないと、「認証エラー時にはログが残るはずなのに、想定したログがない」「レスポンスヘッダーが一部のケースだけ付かない」といった現象につながります。
確認するときは、次の3点を分けて見てみましょう。
- 条件判定より前の処理は実行されたか
$next($request)は実際に呼ばれたか- 外側のミドルウェアへレスポンスが戻ったか
パイプラインの途中で止まったとしても、すべての処理が消えるわけではありません。どの階層まで進み、どの階層へ戻っているのかを考えることが大切です。
$next()を呼ばないことは、単なるスキップではありません。後続の処理をそこで終わらせ、現在のミドルウェアからレスポンスを返すという明確な制御です。PipelineはHTTPリクエスト専用ではない
Pipelineという名前から、HTTPリクエストのミドルウェアだけに使うクラスだと思われることがあります。しかし、実際にはsend($data)->through([...])->thenReturn()の形で、任意のデータを複数の処理へ通すことができます。
ここで扱う対象は、必ずしもRequestオブジェクトである必要はありません。
- 配列
- クエリビルダ
- 独自のデータオブジェクト
- バッチ処理の入力値
- 複数段階で加工する検索条件
この場合でも、各パイプは次の処理を呼び出す役割を持ちます。HTTPミドルウェアのhandle()とは引数や設計が異なる場合がありますが、「対象を受け取り、必要な加工を行い、次へ渡す」という基本構造は同じです。
配列処理へPipelineを使う場合
たとえば、ユーザー一覧に対して次のような加工を順に適用したいとします。
1. 不要なデータを除外する
2. 検索条件を適用する
3. 並び順を適用する
4. ページング用に整える
この処理をひとつの大きなメソッドへ詰め込むと、条件分岐が増えたときに読みづらくなります。処理ごとにパイプを分ければ、それぞれの責務を切り出し、順番も呼び出し側で確認しやすくなります。
ここでも重要なのは、処理を分割すること自体ではありません。どのパイプがどのデータを受け取り、どの状態にして次へ渡すのかを決めることです。
パイプを増やせば自動的に保守性が高くなるわけではありません。各パイプが複数の責務を持ち始めると、HTTPミドルウェアと同じように、処理の流れが見えにくくなります。
クエリビルダ加工への応用
クエリビルダを対象にする場合は、検索条件や並び順などをパイプとして分ける設計が考えられます。
ただし、クエリビルダを変更するパイプでは、どの段階で条件が追加されるのかを明確にしてください。特に、同じカラムへ複数の条件を追加する処理や、ページングのように実行タイミングへ影響する処理が混在すると、意図しないSQLになる可能性があります。
私がこのような設計を考えるときは、まず各パイプの入力と出力を文章にしてみます。
- 検索語を受け取り、
where条件を追加したクエリビルダを返す - 並び順の指定を受け取り、
orderByを追加したクエリビルダを返す - ページ番号を受け取り、ページング可能な状態にしたクエリビルダを返す
このように役割を書けないパイプは、まだ責務が大きすぎるか、処理の境界が曖昧なのかもしれません。
Laravelミドルウェアの実行順序で起きやすい失敗
ここまでの仕組みを知っていても、実際の開発ではいくつかの落とし穴があります。特定のバージョンで設定方法が変わった場合や、同じミドルウェアが複数の経路から登録された場合は、特に混乱しやすくなります。
ミドルウェアの登録場所を見落とす
ミドルウェアが想定より早く、または遅く実行される場合、まず登録場所を確認してみましょう。
- アプリケーション全体へ適用されているか
- 特定のルートグループへ適用されているか
- 個別のルートへ指定されているか
- 別のミドルウェアグループから呼び出されていないか
- Laravel 11の
bootstrap/app.phpで追加されていないか
ミドルウェアのクラスファイルだけを見ても、実行順序は確定しません。Pipelineへ渡される最終的な配列がどのように作られているかを確認する必要があります。
$next()の前後を取り違える
レスポンスヘッダーを追加したいのに$next()より前へ書いている、あるいは認証処理を$next()のあとへ書いている、といった配置ミスも起こります。
処理を置く位置に迷ったときは、次のように考えると整理しやすくなります。
- リクエストを通す前に判断したい処理なら
$next()の前 - 後続処理が終わったあとに触れたいレスポンスなら
$next()の後 - 後続処理へ進めたくない場合は
$next()を呼ばずにレスポンスを返す
この考え方は、ミドルウェアの名前ではなく、処理のタイミングを基準にする方法です。名前が「ログ」や「認証」でも、実際に何をしたいのかによって置き場所は変わります。
途中でレスポンスを返したことを忘れる
条件分岐の中でレスポンスを返すときは、その分岐がパイプラインを止めることを意識してください。
たとえば、開発環境では通過する条件でも、本番環境ではヘッダーや認証情報の違いによって早期リターンが発生することがあります。その結果、コントローラ側のログだけが残らず、ミドルウェアのログだけが残る状態になるかもしれません。
この場合、処理が「失われた」のではなく、Pipelineの途中で止まっています。ログの場所を増やすだけでなく、どの条件で$next()が呼ばれなかったのかを記録すると、再現性のある調査につながります。
パイプに処理を詰め込みすぎる
Pipelineは処理の順序を整理するのに向いていますが、複雑な業務ロジックを無制限に押し込めるための仕組みではありません。
ひとつのミドルウェアやパイプが、認証、データ取得、権限判定、レスポンス加工まで担当すると、どこで何が起きているのか分かりにくくなります。Pipelineの層が増えるほど、各要素の責務を小さく保つことが重要になります。
次のような状態になったら、サービスクラスや専用の処理クラスへ責務を移すことも検討してみましょう。
- ミドルウェアの
handle()が長くなっている - 条件分岐の中にデータベース処理が何段階もある
- レスポンスの種類によって分岐が増えている
- 同じパイプを複数箇所で使うと前提条件が崩れる
- 実行順序を変えると別の機能が壊れる
Pipelineの内部を読むときの確認ポイント
Laravelのソースコードを読むときは、最初からすべての行を追いかけなくても大丈夫です。Pipelineの仕組みを確認する目的なら、まず次のメソッドを順番に見てみましょう。
1. send()
どのデータがパイプラインへ渡されるのかを確認します。HTTPリクエストなのか、配列なのか、独自オブジェクトなのかによって、後続のパイプの責務が変わります。
2. through()
どのパイプが、どの順序で登録されているのかを確認します。ここで見えている配列は実行時の重要な材料ですが、合成時には逆順に扱われる点に注意してください。
3. then()
パイプラインの組み立てと実行の入口です。array_reduce()、array_reverse()、carry()、prepareDestination()の関係を見ると、クロージャの連鎖がどのように作られるか分かります。
4. handle()またはパイプ側の処理
$next($request)の前後で何をしているのかを確認します。Before処理とAfter処理の配置を分けて読むと、実行順序を追いやすくなります。
5. $next()を呼ばない分岐
早期リターンがある場合は、後続の処理が中断されます。条件と返却されるレスポンスを確認し、どの地点で流れが止まるのかを特定します。
この順番で読めば、Pipeline全体を一度に理解しようとして混乱することを避けられます。まず対象データ、次にパイプの一覧、続いて合成方法、最後に各ミドルウェアの処理を見る、という流れです。
クロージャの連鎖はデバッグにも影響する
ミドルウェアのデバッグが難しく感じられる理由のひとつは、処理がクラスからクラスへ直接移動しているのではなく、クロージャを経由して連鎖していることです。
あるミドルウェアのhandle()から$next($request)を呼ぶと、次のミドルウェアへ進みます。しかし、その呼び出しは単なるメソッド呼び出しではなく、Pipelineが準備したクロージャを呼び出しています。
そのため、スタックトレースには、期待したクラス名だけでなく、クロージャやPipeline内部の処理が登場する場合があります。ここで「Laravelが勝手に別の処理を呼んでいる」と考えるのではなく、すでに組み立てられた処理の束を呼んでいると捉えると、読み解きやすくなります。
デバッグでは、次のような確認が有効です。
- 現在のミドルウェアへ本当に到達しているか
$next()の前後でどちらまで実行されているか- 早期リターンの条件が成立していないか
- 返却されているレスポンスがどの階層から来たものか
- 同じミドルウェアが複数回登録されていないか
特に、ログを$next()の前後へ分けて置く方法は、処理の流れを把握するうえで分かりやすい方法です。ログメッセージにはミドルウェア名と段階を含めておくと、複数の処理が重なった場合にも追いやすくなります。
Pipelineを使う設計で意識したい再現性
Pipelineを採用すると、処理を小さな単位へ分割できます。その一方で、処理の順序が動作結果へ直結しやすくなるため、再現性を保つ設計が重要になります。
たとえば、認証情報を読み取るパイプより前に、その情報を必要とする権限判定パイプを置けば、判定に必要な状態がまだ準備されていない可能性があります。逆に、レスポンスへヘッダーを追加する処理を、途中でレスポンスを返すパイプの内側へ置くと、エラー応答には適用されないことがあります。
順序を決めるときは、各処理の依存関係を書き出してみましょう。
- 先に読み取る必要がある情報は何か
- その情報を使う判定はどこで行うか
- 条件不成立時にどの処理を止めるか
- 成功時と失敗時の両方で必要なAfter処理は何か
- 最終処理の前に必要な準備と、戻り値に対する加工を分離できるか
この整理をしておくと、ミドルウェアを追加したときにも、どこへ入れるべきかを判断しやすくなります。
また、HTTP用のミドルウェアと、配列やクエリビルダを扱うPipelineのパイプを同じ感覚で設計しないことも大切です。共通しているのは「次の処理へ渡す」という構造ですが、HTTPレスポンスを返す処理と、データを加工して返す処理では、エラーの扱いや副作用の考え方が異なります。
LaravelミドルウェアとPipelineの仕組みを実装へ活かす
Laravelのミドルウェアパイプラインの仕組みを理解すると、実装時に見るべき場所が変わります。これまでは「このミドルウェアはどこで呼ばれているのか」を探していたところを、「このパイプラインはどの順序でクロージャへ合成され、どの地点で次の処理へ渡されるのか」と考えられるようになります。
要点をまとめると、次のとおりです。
- HTTPカーネルは
Pipelineへリクエストを渡して処理を開始する send()が対象データ、through()がパイプの一覧、then()が最終処理を指定するarray_reverse()とarray_reduce()によって、ミドルウェアが入れ子のクロージャへ合成される$next($request)より前の処理が内側へ進むBefore処理になる$next($request)のあとに書いた処理は、レスポンスが戻るときのAfter処理になる$next($request)を呼ばなければ、後続のパイプラインとコントローラは中断される- PipelineはHTTP以外のデータ処理にも利用できる
私がこの仕組みを確認するときは、まず$next()の位置へ目を向けます。次に、どのミドルウェアがどの順序で登録されているかを確認し、最後に早期リターンの分岐を探します。この3段階だけでも、実行順序に関する問題の多くは整理できます。
Pipelineの内部実装は、最初は少し抽象的に見えるかもしれません。しかし、array_reduce()が処理をひとつへまとめ、クロージャが次の処理を保持し、$next()がその連鎖を進めている、と分けて考えれば、必要以上に難しくありません。
次にミドルウェアの動きで迷ったときは、ソースコードを最初からすべて追うのではなく、send()、through()、then()、そして$next()の前後を順に確認してみましょう。処理の仕組みを分解して見れば、Laravelの実行順序は再現性を持って説明できるようになります。
Related reading: Laravel 11でKernel.phpが廃止された影響とミドルウェア登録の新しい作法 and Laravel 11のミドルウェア登録で判明した新設計と移行の注意点.