Laravel 12→13 アップグレードを実際に走らせて検証した
Laravel 12 から 13 へのアップグレード記事は既に大量にあるが、そこに書かれている注意点の多くは実際に走らせずに書かれている。Docker で本物の Laravel 12 アプリを 13 に上げて実測したところ、広く言われている 4 つの主張のうち 3 つが再現しなかった。
検証環境
- Baseline Laravel 12.64.0
- Upgraded Laravel 13.21.1
- PHP 8.4.23 (identical on both sides)
- Queue driver database (SQLite)
- Cache store database (SQLite)
- Claims tested 4 — three did not reproduce
- OS Docker Desktop (php:8.4-cli-bookworm)
Laravel 12 から 13 へのアップグレード記事は既に大量にあるが、そこに書かれている注意点の多くは実際に走らせずに書かれている。Docker で本物の Laravel 12 アプリを 13 に上げて実測したところ、広く言われている 4 つの主張のうち 3 つが再現しなかった。
本記事は、Docker 上で本物の Laravel 12 アプリを構築し、それを本物の Laravel 13 へアップグレードして何が起きるかを実測した記録である。検証した主張のうち 3 つは再現しなかった。そして、どこにも書かれていない詰まりどころが 2 つ見つかった。
| # | よく書かれている主張 | 実測結果 |
|---|---|---|
| 1 | Laravel 13 に php artisan upgrade が追加された |
存在しない |
| 2 | キャッシュプレフィックスが変わり既存キーが読めなくなる | 変わらない |
| 3 | Laravel 12 のキュージョブが 13 のワーカーで失敗する | 再現せず |
| 4 | 破壊的変更ほぼゼロ、composer update で 10 分 |
素直にやると失敗する |
再現用のスクリプト一式は laravel13-upgrade-lab に置いてある。手元で同じ結果が出せる。
検証環境
PHP は 8.4 に固定した。Laravel 12 と 13 の両方がサポートするバージョンなので、フレームワークのバージョンだけを変数として切り出せる。
| ベースライン | Laravel 12.64.0 |
| アップグレード後 | Laravel 13.21.1 |
| PHP | 8.4.23(両方で同一) |
| queue driver | database(SQLite) |
| cache store | database(SQLite) |
| 実行環境 | Docker Desktop / php:8.4-cli-bookworm |
手順は「Laravel 12 アプリを作り、キュージョブとキャッシュを実際に書き込んだ状態にしてから、同じアプリをその場で 13 に上げる」というもの。状態を持ち越すことが検証の要点である。
1. php artisan upgrade は存在しない
「Laravel 13 には互換性をチェックする php artisan upgrade コマンドが追加された」という記述を複数の記事で見かける。
Laravel 12.64.0 と Laravel 13.21.1 の両方で確認した。
$ php artisan upgrade --help
RESULT: does NOT exist on Laravel 13
$ php artisan list --raw | grep -i upgrade
(none)
upgrade に一致する artisan コマンドは 1 つも登録されていない。このコマンドを前提に手順を組むと最初のステップで止まる。
2. キャッシュプレフィックスは変わっていない
「デフォルトのキャッシュプレフィックスが変わるため、.env で明示しないと既存のキャッシュキーにアクセスできなくなる」という注意書きがある。
まずアップグレード後のアプリで確認した。Laravel 12 で書いた値がそのまま読めた。
prefix now : laravel-cache-
codelift:probe : 'written-on-12'
codelift:forever: array ( 'v' => 12 )
ただしこれだけでは結論にならない。 cache.prefix を定義しているのは config/cache.php で、これはフレームワークではなくアプリが所有するファイルである。アップグレードでは上書きされないので、「Laravel 12 の config が残っただけ」という可能性を排除できていない。
そこで、新規に作成した Laravel 13 アプリの config と突き合わせた。
アップグレードしたアプリ(config/cache.php:115):
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
新規 Laravel 13 アプリ(config/cache.php:121):
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
式は完全に同一で、どちらも laravel-cache- に解決される。 行番号が違うのは前後のコメント量の差にすぎない。この主張は成立しない。
3. Laravel 12 のキュージョブは Laravel 13 で普通に動く
これが一番よく見る注意喚起である。「破壊的変更ゼロはアプリケーションコードに対するものであって、キューに残るシリアライズ済みデータには適用されない。アップグレード前にキューをドレインせよ」というもの。
最初に Collection を積んだジョブで試したところ、普通に処理された。しかし 1 例通っただけで主張を否定するのは乱暴なので、壊れる可能性が高い形状を選び直して組み直した。
構成は「本物の Laravel 12.64.0 が直列化 → SQLite ファイルごと持ち込み → 本物の Laravel 13.21.1 のワーカーが処理」。ジョブクラスは両者でバイト単位で同一のものを置いた。
| ペイロード | 結果 |
|---|---|
Eloquent モデル(SerializesModels) |
✅ 処理成功 |
CarbonImmutable |
✅ 処理成功 |
| ネストした配列 | ✅ 処理成功 |
Bus::chain 2 段 |
✅ 処理成功 |
Illuminate\Support\Collection |
✅ 処理成功 |
App\Jobs\PayloadProbe .......................... RUNNING
[ok] kind=model-only user=probe@example.test at=- extra=[]
App\Jobs\PayloadProbe .................... 419.72ms DONE
...
jobs remaining : 0
failed : 0
5 形状すべて成功、失敗 0 件。 デフォルト構成において、この主張は再現しない。
この検証が言えないこと
以下は試していない。条件が変われば結論も変わりうる。
- Redis / SQS ドライバ、Horizon 経由
- 暗号化されたペイロード
- アプリ側で削除・改名されたクラスを参照するジョブ
3 番目は実際に壊れる。ただしそれは「古いワーカープロセスが新しいコードを見る」というデプロイ順序の問題であって、Laravel 13 固有の話ではない。Laravel 11→12 でも、コードを変えずに 13 に上げただけの場合でも同じように起きる/起きない。ドレインが有効な対策であることは変わらないが、理由が違う。
4. 実際に詰まるのは laravel/tinker
「composer.json を更新して composer update を走らせれば 10 分程度」は成り立たない。framework だけを上げようとすると依存解決に失敗する。
$ composer require laravel/framework:^13.0 --update-with-all-dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires laravel/framework ^13.0 -> satisfiable by laravel/framework[v13.12.0, ..., v13.21.1].
- laravel/tinker is locked to version v2.11.1 and an update of this package was not requested.
- laravel/tinker v2.11.1 requires illuminate/support ^6.0|...|^12.0
composer exit: 2
laravel/tinker v2 系は illuminate/support ^12.0 が上限である。--update-with-all-dependencies を付けていても、ルートで要求していないパッケージは更新対象にならないため衝突が解けない。
両方を明示すれば通る。
$ composer require "laravel/framework:^13.0" "laravel/tinker:^3.0" \
--update-with-all-dependencies
composer exit: 0
エラーメッセージには「tinker を上げろ」とは書かれていない。illuminate/support と spatie/once の replace 衝突として現れるため、原因にたどり着きにくい。
5. composer.json の PHP 制約が古いまま残る
これはどこにも書かれていなかった罠である。アップグレードが成功した後の状態を見てほしい。
| 値 | |
|---|---|
laravel/framework が実際に要求する PHP |
^8.3 |
アプリの composer.json が宣言している PHP |
^8.2 |
composer require はアプリの require.php を書き換えない。そして ^8.2 と ^8.3 は範囲が重なるため、composer はこの矛盾をエラーにしない。
結果、「PHP 8.2 で動く」と宣言したままのアプリが出来上がる。開発機が 8.3 以上なら気付かない。PHP 8.2 のホストに配った瞬間に初めて壊れる。
composer.json を手で直す必要がある。
"require": {
"php": "^8.3",
まとめ — 実際に必要な手順
composer require "laravel/framework:^13.0" "laravel/tinker:^3.0" \
--update-with-all-dependencies
その後、composer.json の require.php を ^8.3 に手で修正する。
事前のキュードレインは、Laravel 13 が理由なら不要である。デプロイ時にジョブクラスを変更するなら必要だが、それはバージョンに関係なく常に必要な配慮である。
よくある質問
Q. Laravel 13 に php artisan upgrade コマンドはありますか
ありません。Laravel 12.64.0 と 13.21.1 の両方で確認しましたが、upgrade に一致する artisan コマンドは 1 つも登録されていませんでした。このコマンドを前提に手順を組むと最初のステップで止まります。
Q. Laravel 12 のときにキューに積んだジョブは、13 のワーカーで失敗しますか
デフォルト構成では再現しませんでした。本物の Laravel 12.64.0 で直列化したジョブを、SQLite ごと持ち込んで本物の 13.21.1 のワーカーで処理したところ、Eloquent モデル・CarbonImmutable・ネスト配列・Bus::chain・Collection の 5 形状すべてが成功し、失敗は 0 件でした。
ただし Redis / SQS・Horizon・暗号化ペイロードは試していません。またアプリ側で削除・改名したクラスを参照するジョブは実際に壊れますが、これはデプロイ順序の問題で Laravel 13 固有ではありません。
Q. Laravel 13 でキャッシュのプレフィックスは変わりますか
変わりません。アップグレードしたアプリと新規に作成した Laravel 13 アプリの config/cache.php を突き合わせたところ、式は完全に同一で、どちらも laravel-cache- に解決されました。Laravel 12 で書き込んだ値もそのまま読めています。
Q. composer require laravel/framework:^13.0 だけでアップグレードできますか
できません。依存解決に失敗します。laravel/tinker の v2 系が illuminate/support ^12.0 を上限としているためで、--update-with-all-dependencies を付けてもルートで要求していないパッケージは更新対象にならず衝突が解けません。
両方を明示すれば通ります。
composer require "laravel/framework:^13.0" "laravel/tinker:^3.0" --update-with-all-dependencies
Q. アップグレードすると composer.json の PHP 要件は自動で更新されますか
されません。composer require はアプリ側の require.php を書き換えないので、framework が ^8.3 を要求しているのに composer.json は ^8.2 のまま残ります。^8.2 と ^8.3 は範囲が重なるため composer はエラーにしません。
開発機が PHP 8.3 以上なら気付かず、PHP 8.2 のホストに配った時点で初めて壊れます。手で ^8.3 に直してください。
再現方法
本記事の主張はすべて、実行ログから起こしている。同じ結果は手元で再現できる。
git clone https://github.com/codelift-dev/laravel13-upgrade-lab
cd laravel13-upgrade-lab
docker compose build
docker compose run --rm lab bash 01-baseline-l12.sh
docker compose run --rm lab bash 03-upgrade-correctly.sh
docker compose run --rm lab bash 05-queue-payloads.sh
検証は Docker 内で完結する。ホスト側に PHP も Composer も要らない。
なお本記事の結論は Laravel 13.21.1 時点のものである。将来のパッチリリースで laravel/tinker の制約が緩和されれば 4 は解消しうる。その場合は本記事を更新する。
関連記事
- Laravel スターターキット共通 backend を本番化する 6 つの改修 Laravel 公式のスターターキット(React / Vue / Livewire)は、フロントエンド層こそ違うものの backend は同一の Laravel + Fortify コード を共有している。そのため、本番運用に向けた hardening も 3 つで共通する部分が大半を占める。本記事はその共通部分 — CodeLift が 3 つの fork すべてに適用した backend 改修 — を 1 か所にまとめた pillar(土台)記事である。
- Laravel の 419 Page Expired は原因を特定できる 419 Page Expired の対処法を検索すると、だいたい同じリストが出てくる。「@csrf を確認」「セッションドライバを確認」「php artisan config:clear」「APP_KEY を確認」——。
- Laravel の Vite manifest not found を原因別に切り分ける デプロイ直後に Vite manifest not found が出たとき、検索して見つかる答えはほぼ npm run build の一択である。だが Docker で 6 ケースを実測したところ、ビルドで直るのは 4 つある原因のうち 1 つだけだった。
- Laravel で Content-Security-Policy を実装する Content-Security-Policy(CSP)は、XSS(クロスサイトスクリプティング)の被害をブラウザ側で食い止める最後の防御層。サーバーが「このページで実行してよいスクリプト・読み込んでよいリソースはこれだけ」と宣言し、ブラウザがそれ以外を拒否する。