Laravel Target Class Does Not Exist: 13 Cases
Search for Target class [App\Http\Controllers\FooController] does not exist. and you get the same two answers you would have got a decade ago.
Verification environment
- 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)
Search for Target class [App\Http\Controllers\FooController] does not exist. and you get the same two answers you would have got a decade ago.
Run
composer dump-autoload. Uncomment$namespaceinRouteServiceProvider.
The second cannot be followed at all: Laravel 11 deleted RouteServiceProvider from the skeleton. The first was measured here across thirteen cases in Docker, and it changed the outcome of one of them.
The cause that actually reaches production is not that one, and composer dump-autoload leaves it failing. That is measured too.
The scripts are in laravel-error-lab.
Test environment
| Laravel | 13.30.1 |
| PHP | 8.4.25 |
| Composer | 2.10.3 |
| Runtime | Docker Desktop / php:8.4-cli-bookworm |
| Filesystem | case-sensitive (this matters — see below) |
APP_DEBUG |
false (this matters too) |
Each case changes exactly one thing from a working baseline. Both the HTTP status and the exception actually written to storage/logs/laravel.log are recorded.
The thirteen cases
| Condition | HTTP | What was logged | |
|---|---|---|---|
| A | [Class::class, 'method'] (baseline) |
200 | — |
| B | 'ProbeController@index' — bare name |
500 | Target class [ProbeController] does not exist. |
| C | 'App\Http\Controllers\ProbeController@index' |
200 | — |
| D | namespace in the file does not match its directory |
500 | Cannot redeclare class ... |
| E | file name and class name differ only in case | 500 | Target class [...] does not exist. |
| F | new file, composer dump-autoload never run |
200 | — |
| G | new file added after --classmap-authoritative |
500 | Target class [...] does not exist. |
| H | G, then composer dump-autoload |
200 | — |
| I | constructor needs an unbound interface | 500 | Target [...] is not instantiable |
| J | constructor needs a class that does not exist | 500 | Target class [...] does not exist. |
| K | route:cache predates a controller rename |
500 | Target class [**the old name**] does not exist. |
| L | K, then composer dump-autoload |
500 (unchanged) | same |
| M | K, then php artisan route:clear |
200 | — |
composer dump-autoload changes exactly one case
F, G and H settle it.
F returns 200. Dropping a new controller file into app/Http/Controllers/ needs no regeneration. Composer's PSR-4 mapping only knows that App\ lives in app/; it does not hold a list of classes. It resolves from the path at runtime.
The one case it matters is G:
composer dump-autoload --classmap-authoritative
That flag tells Composer to look at nothing outside the generated class map. The PSR-4 fallback is switched off. Add a file afterwards and it cannot be found. H shows that a plain composer dump-autoload restores it.
So the honest position on that advice is:
- if you are not using
--classmap-authoritative, it is irrelevant - if you are — and it is a standard production deploy flag — it is mandatory every time a file is added
The cause that reaches production is the route cache
K, L and M are the three most useful rows in the table.
php artisan route:cache serialises the route definitions into bootstrap/cache/routes-v7.php, and the controller's fully-qualified name goes in with them.
Rename a controller and you get a 500 (K) — even though the rename commit also updated routes/web.php, because the cache file still holds the old name.
The message is the problem:
Target class [App\Http\Controllers\ProbeController] does not exist.
ProbeController exists nowhere in the codebase any more. routes/web.php names only the new class. Grep finds nothing. The error keeps pointing at it.
Follow the standard advice here and nothing changes (L). This was never an autoloader problem.
php artisan route:clear
That fixes it (M). If your deploy script runs route:cache, make sure it clears first, or that the cache is always rebuilt against the code being deployed.
A wrong namespace does not produce this error at all
D is counterintuitive. Put a file at app/Http/Controllers/ProbeController.php and typo its namespace as App\Http\Controller (no s), and what you get is not Target class does not exist:
Cannot redeclare class App\Http\Controller\ProbeController
(previously declared in .../app/Http/Controllers/ProbeController.php:5)
That was the only line logged. Target class never appeared.
The mechanism is visible in the message. PSR-4 correctly includes app/Http/Controllers/ProbeController.php looking for App\Http\Controllers\ProbeController. The file declares something else, so the target is still missing — and the same file is included a second time. "Previously declared in" naming that same file is the proof it was read twice.
Cannot redeclare means the namespace does not match the directory. Not the autoloader, not a cache.
"is not instantiable" is a different problem
I and J both come from a constructor type hint and look alike. The messages are not the same.
| Situation | Message | |
|---|---|---|
| I | the interface exists, but nothing is bound to it | Target [App\Contracts\Reporter] is not instantiable while building [...] |
| J | the class does not exist | Target class [App\Contracts\Absent] does not exist. |
I is not a missing-class problem. The container simply does not know which implementation to use:
$this->app->bind(Reporter::class, DatabaseReporter::class);
Neither composer dump-autoload nor route:clear has anything to do with it.
J does carry our message — but note that it names a dependency, not the controller. Read the class name in the message carefully.
String route actions are not dead
B against C shows where the "uncomment $namespace" advice now stands:
Route::get('/bare', 'ProbeController@index'); // 500
Route::get('/fqcn', 'App\Http\Controllers\ProbeController@index'); // 200
Only the un-namespaced form breaks. Fully qualified, the string syntax still works today.
Laravel 8 removed the automatic namespace prefix, and Laravel 11 removed RouteServiceProvider itself. There is no file left to uncomment anything in. That step survives in search results as an instruction nobody can carry out.
For new code, [ProbeController::class, 'index'] is the better form: the IDE can follow it, and a rename is an editor operation.
The case-mismatch cause only reproduces on Linux
E declares class ProbeController inside a file named Probecontroller.php. PSR-4 looks for ProbeController.php, so on a case-sensitive filesystem it is not found.
What makes this one expensive is that it cannot reproduce on your machine. APFS on macOS and NTFS on Windows are case-insensitive by default, so it works locally and fails the moment it lands on a Linux server.
It usually arrives via a git mv that changes only capitalisation — which Git ignores by default, leaving the old name in the repository:
git config core.ignorecase false
A procedure
Read it top to bottom.
1. Is the message actually Target class ... does not exist?
Cannot redeclare class → namespace/directory mismatch (D).
is not instantiable → missing container binding (I). Stop here.
2. Grep the codebase for the class name in the message.
Not found → the route cache is stale (K). Run php artisan route:clear.
This is the most common production cause. Check it before reaching for dump-autoload.
3. If found, does the file name match the class name exactly?
Including capitalisation (E). Look at the ls output rather than assuming.
4. Does the file's namespace match its directory?
app/Http/Controllers/ means App\Http\Controllers (D).
5. Is the route defined as a string with the namespace omitted?
'FooController@index' will not resolve. Qualify it or use the array form (B).
6. Only now, suspect the autoloader.
grep -c setClassMapAuthoritative vendor/composer/autoload_real.php
1 means --classmap-authoritative is in effect — run composer dump-autoload (G → H). 0 means the autoloader is not your problem.
The environment erased the findings twice
None of the numbers above survived the first two runs. Both failures looked like successful measurements, which is why they are written down.
Run one — a bind mount removed case sensitivity.
The application was first built inside a host bind mount (./work:/lab). The container is Linux, but a bind mount from a Windows or macOS host is case-insensitive. E returned 200:
class_exists("App\Http\Controllers\ProbeController") => true
ReflectionClass::getFileName() => .../ProbeController.php
The file getFileName() named does not exist. The only file on disk was Probecontroller.php. The environment had quietly neutralised the one cause this scenario exists to demonstrate. The app is now built at /build, on the container's own filesystem.
Run two — APP_DEBUG=true makes the failures unmeasurable.
Every failing case returned HTTP 000, nothing reached the access log, and every request after it failed too. It looks exactly like the application crashing.
It is not. The PHP built-in server — artisan serve and plain php -S alike — drops the connection while rendering the debug page. With APP_DEBUG=false the same cases return an ordinary 500.
Production runs with APP_DEBUG=false, so the table above is also what production returns.
FAQ
Q. Should I run composer dump-autoload?
If you are not using --classmap-authoritative, it will not change anything — F returning 200 is the evidence. It is only required when that flag is in effect:
grep -c setClassMapAuthoritative vendor/composer/autoload_real.php
1 means it is on. 0 means the autoloader is not the cause; look elsewhere.
Q. The class in the error message does not exist anywhere in my code
Almost certainly a stale route cache. Run php artisan route:clear.
route:cache bakes the controller's fully-qualified name into bootstrap/cache/routes-v7.php, so it keeps returning the pre-rename name. composer dump-autoload does not fix it — measured.
Q. I cannot find $namespace in RouteServiceProvider
Laravel 11 removed RouteServiceProvider from the application skeleton. The file no longer exists.
If you define routes as strings, qualify them fully instead: 'App\Http\Controllers\FooController@index' still works.
Q. It works locally and only fails in production
Check the capitalisation of the file name against the class name. macOS and Windows are case-insensitive by default, so this cannot reproduce locally.
Also note that without git config core.ignorecase false, a git mv that changes only capitalisation never reaches the repository.
Q. Is "Target [X] is not instantiable" the same problem?
No. That class does exist. You have type-hinted an interface or abstract class and the container does not know which implementation to build.
Bind it in a service provider: $this->app->bind(X::class, Y::class). Neither the autoloader nor any cache is involved.
Q. I got "Cannot redeclare class"
The file's namespace declaration does not match the directory it sits in. app/Http/Controllers/ requires namespace App\Http\Controllers;.
PSR-4 finds and includes the right file, the file declares a different class, and the same file ends up included a second time.
Reproduce it
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
Everything runs inside the container. No PHP or Composer on the host, and the application is built at /build so the host filesystem's case sensitivity cannot affect the result.
Verified against Laravel 13.30.1 / PHP 8.4.25 / Composer 2.10.3.
Related articles
- Laravel's 419 Page Expired Can Be Diagnosed Search for how to fix 419 Page Expired and you get the same list every time. Check @csrf. Check the session driver. Run php artisan config:clear. Check APP_KEY.
- Vite Manifest Not Found: Four Different Causes When Vite manifest not found appears right after a deploy, the answer you find is almost always "run npm run build". Measuring six cases in Docker shows that the build fixes one of the four causes, and that two of them never produce an err…