Laravel の Target class does not exist を原因別に切り分ける
Target class [App\Http\Controllers\FooController] does not exist. の対処法を検索すると、十年変わっていない 2 つの答えが出てくる。
検証環境
- Laravel 13.30.1
- PHP 8.4.25
- Composer 2.10.3
- Cases reproduced 13 — dump-autoload changed one of them
- Key finding A stale route:cache is not fixed by composer dump-autoload
- OS Docker Desktop (php:8.4-cli-bookworm)
Target class [App\Http\Controllers\FooController] does not exist. の対処法を検索すると、十年変わっていない 2 つの答えが出てくる。
composer dump-autoloadを実行してください。RouteServiceProviderの$namespaceのコメントを外してください。
後者は Laravel 11 でスケルトンから RouteServiceProvider そのものが消えたので、現行のアプリでは実行のしようがない。前者は Docker で 13 ケースを実測したところ、結果が変わったのは 1 ケースだけだった。
そして本番で最も刺さる原因は、その composer dump-autoload では直らない。実測している。
スクリプトは laravel-error-lab に置いてある。
検証環境
| Laravel | 13.30.1 |
| PHP | 8.4.25 |
| Composer | 2.10.3 |
| 実行環境 | Docker Desktop / php:8.4-cli-bookworm |
| ファイルシステム | 大文字小文字を区別(後述) |
APP_DEBUG |
false(後述) |
正常系から一度に 1 つだけ条件を変えて計測している。HTTP ステータスと、storage/logs/laravel.log に実際に記録された例外の両方を取っている。
13 ケースの実測結果
| 条件 | HTTP | ログに出た例外 | |
|---|---|---|---|
| A | [Class::class, 'method'](正常系) |
200 | — |
| B | 'ProbeController@index'(名前空間なし) |
500 | Target class [ProbeController] does not exist. |
| C | 'App\Http\Controllers\ProbeController@index' |
200 | — |
| D | ファイルの namespace がディレクトリと不一致 |
500 | Cannot redeclare class ... |
| E | ファイル名とクラス名が大文字小文字だけ違う | 500 | Target class [...] does not exist. |
| F | 新規ファイル。composer dump-autoload 未実行 |
200 | — |
| G | --classmap-authoritative の後に新規ファイル |
500 | Target class [...] does not exist. |
| H | G の状態で composer dump-autoload |
200 | — |
| I | 未バインドの interface をコンストラクタで要求 | 500 | Target [...] is not instantiable |
| J | 存在しないクラスをコンストラクタで要求 | 500 | Target class [...] does not exist. |
| K | route:cache 後にコントローラをリネーム |
500 | Target class [**旧名**] does not exist. |
| L | K の状態で composer dump-autoload |
500(変わらず) | 同上 |
| M | K の状態で php artisan route:clear |
200 | — |
この表から読み取れることを順に見ていく。
composer dump-autoload が効くのは 1 ケースだけ
F と G と H が答えを出している。
F は 200 である。 コントローラのファイルを新しく置いただけなら、autoloader の再生成は要らない。Composer の PSR-4 は App\ → app/ の対応を持っているだけで、クラスの一覧を持っているわけではないからだ。実行時にパスから探す。
効くのは G だけである。
composer dump-autoload --classmap-authoritative
このオプションを打つと、Composer は生成済みのクラスマップに無いものを一切探さなくなる。PSR-4 のフォールバックが切られる。この状態でファイルを足せば、当然そのクラスは見つからない。H が示すとおり、素の composer dump-autoload を打ち直せば直る。
つまり composer dump-autoload は、
--classmap-authoritativeを使っていないなら、まず関係が無い- 使っているなら(本番デプロイでは定番のオプションである)、ファイルを足すたびに必須
という位置づけになる。「とりあえず dump-autoload」が当たるのは後者に限られる。
本番で刺さるのは route:cache — そしてこれは dump-autoload で直らない
K・L・M が、この記事で最も実務に効く 3 行である。
php artisan route:cache は、ルート定義を bootstrap/cache/routes-v7.php にシリアライズして焼き込む。このときコントローラの完全修飾名も一緒に焼き込まれる。
そこでコントローラをリネームするとどうなるか。リネームのコミットには当然 routes/web.php の修正も含まれている。それでも 500 になる(K)。キャッシュファイルが古い名前を持ったままだからだ。
このときのメッセージが厄介である。
Target class [App\Http\Controllers\ProbeController] does not exist.
ProbeController はもうコードのどこにも存在しない。routes/web.php を開いても新しい名前しか書いていない。grep しても出てこない。それでもエラーはこの名前を指し続ける。
ここで定番の助言に従って composer dump-autoload を打っても、何も変わらない(L)。autoloader の問題ではないからだ。
php artisan route:clear
これで直る(M)。デプロイスクリプトが route:cache を打つなら、その前に route:clear を打つか、route:cache を必ず最新のコードに対して実行する形にしておくこと。
名前空間の書き間違いは、このエラーにならない
D は直感に反する。app/Http/Controllers/ProbeController.php に置いたファイルの中で namespace を App\Http\Controller(s 抜け)と書き間違えた場合、出るのは Target class does not exist ではない。
Cannot redeclare class App\Http\Controller\ProbeController
(previously declared in .../app/Http/Controllers/ProbeController.php:5)
ログに記録されたのはこの 1 件だけで、Target class は一度も出ていない。
理由は追える。PSR-4 は App\Http\Controllers\ProbeController の解決先として正しく app/Http/Controllers/ProbeController.php を include する。ところがそのファイルは別のクラスを宣言しているので、目的のクラスは見つからないままになる。そして同じファイルがもう一度 include される — 「以前に宣言された場所」が同じファイル自身であることが、二度読まれた証拠である。
Cannot redeclare が出たら、まず名前空間とディレクトリの対応を疑う。 autoloader の再生成でもキャッシュのクリアでもない。
is not instantiable は別の問題
I と J は、どちらもコンストラクタの型宣言が原因で、症状も似ている。だがメッセージが違う。
| 状況 | メッセージ | |
|---|---|---|
| I | interface は存在するが、バインドが無い | Target [App\Contracts\Reporter] is not instantiable while building [...] |
| J | クラスが存在しない | Target class [App\Contracts\Absent] does not exist. |
I はクラスが見つからない話ではない。コンテナがその interface の実装を知らないだけである。AppServiceProvider で
$this->app->bind(Reporter::class, DatabaseReporter::class);
と結べば解決する。composer dump-autoload も route:clear も無関係である。
J は Target class ... does not exist なので本記事の対象だが、指しているのはコントローラではなく、その依存である点に注意する。メッセージのクラス名をよく読むこと。
文字列でのアクション指定は死んでいない
B と C の対比が、冒頭の「$namespace のコメントを外す」という助言の現状を示している。
Route::get('/bare', 'ProbeController@index'); // 500
Route::get('/fqcn', 'App\Http\Controllers\ProbeController@index'); // 200
壊れるのは名前空間を省いた B だけで、完全修飾すれば文字列形式は今でも動く(C)。
Laravel 8 が自動の名前空間付与をやめ、Laravel 11 が RouteServiceProvider 自体をスケルトンから削除した。「コメントを外す」対象のファイルがもう存在しない。 古い記事のこの手順は、実行できないまま検索結果に残り続けている。
新規に書くなら [ProbeController::class, 'index'] を使えばよい。IDE が追跡でき、リネームもエディタの機能で済む。
大文字小文字違いは、Linux でしか再現しない
E は、ファイル名 Probecontroller.php の中で class ProbeController を宣言したケースである。PSR-4 は ProbeController.php を探すので、大文字小文字を区別するファイルシステム上では見つからない。
これが厄介なのは、手元では絶対に再現しないことである。macOS の APFS も Windows の NTFS も既定で大文字小文字を区別しないため、ローカルでは何事もなく動く。落ちるのは Linux のサーバに置いた瞬間だけになる。
git mv でファイル名の大文字小文字だけを変えたときに起きやすい。Git は既定でこの変更を無視するので、リポジトリには古い名前が残ったままになる。
git config core.ignorecase false
切り分け手順
ここまでを、上から順に見るだけの手順にまとめる。
1. メッセージが Target class ... does not exist かどうか
Cannot redeclare class なら → 名前空間とディレクトリの不一致(D)。
is not instantiable なら → コンテナのバインド不足(I)。ここで終わり。
2. メッセージが指しているクラス名を、コードに grep する
見つからない → route:cache が古い(K)。php artisan route:clear。
これが本番で最も多い。composer dump-autoload を打つ前にここを確認する。
3. 見つかった場合、そのファイル名はクラス名と完全一致しているか
大文字小文字まで一致しているか確認する(E)。ls の出力を目で見ること。
4. ファイルの namespace は、ディレクトリと対応しているか
app/Http/Controllers/ なら App\Http\Controllers(D)。
5. ルート定義が文字列で、名前空間を省いていないか
'FooController@index' は動かない。完全修飾するか配列形式にする(B)。
6. ここまで全部問題なければ、autoloader を疑う
grep -c setClassMapAuthoritative vendor/composer/autoload_real.php
1 が返るなら --classmap-authoritative が効いている。composer dump-autoload を打つ(G→H)。0 なら autoloader は関係が無い。
検証環境そのものが、結論を 2 回消していた
この記事の数値は、最初の 2 回の実行では取れなかった。どちらも「測れていないのに測れたように見える」壊れ方だったので、記録しておく。
1 回目 — bind mount が大文字小文字の区別を消していた。
最初はアプリをホストからの bind mount(./work:/lab)の中に作っていた。コンテナは Linux だが、Windows / macOS ホストの bind mount は大文字小文字を区別しない。結果、E が 200 を返した。
class_exists("App\Http\Controllers\ProbeController") => true
ReflectionClass::getFileName() => .../ProbeController.php
getFileName() が返したファイルは存在しない。ディスクにあるのは Probecontroller.php だけである。この scenario の目玉である「Linux でだけ落ちる原因」を、環境そのものが無効化していた。アプリをコンテナ内の /build に移して測り直している。
2 回目 — APP_DEBUG=true では計測できない。
失敗ケースが全て HTTP 000 になり、アクセスログにも何も残らず、以降のリクエストも全滅した。アプリが落ちているようにしか見えない。
実際には、PHP のビルトインサーバ(artisan serve でも素の php -S でも)がデバッグ画面の描画中に接続を切っている。APP_DEBUG=false にすると、同じケースが素直に 500 を返す。
本番は APP_DEBUG=false なので、この表の値は本番の挙動でもある。
よくある質問
Q. composer dump-autoload は打つべきですか
--classmap-authoritative を使っていないなら、打っても結果は変わりません(F が 200 であることが示しています)。使っている場合のみ必須です。次で確認できます。
grep -c setClassMapAuthoritative vendor/composer/autoload_real.php
1 なら効いています。0 なら autoloader は原因ではないので、他を見てください。
Q. エラーが指すクラス名が、コードのどこにも無い
route:cache が古い可能性が非常に高いです。php artisan route:clear を実行してください。
route:cache はコントローラの完全修飾名を bootstrap/cache/routes-v7.php に焼き込むため、リネーム後も古い名前を返し続けます。composer dump-autoload では直りません(実測済み)。
Q. RouteServiceProvider の $namespace が見つかりません
Laravel 11 で RouteServiceProvider がスケルトンから削除されました。そのファイルはもう存在しません。
ルートを文字列で書いているなら、名前空間を省かず完全修飾してください。'App\Http\Controllers\FooController@index' は現在も動きます。
Q. ローカルでは動くのに本番だけ落ちます
ファイル名とクラス名の大文字小文字が食い違っている可能性があります。macOS と Windows は既定で区別しないため、ローカルでは再現しません。
git config core.ignorecase false を設定していないと、git mv での大文字小文字変更がリポジトリに反映されない点にも注意してください。
Q. Target [X] is not instantiable も同じ問題ですか
違います。こちらはクラスが存在しています。interface や abstract class を型宣言していて、コンテナがどの実装を使うか知らない状態です。
サービスプロバイダで $this->app->bind(X::class, Y::class) を書いてください。autoloader もキャッシュも関係ありません。
Q. Cannot redeclare class が出ました
ファイルの namespace 宣言が、置いてあるディレクトリと対応していません。app/Http/Controllers/ なら namespace App\Http\Controllers; です。
PSR-4 が正しいファイルを読み、そこに別のクラスが宣言されていて、同じファイルがもう一度 include されることで起きます。
再現
git clone https://github.com/codelift-dev/laravel-error-lab
cd laravel-error-lab
docker compose build
docker compose run --rm lab bash target-class.sh
Docker 内で完結する。ホストに PHP も Composer も要らない。アプリはコンテナ内の /build に作られるので、ホストのファイルシステムの大文字小文字設定に影響されない。
Laravel 13.30.1 / PHP 8.4.25 / Composer 2.10.3 で確認している。
関連記事
- 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 つだけだった。