Laravel 10 → 11/12 へのアップグレードする際に気をつけたいこと(自省記事)
(2026-05-04)
こちらのコメントで読み落とししていた部分がありましたので記事修正しました!(kawax さんありがとうございます!)
これなに
Laravel 10 → 12 へのアップグレードを実際にやってみたら 「動いているように見えて、実は静かに壊れている部分」 がいくつか見つかったので、これからアップグレードする人向けに、確認しておきたい変更点をまとめておきます。
実態としては Laravel 10 → 11 の変更インパクトが一番大きい(11 はかなり大きな構造改革を含むメジャーアップ)ので、本記事では「11 で何が変わったのか」が話の中心になります。12 は維持リリースに近い位置付けで、破壊的な変更は少なめ。
想定読者
- これから Laravel 10 系から 11 / 12 系にバージョンアップする予定がある
- もしくはすでに上げ終わったけど、本当に正しく動いているか自信がない
- Laravel 11 のリリースノートをパッと読んだけど、何が実害として効いてくるのか今ひとつピンと来ない
大前提:「構造移行」は必須ではない
本題に入る前に、 見落とされがちな超重要な前提から共有させてください。
Laravel 11 のアップグレードガイドには次のような記述があります:
However, we do not recommend that Laravel 10 applications upgrading to Laravel 11 attempt to migrate their application structure, as Laravel 11 has been carefully tuned to also support the Laravel 10 application structure.
訳すと 「Laravel 10 から 11 に上げるアプリは、構造の移行を試みないことをむしろ推奨する。Laravel 11 は Laravel 10 の構造もちゃんとサポートするように調整してある」 ということ。
つまり Laravel 10 のディレクトリ構造のまま(app/Http/Kernel.php も app/Console/Kernel.php も残したまま)11 / 12 に上げるのが、公式のおすすめパスです。
ところが現実には、
- 「Laravel 11 と言えば
bootstrap/app.php集約」という新機能の話だけがバズっている - アップグレードガイドの一部の手順(後述する TrustProxies など)が、新形式での書き方しか例示していない
-
composer create-projectで雛形を作り直すと、新形式のスケルトンが落ちてくる
…といった理由で、 「なんとなく雰囲気で構造移行に手を出してしまう」 ケースが多発します。私のチームもこのパターンでした。
そして本当に怖いのが、 「構造移行を中途半端にやった状態」が、最も事故が起きやすい状態ということ。本記事はここに焦点を当てます。
結論を先に書くと、選ぶべきは以下のいずれかです:
- 旧構造のまま運用し続ける(公式推奨)
- 完全に新構造へ移行する(やるなら腹を括って全部)
一番危ないのは「片方だけ新しくして、片方を旧のまま残す」という中間状態。
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();
繰り返しですが、 これは「できる」だけで、旧形式のままでも 11 / 12 は普通に動きます。
① 最大の地雷:bootstrap/app.php を新形式にした瞬間、旧 Kernel/Handler は静かに無視される
これが今回一番のキモです。
旧構造(Laravel 10)では Laravel が自動的に App\Http\Kernel App\Console\Kernel App\Exceptions\Handler を読み込んでいました。
新構造(bootstrap/app.php で Application::configure())では、 これらのクラスは参照されなくなります。withRouting() / withMiddleware() / withSchedule() / withExceptions() 側の設定が代わりに使われる。
ここまでは仕様通り。問題は次の点:
- 旧ファイル(
Kernel.php等)は 削除しなくても PHP として有効 - IDE は警告を出さない、
grepでも普通にヒットする - でも Laravel 11+ は読み込まない = 完全な死にコード
ファイルが残っていることで、開発者は「これは生きてるコード」と認識してしまい、 読めるけど呼ばれていないファイルを保守し続ける羽目になります。
実際に起きた症例
私のケースでは app/Console/Kernel.php::schedule() に書かれていた複数のスケジュール定義が、アップグレード後に丸ごと宙に浮いていました。
経緯はこう:
- Laravel 10 → 11 に上げる
- アップグレードガイドの TrustProxies の節を読む。新形式での書き方が載っているので、雰囲気で
bootstrap/app.phpを新形式に書き換えてしまう - その時点では
withSchedule()を書いていなかったが、Application::configure()を使った時点でApp\Console\Kernel::schedule()は読まれなくなる - スケジューラが完全に止まる
- しばらく誰も気付かない
しかも厄介なのが、
- エラーは出ない(呼ばれていないだけなので例外にならない)
- ログにも残らない(処理が走っていない以上、何も書きようがない)
-
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 に有効です。
対策
冒頭の選択肢ふたつのどちらを取るかで、対策が変わります。
(A) 旧構造のまま運用する場合(公式推奨)
-
bootstrap/app.phpは雛形のまま、Application::configure()を導入しない -
App\Http\KernelApp\Console\KernelApp\Exceptions\Handlerを引き続き使う - アップグレードガイドの「新形式の書き方」例は読み流す
(B) 新構造に移行する場合
- 必ず全部移行する。「とりあえずミドルウェアだけ新形式にして、スケジュールは旧 Kernel に残す」という中間状態は作らない
- 移行が終わったら 旧ファイルを完全に削除する(
Kernel.phpHandler.php等)。「念のため残しておく」は禁物 - アップグレード後に
schedule:listroute:listで件数を確認(移行前の件数と一致するか) - ステージングで実時刻を待ってジョブが発火するかを確認、もしくは
schedule:workでフォアグラウンド実行
② TrustProxies の設定場所が変わった
ALB / CloudFront / Cloudflare のようなリバースプロキシ配下で運用している場合、TrustProxies 設定は 必ず確認しないとハマります。
旧構造(Laravel 10、11 / 12 でも引き続き利用可)
// 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 / 12 でも問題なく動きます。
新構造(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(),
]);
});
ブラウザで叩いて期待値が返ってくるかを確認します。
③ サービスプロバイダの集約(新構造を選んだ場合のみ)
旧構造のまま運用するならこの節はスキップしてOKです。 新構造に移行する場合のみ関係します。
Laravel 10 までは app/Providers/ 配下に複数の Provider が並んでいました:
app/Providers/
├── AppServiceProvider.php
├── AuthServiceProvider.php
├── BroadcastServiceProvider.php
├── EventServiceProvider.php
└── RouteServiceProvider.php
新構造ではこれを 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 ファイルを config/app.php の providers 配列に登録したまま残すのであれば、Laravel 11 / 12 でも引き続き読み込まれます。「集約しないと動かない」わけではないので、移行が重い場合は無理に集約しないという選択肢もあります。
検知方法
少し地道ですが、以下のような確認をするしかないかなと思います:
# 旧 Provider に書かれていた処理が新環境でも動いているか確認
# - Gate / Policy 系:実際に admin 機能を試してみる
# - Event リスナー:イベント発火で発動するか試す
# - View Composer:該当 View を表示して値が入っているか
機能テストを充実させているプロジェクトならここで弾けるはず。手動テスト主体のプロジェクトは アップグレード時にスモークテスト範囲を広めにとるのが安全です。
アップグレード時のチェックリスト
ここまでの内容を踏まえて、Laravel 10 → 11/12 でアップグレードする際のチェックリストをまとめました。
事前準備
- 公式アップグレードガイドを通読する(10.x → 11.x)
- 構造移行を行うかどうかをチームで意思決定する(やらない/やる)
-
現状の
App\Console\Kernel::schedule()の件数をメモ -
現状のサービスプロバイダ一覧と各
boot()の中身をメモ
移行作業(旧構造のままにする場合)
-
bootstrap/app.phpを Laravel 10 形式のまま維持する(Application::configure()を導入しない) -
App\Http\KernelApp\Console\KernelApp\Exceptions\Handlerを引き続き利用する - アップグレードガイドの「新形式での書き方」例は参考程度に留め、無理に追従しない
移行作業(新構造に切り替える場合)
-
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();
メソッドチェーンで設定を組み立てる「ビルダーパターン」と呼ばれる書き方です。
重要:このメソッドを使った時点で、旧 App\Http\Kernel / App\Console\Kernel / App\Exceptions\Handler は読み込まれなくなることを必ず覚えておく必要があります。
サービスプロバイダ
Laravel が起動する時に 「アプリ全体で使うインスタンスをコンテナに登録する場所」。
ざっくり言うと「アプリの初期化処理をまとめて書くファイル」のような位置付け。Laravel 10 までは役割別に複数並べる文化でしたが、11 以降は AppServiceProvider 1 つに集約することが推奨されています(が、複数のままでも動きます)。
Kernel
Laravel における 「リクエスト or コマンドの入口」。
-
App\Http\Kernel:HTTP リクエストの入口(ミドルウェア通過の起点) -
App\Console\Kernel:artisan コマンドの入口(スケジューラ定義の場所)
Laravel 11+ では「Application::configure() を使うなら bootstrap/app.php に役割が吸収される」という形に変わりました。逆に言えば、 Application::configure() を使わなければ Kernel クラスは引き続き入口として機能します。
schedule:list
Laravel が「現在認識しているスケジュール」を一覧表示してくれる artisan コマンド。
php artisan schedule:list
定期実行ジョブの設定確認では 真っ先に叩くべきコマンド。「自分が書いたつもり」と「Laravel が認識している」のズレが一発で見えます。
サイレント・フェイル
「処理が失敗したのに、エラーも出さずに終了する」状態のこと。
例えば:
- 例外を
catchしてログにも書かずにreturnしてしまう - 想定外の入力で何も起きないまま処理を抜ける
- そもそも処理自体が呼ばれていない(今回のパターン)
「動いていない」より「動いているか分からない」のほうが事故になりやすいので、定期実行系の処理には heartbeat や最終実行時刻の記録を仕込んでおくと安心です。
まとめ
| 観点 | チェックポイント |
|---|---|
| 方針決定 | 構造移行をやる/やらないをチームで合意したか |
| bootstrap/app.php | 新形式にしたなら、旧 Kernel/Handler の内容を全部移行できているか |
| 死にコード | 新形式に移行したなら、旧 Kernel.php Handler.php TrustProxies.php 等を完全削除したか |
| スケジューラ |
schedule:list で件数が一致するか |
| TrustProxies | プロキシ配下で request()->isSecure() が true になるか |
| サービスプロバイダ | 新形式に移行したなら、旧 Provider の boot() 内容を AppServiceProvider に集約したか |
Laravel 10 → 11 / 12 は、「動いているように見えるけど、実は静かに壊れている」状態が発生しやすいバージョンアップです。ただし重要なのは、 その事故の多くは「公式が推奨していない構造移行」を中途半端にやったことが原因だという点。
「公式が言っている "やらなくていい" を素直に受け取って旧構造のまま上げる」か、「やるなら全部やる」のどちらかに振り切るのが、実は一番安全なルートです。
リリース直後だけでなく、数日〜数週間は本番で起きていることを観察する余裕を持って臨むのがおすすめ。
同じくアップグレードを控えている人の参考になれば。
Discussion
アップグレードガイドには新構造への変更は推奨しないとしか書かれてないのでフレームワーク内部まで詳しい人以外はやめたほうがいいでしょう。
意図的に旧構造のまま残して試してるプロジェクトがあるけど13まで問題なく更新できている。
変更方法は当時記事を書いてたけどもうqiitaもzennも全部消したので残ってない。
@kawax さん
コメントありがとうございます!勉強になります 🙇♂️
コメントを受けて少し修正させていただきました、ありがとうございます!