リクエストライフサイクルと Laravel 13 の骨格
この章では、Laravel が 1 つのリクエストをどう処理しているかを追います。
ミドルウェアも例外ハンドラもサービスプロバイダも、あとの章で必ず出てきます。それぞれが経路のどこに刺さるのかを先に掴んでおくと、新しい概念が出てきたときに置き場所が分かります。逆に、この地図がないまま個別の機能を覚えると、設定をどのファイルに書けばよいのか毎回迷うことになります。
1 つのリクエストが通る道
ブラウザから GET /api/v1/products が届いてからレスポンスが返るまでを、順に追います。
入口は 1 つだけ
Laravel アプリケーションへのリクエストは、すべて public/index.php を通ります1。Nginx や Apache の設定が、どの URL へのアクセスもこのファイルへ向けるようになっています。
/api/v1/products というパスに対応するファイルがサーバー上にあるわけではありません。存在しないパスもすべて index.php に集められ、そこから先は PHP のコードがどう処理するかを決めます。この方式をフロントコントローラと呼びます。
index.php 自体は短いファイルです。やることは 2 つで、Composer が生成したオートローダを読み込むことと、bootstrap/app.php からアプリケーションのインスタンスを受け取ることだけです。
最初に作られるのはサービスコンテナ
Laravel が最初に行うのは、アプリケーション、つまりサービスコンテナのインスタンスを作ることです1。
サービスコンテナは、クラスとその依存関係を管理する仕組みです。「OrderPlacementService が欲しい」と頼むと、そのコンストラクタが必要とするクラスも一緒に組み立てて返してくれます。この仕組みは第6章で詳しく扱います。ここでは「リクエスト処理の一番最初に作られる、部品の管理台帳のようなもの」と捉えてください。
HTTP カーネルが全体を受け持つ
次に、リクエストは HTTP カーネルへ渡されます。カーネルは Illuminate\Foundation\Http\Kernel のインスタンスで、Request を受け取って Response を返す handle メソッドを持ちます1。
公式ドキュメントはカーネルを「アプリケーション全体を表す大きな黒箱」と表現しています。HTTP リクエストを入れると HTTP レスポンスが出てくる、という捉え方です。
カーネルは処理に入る前に bootstrappers と呼ばれる一連の処理を走らせます。エラーハンドリングの設定、ログの設定、実行環境の判定などがここで済みます1。これらは Laravel の内部処理で、通常は意識する必要がありません。
サービスプロバイダで部品が揃う
bootstrappers のあと、サービスプロバイダが処理されます。ここには順序に理由があるので、注意して読んでください。
Laravel はプロバイダの一覧をたどって、まず全部のインスタンスを作ります。次にすべてのプロバイダの register メソッドを呼びます。それが終わってから、すべてのプロバイダの boot メソッドを呼びます1。
register を全部済ませてから boot に移るのは、boot の時点でコンテナへの登録がすべて完了している状態を保証するためです。プロバイダ A の boot がプロバイダ B の登録した部品を使いたい場合、この順序でなければ B がまだ登録されていない可能性があります。
この順序から、書き方の規則が導かれます。
public function register(): void
{
// 「この型を頼まれたらこのクラスを返す」という対応を登録するだけ
$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
}
public function register(): void
{
// 他のプロバイダの登録がまだ終わっていない可能性がある
Event::listen(OrderPlaced::class, SendOrderConfirmation::class);
}
公式ドキュメントは register メソッドについて「サービスコンテナへのバインドだけを行うべき」「イベントリスナー・ルート・その他の機能を register 内で登録しようとしてはいけない」と明記しています2。それらは boot に書きます。
なお、Laravel が内部で使うプロバイダの多くは deferred provider です。毎リクエストで読み込まれるのではなく、そのサービスが実際に必要になったときだけ読み込まれます2。メールもキューもキャッシュもプロバイダ経由で提供されますが、使わないリクエストではコストがかかりません。
ミドルウェア、ルーター、コントローラ
ここまでで準備が整い、Request がミドルウェアを通り始めます。ミドルウェアはリクエストとレスポンスの両方に触れる層で、認証・レート制限・CORS などがここに入ります。自分で書く方法は第13章で扱います。
ミドルウェアを通過したリクエストは、ルーターが URL とメソッドを見て、対応するコントローラのアクションへ渡します。レスポンスは来た道を逆にたどって返っていきます。
bootstrap/app.php が設定の入口
Laravel 13 で新規にプロジェクトを作ると、bootstrap/app.php は次の内容になっています。
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
})->create();
40 行に満たないこのファイルが、アプリケーション全体の設定の入口です。それぞれのメソッドが、あとの章と対応しています。
| メソッド | 役割 | 扱う章 |
|---|---|---|
withRouting | ルートファイルの登録とヘルスチェックの URL | 第4章 |
withMiddleware | ミドルウェアの追加・削除・グループ化 | 第13章 |
withExceptions | 例外を HTTP レスポンスへ変換する規則 | 第9章 |
3 点、いま気づいておくと後が楽になることがあります。
api: の指定がありません。 初期状態のルートファイルは web.php と console.php の 2 つだけです3。API 用の routes/api.php は php artisan install:api を実行して初めて作られ、同時にこの withRouting へ api: が追加されます。第4章で実際に実行します。
health: '/up' が最初から入っています。 /up にアクセスするとアプリケーションが起動できているかを確認できます。監視サービスから叩く先として第21章で使います。
withExceptions に shouldRenderJsonWhen が既に書かれています。 リクエストのパスが api/* に一致するか、クライアントが JSON を期待している場合に、例外を HTML のエラーページではなく JSON で返す設定です。API を作るうえで重要な既定値で、第9章でこの続きを書きます。
ディレクトリの地図
新規プロジェクトの app ディレクトリには、Http / Models / Providers の 3 つしかありません3。
Jobs も Events も Policies も Notifications も、最初は存在しません。make:job や make:policy といった Artisan コマンドを実行したときに作られます。空のディレクトリが並んでいないのは、使っていない機能を視界に入れないためです。
各ディレクトリの役割のうち、この連載で使うものを挙げます。
| 場所 | 中身 | 初期状態 | 扱う章 |
|---|---|---|---|
app/Http | コントローラ・ミドルウェア・フォームリクエスト | あり | 4, 5, 13 |
app/Models | Eloquent モデル | あり | 3 |
app/Providers | サービスプロバイダ (AppServiceProvider が 1 つ) | あり | 6, 13 |
app/Exceptions | 自作の例外クラス (make:exception で生成) | なし | 9 |
app/Jobs | キューに載せるジョブ (make:job で生成) | なし | 16, 17 |
app/Policies | 認可のポリシークラス (make:policy で生成) | なし | 12 |
app/Notifications | 通知クラス (make:notification で生成) | なし | 15 |
routes | web.php と console.php。api.php は install:api で追加 | 2 ファイル | 4, 20 |
database | マイグレーション・ファクトリ・シーダー | あり | 3 |
config | 設定ファイル | あり | 本章 |
bootstrap | app.php とフレームワークのキャッシュ | あり | 本章 |
tests | Pest / PHPUnit のサンプルテストが同梱 | あり | 10 |
routes/web.php と routes/api.php の違いは、この連載では重要です。web.php のルートには web ミドルウェアグループが割り当てられ、セッション・CSRF 保護・cookie 暗号化が働きます。対して api.php のルートは stateless で、トークンによる認証を前提とし、セッション状態にアクセスできません3。また、api.php のルートには自動的に /api のプレフィックスが付きます4。
この連載では api.php だけを使います。
config と .env の役割分担
設定値は 2 段階で扱われます。
.env config/*.php アプリケーションのコード
(環境ごとの値) → (値の解釈と既定値) → config('...') で読む
.env には環境ごとに変わる値を書きます。データベースの接続先、メールサーバーの認証情報、アプリケーションの動作モードなどです。このファイルは環境ごとに違う内容になるので、リポジトリには入れません。
config/ 以下の PHP ファイルは、.env の値を読んで構造化します。
'debug' => (bool) env('APP_DEBUG', false),
env() の第 2 引数は、環境変数が設定されていないときの既定値です。この例では、APP_DEBUG が未設定なら false になります。
そしてアプリケーションのコードからは、env() ではなく config() で読みます。
if (config('app.debug')) {
// ...
}
if (env('APP_DEBUG')) {
// 本番環境で null になる (理由は次節)
}
この使い分けは好みの問題ではありません。次の節で説明するとおり、本番環境では env() が期待どおりに動かなくなります。
本番で効く注意点
config:cache を実行すると .env が読まれなくなる
本番環境では、すべての設定ファイルを 1 つにまとめる php artisan config:cache を実行します。読み込むファイル数が減るぶん起動が速くなります。
このコマンドを実行すると、.env ファイルはリクエスト処理と Artisan コマンドのどちらでも読み込まれなくなります5。env() を呼んでも、サーバーレベルやシステムレベルの環境変数か、そうでなければ null が返ります。
だから env() は config/ のファイルの中だけで呼びます。この規則を破ったコードは、ローカルでは動いて本番でだけ null を掴む、という最も見つけにくい形で壊れます。第21章のデプロイ手順でもう一度触れます。
なお config:cache はローカル開発では実行しません。設定を変えるたびにキャッシュを作り直す必要があるためです5。誤って実行してしまったら php artisan config:clear で消せます。
.env をリポジトリに入れない
.env にはデータベースのパスワードや外部サービスの API キーが入ります。Laravel の .gitignore には最初から .env が含まれていますが、.env.example のような別名でコピーを作って commit してしまう事故は起こりえます。
すでに commit してしまった場合、その値は履歴に残るので削除しても無効化されません。値そのものを再発行してください。
本番で APP_DEBUG を true にしない
APP_DEBUG=true の状態で例外が発生すると、スタックトレースと一緒に設定値が画面に表示されます。API であればレスポンスの JSON に含まれます。本番環境では必ず false にします。
古い記事を読むときの注意
Laravel は設定の置き場所を整理した経緯があり、少し前の記事とは構成が噛み合いません。検索で見つけた記事が現在と違うことを書いていたら、次の対応で読み替えてください。
| 古い記事に出てくるもの | 現在の書き方 | この連載で扱う章 |
|---|---|---|
app/Http/Kernel.php にミドルウェアを登録 | bootstrap/app.php の withMiddleware() | 第13章 |
app/Exceptions/Handler.php で例外を処理 | bootstrap/app.php の withExceptions() | 第9章 |
config/app.php の providers 配列にプロバイダを追加 | bootstrap/providers.php に追加 | 第6章 |
EventServiceProvider の $listen プロパティにイベントを登録 | 命名規約による自動検出 | 第15章 |
現在の app/Http に入るのはコントローラ・ミドルウェア・フォームリクエストで、Kernel.php はありません3。app/Exceptions は自作の例外クラスを置く場所で、そもそも初期状態では存在せず make:exception で作られます。
ユーザーが定義したサービスプロバイダは、すべて bootstrap/providers.php に登録されます2。
<?php
return [
App\Providers\AppServiceProvider::class,
];
app/Http/Kernel.php がなくなったのは、アプリケーション側でカーネルを継承したクラスを持たなくなったという意味です。前の節で見たとおり、Illuminate\Foundation\Http\Kernel はいまもリクエスト処理の中心にいます。「Kernel が廃止された」と書いてある記事は、この区別が曖昧です。
まとめ
- すべてのリクエストは
public/index.phpを通る。ここが唯一の入口 - Laravel が最初に作るのはサービスコンテナ。以降のすべてがこの上に乗る
- サービスプロバイダは全部の
registerが終わってからbootに移る。だからregisterにはコンテナへのバインドだけを書く bootstrap/app.phpがルーティング・ミドルウェア・例外処理の設定の入口。初期状態にapi:はなく、install:apiで追加されるenv()はconfig/のファイルの中だけで呼ぶ。本番ではconfig:cacheによって.envが読まれなくなる
次に読む
次章 題材のモデリングとデータ整備 では、この連載で 20 章にわたって使う題材のテーブルを作ります。商品・在庫・注文・注文明細をマイグレーションで定義し、Eloquent モデルと enum を対応させ、ファクトリとシーダーで開発用のデータを用意します。ここで払う初期投資が、以降のすべての章で効いてきます。
練習問題
次のコードは、ローカルでは意図どおりに動きますが本番環境では動きません。理由を説明してください
class ExternalApiClient
{
public function __construct()
{
$this->apiKey = env('PAYMENT_API_KEY');
$this->timeout = env('PAYMENT_TIMEOUT', 30);
}
}
解答例
本番環境では php artisan config:cache を実行するため、.env ファイルが読み込まれなくなります。その結果 env('PAYMENT_API_KEY') は null を返し、決済 API の呼び出しが認証エラーになります。
厄介なのは、env('PAYMENT_TIMEOUT', 30) のほうは第 2 引数の既定値 30 が返るため、エラーにならずに動いてしまうことです。設定が効いていないことに気づけません。
対応は、config/services.php のような設定ファイルに値を移し、コードからは config() で読むことです。
// config/services.php
return [
'payment' => [
'key' => env('PAYMENT_API_KEY'),
'timeout' => env('PAYMENT_TIMEOUT', 30),
],
];
public function __construct()
{
$this->apiKey = config('services.payment.key');
$this->timeout = config('services.payment.timeout');
}
config:cache は config/ 以下を読んでキャッシュを作るので、この形なら本番でも値が残ります。
サービスプロバイダの register メソッドにイベントリスナーの登録を書いてはいけないのはなぜですか
解答例
Laravel はすべてのプロバイダの register を呼び終えてから、boot の呼び出しに移ります。つまり register が実行される時点では、他のプロバイダの登録がまだ終わっていない可能性があります。
イベントリスナーの登録は、イベントディスパッチャという部品がコンテナに登録済みであることを前提にします。その前提が成立している保証があるのは boot の側です。
公式ドキュメントは register について「サービスコンテナへのバインドだけを行うべき」と明記しています。バインドは「この名前で頼まれたらこのクラスを返す」という対応を台帳に書くだけの操作で、他の部品を必要としません。だから順序の影響を受けません。