これなに
Laravel 10 → 12 へのアップグレードを実際にやってみたら 「動いているように見えて、実は静かに壊れている部分」 がいくつか見つかったので、これからアップグレードする人向けに、確認しておきたい変更点をまとめておきます。
実態としては Laravel 10 → 11 の変更インパクトが一番大きい(11 はかなり大きな構造改革を含むメジャーアップ)ので、本記事では「11 で何が変わったのか」が話の中心になります。12 は維持リリースに近い位置付けで、破壊的な変更は少なめ。
想定読者
- これから Laravel 10 系から 11 / 12 系にバージョンアップする予定がある
- もしくはすでに上げ終わったけど、本当に正しく動いているか自信がない
- Laravel 11 のリリースノートをパッと読んだけど、何が実害として効いてくるのか今ひとつピンと来ない
Laravel 11 で何が変わったか
ざっくり言うと、 「アプリ全体の入口が bootstrap/app.php 1 ファイルに集約された」 のが最大の変更点です。
それまで以下のように分散していた設定が、
| 設定の種類 | Laravel 10 までの場所 |
|---|---|
| ルート登録 | app/Providers/RouteServiceProvider.php |
| ミドルウェア | app/Http/Kernel.php |
| スケジュールジョブ | app/Console/Kernel.php |
| 例外ハンドラ | app/Exceptions/Handler.php |
| サービスプロバイダ群 |
app/Providers/*ServiceProvider.php × 複数 |
Laravel 11 では基本的にすべて bootstrap/app.php の Application::configure() 内に書く形に変わりました。
// bootstrap/app.php (Laravel 11+)
return Application::configure(basePath: dirname(__DIR__))
->withRouting(/* ルート登録 */)
->withMiddleware(/* ミドルウェア設定 */)
->withSchedule(/* スケジュールジョブ */)
->withExceptions(/* 例外ハンドラ */)
->create();
1か所で見渡せて良い反面、移行時にいくつか注意点があるので、以降で順に紹介します。
① 旧 Kernel/Handler ファイルが残っても、Laravel は何も警告しない
これが今回一番のキモです。
Laravel 11 へのアップグレードガイドを淡々と進めると、bootstrap/app.php を新しい形に書き換える作業が含まれます。このとき、旧 Kernel/Handler ファイルの中身を新形式に移し替える必要があります。
app/Console/Kernel.php → bootstrap/app.php の withSchedule()
app/Http/Kernel.php → bootstrap/app.php の withMiddleware()
app/Exceptions/Handler.php → bootstrap/app.php の withExceptions()
ところが、 「移し替えを忘れる」or「中身を移したけど旧ファイルを削除し忘れる」 と、以下のような状態が発生し得ます:
// [旧ファイル] app/Console/Kernel.php
→ 中身は健在。PHP として有効。grep でヒットする。IDE も警告を出さない。
→ でも Laravel 11+ は読み込まない。完全に死にコード。
ファイルが残っていることで、開発者は「これは生きてるコード」と認識してしまい、後から触りに来た人が 読めるけど呼ばれていないファイルを保守し続ける羽目になります。
実際に起きた症例
私のケースでは app/Console/Kernel.php::schedule() に書かれていた複数のスケジュール定義が、アップグレード後に丸ごと宙に浮いていました。
旧構造では Laravel が自動で App\Console\Kernel を読み込んでくれていましたが、新構造(Application::configure())ではそもそもこのクラスを参照しないので、schedule() メソッドの中身は 誰も呼ばない単なる PHP コードになっていた、というわけです。
しかも厄介なのが、
- エラーは出ない(呼ばれていないだけなので例外にならない)
- ログにも残らない(処理が走っていない以上、何も書きようがない)
-
grepでschedule(を探すと普通にヒットする(あるように見える)
という三重苦で、ファイルを開いて中身を見ているだけでは「これが死んでいる」と気付けない。
検知方法
幸い、このパターンは コマンド一発で検知できます。
php artisan schedule:list
ここで「自分が想定している件数」と一致しなければ、定義漏れか移行漏れがある可能性大。極端な場合:
INFO No scheduled tasks have been defined.
と出たら、定義が 1 件も認識されていないということ。同じ要領で、
php artisan route:list # ルートが正しく登録されているか
php artisan middleware:list # (Laravel 11 で追加)ミドルウェアの状態確認
なども、アップグレード直後の sanity check に有効です。
対策
アップグレード作業時に必ずやることリスト:
-
bootstrap/app.phpへの移行が終わったら、旧ファイル(Kernel.phpHandler.php等)を完全に削除する- 「念のため残しておく」は禁物。死にコードを後から発見できなくなる
-
アップグレード後に
schedule:listroute:listで件数を確認- アップグレード前と件数が一致するか
-
ステージング環境で実時刻を待ってジョブが発火するかを確認
- もしくは
schedule:workでフォアグラウンド実行して動きを見る
- もしくは
② TrustProxies の設定場所が変わった
ALB / CloudFront / Cloudflare のようなリバースプロキシ配下で運用している場合、TrustProxies 設定は 必ず確認しないとハマります。
旧(Laravel 10)
// app/Http/Middleware/TrustProxies.php
class TrustProxies extends Middleware
{
protected $proxies = '*';
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
// ...
}
新(Laravel 11+)
app/Http/Middleware/TrustProxies.php 自体が 削除推奨。設定は bootstrap/app.php の withMiddleware() 内に書きます。
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(
at: '*',
headers: Request::HEADER_X_FORWARDED_FOR
| Request::HEADER_X_FORWARDED_HOST
| Request::HEADER_X_FORWARDED_PORT
| Request::HEADER_X_FORWARDED_PROTO
| Request::HEADER_X_FORWARDED_AWS_ELB,
);
})
ありがちな事故
- 旧
TrustProxies.phpが残ったままで新しい設定をbootstrap/app.phpに書く
→ どちらが効いているのか分からなくなる - 旧ファイルを削除したが、新形式での設定を書き忘れる
→request()->isSecure()が常に false になり、asset()がhttp://を生成して Mixed Content - ヘッダー定数の組み合わせが旧設定とズレる
→X-Forwarded-Protoが認識されず、HTTPS 判定が壊れる
検証は以下のような一時ルートで一発:
Route::get('/_debug/scheme', function (\Illuminate\Http\Request $request) {
return response()->json([
'isSecure' => $request->isSecure(),
'scheme' => $request->getScheme(),
'x_fwd_proto' => $request->header('X-Forwarded-Proto'),
'remote_addr' => $request->server('REMOTE_ADDR'),
'trusted_proxies' => $request->getTrustedProxies(),
]);
});
ブラウザで叩いて期待値が返ってくるかを確認します。
③ サービスプロバイダがほぼ AppServiceProvider 1 つに
Laravel 10 までは app/Providers/ 配下に複数の Provider が並んでいました:
app/Providers/
├── AppServiceProvider.php
├── AuthServiceProvider.php
├── BroadcastServiceProvider.php
├── EventServiceProvider.php
└── RouteServiceProvider.php
Laravel 11 ではこれが AppServiceProvider.php 1 ファイルに集約されます(必要なら手動で増やす形)。
よくある対応漏れ
旧 Provider たちに書かれていた boot() register() の中身を AppServiceProvider に集約し忘れるケース。
たとえば AuthServiceProvider::boot() で Gate 定義をしていた場合:
// 旧 AuthServiceProvider.php
public function boot(): void
{
Gate::define('admin', fn ($user) => $user->is_admin);
}
これを AppServiceProvider::boot() に移し忘れると、Gate 定義が消えるので認可ロジックが崩れます。さらに恐ろしいのが、Gate が未定義だと denies() が false を返す(= 全部通る)かのように見える挙動が起きうる点。セキュリティ事故になり得るのでかなり注意。
検知方法
少し地道ですが、以下のような確認をするしかないかなと思います:
# 旧 Provider に書かれていた処理が新環境でも動いているか確認
# - Gate / Policy 系:実際に admin 機能を試してみる
# - Event リスナー:イベント発火で発動するか試す
# - View Composer:該当 View を表示して値が入っているか
機能テストを充実させているプロジェクトならここで弾けるはず。手動テスト主体のプロジェクトは アップグレード時にスモークテスト範囲を広めにとるのが安全です。
アップグレード時のチェックリスト
ここまでの内容を踏まえて、Laravel 10 → 11/12 でアップグレードする際のチェックリストをまとめました。
事前準備
- 公式アップグレードガイドを通読する(10.x → 11.x)
-
現状の
App\Console\Kernel::schedule()の件数をメモ -
現状のサービスプロバイダ一覧と各
boot()の中身をメモ
移行作業
-
bootstrap/app.phpを新形式に書き換え -
旧
Kernel.php/Handler.phpの中身をbootstrap/app.phpに移行 - 旧ファイルを完全に削除(残しておくのは禁物)
-
サービスプロバイダの中身を
AppServiceProviderに集約 - TrustProxies 設定の引っ越し(プロキシ配下で運用している場合)
検証
-
php artisan schedule:listで件数確認(事前にメモした件数と一致するか) -
php artisan route:listで件数確認 - ステージング環境で実時刻を待ってスケジュールジョブが発火するか確認
-
プロキシ配下なら
_debug/scheme等で TrustProxies 動作確認 - 認可(Gate / Policy)が想定通り動くか手動 / 自動テスト
- イベント / リスナー、View Composer が動くか確認
- CI のテストが全件通る
本番デプロイ後
-
数日間は
schedule:list件数 / 主要ジョブの最終実行時刻を監視 - エラーログにアップグレード関連の例外が増えていないか
- サードパーティ連携系(OAuth、Webhook、SDK 経由の通信)の動作確認
補足:用語解説
Application::configure()
Laravel 11 で導入された 新しい bootstrap 構造。
それまで複数の Kernel/Handler クラスでアプリを設定していたのを、bootstrap/app.php 1 ファイルに集約したものです。
return Application::configure(basePath: dirname(__DIR__))
->withRouting(/* ルート設定 */)
->withMiddleware(/* ミドルウェア設定 */)
->withSchedule(/* スケジュール設定 */)
->withExceptions(/* 例外ハンドラ設定 */)
->create();
メソッドチェーンで設定を組み立てる「ビルダーパターン」と呼ばれる書き方です。
サービスプロバイダ
Laravel が起動する時に 「アプリ全体で使うインスタンスをコンテナに登録する場所」。
ざっくり言うと「アプリの初期化処理をまとめて書くファイル」のような位置付け。Laravel 10 までは役割別に複数並べる文化でしたが、11 以降は AppServiceProvider 1 つに集約することが推奨されています。
Kernel
Laravel における 「リクエスト or コマンドの入口」。
-
App\Http\Kernel:HTTP リクエストの入口(ミドルウェア通過の起点) -
App\Console\Kernel:artisan コマンドの入口(スケジューラ定義の場所)
Laravel 11+ では両方とも消え、bootstrap/app.php に役割が吸収されました。
schedule:list
Laravel が「現在認識しているスケジュール」を一覧表示してくれる artisan コマンド。
php artisan schedule:list
定期実行ジョブの設定確認では 真っ先に叩くべきコマンド。「自分が書いたつもり」と「Laravel が認識している」のズレが一発で見えます。
サイレント・フェイル
「処理が失敗したのに、エラーも出さずに終了する」状態のこと。
例えば:
- 例外を
catchしてログにも書かずにreturnしてしまう - 想定外の入力で何も起きないまま処理を抜ける
- そもそも処理自体が呼ばれていない(今回のパターン)
「動いていない」より「動いているか分からない」のほうが事故になりやすいので、定期実行系の処理には heartbeat や最終実行時刻の記録を仕込んでおくと安心です。
余談:ファイル構造のシュリンクは「諸刃の剣」
Laravel 11 の構造変更の方向性自体は、個人的には好みです。「アプリの全体像が bootstrap/app.php を見れば把握できる」のは新規参入者に優しい。
一方で、「シュリンクの過程で発生する死にコード」が静かに残るリスクは、フレームワーク選定時にあまり語られないところだなと思いました。
公式ガイドはあくまで「新しい書き方」を教えてくれるだけで、「旧構造の残骸が悪さをしないかをチェックする方法」は能動的に調べないと出てきません。アップグレードを進める時は 「何を新しく書くか」と同じ熱量で「何を消すか」 を意識するのが大事だなと、今回の経験で痛感しました。
まとめ
| 観点 | チェックポイント |
|---|---|
| bootstrap/app.php | 旧 Kernel/Handler の内容をすべて移行できているか |
| 死にコード | 旧 Kernel.php Handler.php TrustProxies.php 等を完全削除したか |
| スケジューラ |
schedule:list で件数が一致するか |
| TrustProxies | プロキシ配下で request()->isSecure() が true になるか |
| サービスプロバイダ | 旧 Provider の boot() 内容を AppServiceProvider に集約したか |
Laravel 10 → 11 / 12 は、「動いているように見えるけど、実は静かに壊れている」状態が発生しやすいバージョンアップです。リリース直後だけでなく、数日〜数週間は本番で起きていることを観察する余裕を持って臨むのがおすすめ。
同じくアップグレードを控えている人の参考になれば。