If you write PHP for long enough, you will meet this line:
Fatal error: Uncaught Error: Class "App\Services\InvoiceMailer" not found
Nine times out of ten, a php class not found autoload error has nothing to do with the class being missing. The file is sitting right there on disk. The problem is that Composer's autoloader can't turn the class name into a file path it's allowed to load. Once you understand how that mapping works, the fix is usually a 30-second job instead of a 30-minute panic.
I've hit this error on fresh projects, on deploys that worked yesterday, and on a script I copied out of a Gist at midnight. The causes are boringly repetitive once you know where to look. Let me walk through how autoloading actually resolves a class, then hand you a checklist you can run top to bottom.
What actually happens when PHP "can't find" a class
PHP does not scan your project for classes. When it hits new InvoiceMailer() or a static call and the class isn't already loaded, it fires every registered autoloader function in turn, passing them the fully-qualified class name (FQCN). If none of them load the class, you get the fatal error.
In a Composer project, the registered autoloader is Composer's. It reads the maps generated in vendor/composer/ and tries to translate the FQCN into a filename.
The most common strategy is PSR-4. Here's the rule in one sentence: PSR-4 maps a namespace prefix to a base directory, then treats the rest of the namespace as folders and the class name as the filename plus .php.
So with this in composer.json:
{
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}
The class App\Services\InvoiceMailer resolves like this:
- Strip the mapped prefix
App\→ leavesServices\InvoiceMailer - Replace the namespace separators with directory separators →
Services/InvoiceMailer - Prepend the base dir and append
.php→app/Services/InvoiceMailer.php
If a file exists at exactly that path and declares exactly that namespace and class, it loads. If any part of that chain is off by one character, you get "class not found." That's the whole game.
The usual suspects (and how to confirm each one)
The namespace in the file doesn't match the path
This is the number-one cause I see. The file lives at app/Services/InvoiceMailer.php but the top of the file says:
<?php
namespace App\Service; // singular - should be Services
class InvoiceMailer {}
PSR-4 will look for App\Service\InvoiceMailer in app/Service/, not find it, and give up. Open the file, check the namespace line against the folder path, character for character.
A missing or wrong use statement
If the calling file is in a different namespace and you didn't import the class, PHP resolves the short name relative to the current namespace:
<?php
namespace App\Http\Controllers;
// No `use` - PHP looks for App\Http\Controllers\InvoiceMailer
class ReportController
{
public function send()
{
$mailer = new InvoiceMailer(); // not found here
}
}
Add the import, and it resolves correctly:
use App\Services\InvoiceMailer;
Alternatively, reference the FQCN directly with a leading backslash: new \App\Services\InvoiceMailer(). The leading backslash means "from the global namespace," which is what trips people up. Write new App\Services\InvoiceMailer() without the backslash inside App\Http\Controllers and it becomes App\Http\Controllers\App\Services\InvoiceMailer. That doubled prefix produces a genuinely confusing error message.
Case mismatch or a filename that doesn't match the class
PSR-4 is case-sensitive on the filesystem level, and this is the classic "works on my machine" bug. Your Mac or Windows dev box has a case-insensitive filesystem, so Invoicemailer.php happily loads the class InvoiceMailer. You deploy to a Linux server, its filesystem is case-sensitive, and the same code throws immediately.
The fix is discipline: the filename must match the class name exactly, including case. InvoiceMailer lives in InvoiceMailer.php, never invoicemailer.php or InvoiceMailer.PHP.
You forgot composer dump-autoload
Composer caches the class-to-file map. When you add a brand-new class, or rename one, or edit the autoload block in composer.json, the cached map is stale until you regenerate it:
composer dump-autoload
For production, generate an optimized classmap so lookups don't touch the filesystem at runtime:
composer dump-autoload --optimize
If you added a class and it "doesn't exist" even though the path and namespace look perfect, this is almost certainly it. I've lost real time to this exact thing more than once.
The autoload mapping in composer.json is wrong
Maybe your source lives in src/ but composer.json still maps App\ to app/. Or someone renamed the root namespace and didn't update the map. Check that the prefix and directory in the psr-4 block actually describe your project layout:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Note the trailing backslashes are escaped in JSON (App\\) and the prefix ends with a separator. After any change here, run composer dump-autoload.
The class belongs to a package you never installed
If the missing class is something like GuzzleHttp\Client or Ramsey\Uuid\Uuid, it lives in a third-party package. "Not found" here means the package isn't in vendor/:
composer require guzzlehttp/guzzle
Check composer.json and vendor/ before assuming your own code is broken. A fresh clone that skipped composer install will throw this for every vendor class.
classmap vs psr-4 confusion
Some libraries, and plenty of legacy code, use classmap instead of PSR-4. A classmap doesn't derive the path from the namespace; it scans the listed files and directories once and records exactly where each class sits:
{
"autoload": {
"classmap": ["database/seeders", "database/factories"]
}
}
The gotcha: classmaps are built at dump time. Add a new class into a classmapped directory and it stays invisible until you run composer dump-autoload again. PSR-4, by contrast, can find new files on the fly in development because it computes the path from the name.
Non-Composer scripts with no autoloader
If you're running a standalone script that isn't part of a Composer project, nothing registers an autoloader for you. Either pull in Composer's:
require __DIR__ . '/vendor/autoload.php';
or, in tiny scripts, require the class file manually. No autoloader, no autoloading, and PHP won't guess.
Laravel-specific: stale caches
Laravel adds its own layer of caching on top of Composer, and it can mask an otherwise-correct setup. If the class genuinely exists, the namespace is right, and dump-autoload didn't help, clear the framework caches:
composer dump-autoload
php artisan optimize:clear
optimize:clear wipes the config, route, event, and compiled caches in one shot. I hit this after moving a service provider between namespaces - the code was correct, but a cached bootstrap/cache/services.php still pointed at the old FQCN. One optimize:clear and it was gone.
If you're modernizing an older Laravel codebase while you're in here, it's a good moment to lean on newer language features too - our guide to PHP enums and the write-up on typed class constants in PHP 8.3 both pair well with a tidy, well-namespaced app/ directory.
Cause to fix checklist
Run this in order. Most cases resolve before you reach the bottom.
-
Symptom: brand-new class, path and namespace look correct. Run
composer dump-autoload. Stale map is the likeliest culprit. - Symptom: works locally, breaks on the Linux server. Compare filename casing to the class name. Case-sensitive filesystem is exposing a mismatch.
-
Symptom: doubled namespace in the error (
App\Http\App\Services\...). You used a relative FQCN without a leading\, or you're missing auseimport. -
Symptom: the class is a vendor/library class. Confirm the package is installed with
composer requireand thatcomposer installactually ran. -
Symptom: namespace declared in the file doesn't match its folder. Fix the
namespaceline to mirror the PSR-4 directory path. -
Symptom: you changed the
autoloadblock. Verify the prefix/directory pair, thencomposer dump-autoload. -
Symptom: Laravel, everything above checks out. Run
php artisan optimize:clearto drop stale compiled caches. -
Symptom: plain PHP script, no framework. Make sure
require 'vendor/autoload.php'runs before you touch the class.
FAQ
Does composer dump-autoload delete anything or touch my database?
No. It only regenerates the files in vendor/composer/ that map class names to paths. It never touches your source, your .env, or your database. It's safe to run anytime, and it's the first thing to try when a class you just wrote "doesn't exist."
Why does my code work on Windows or macOS but fail on the production Linux box?
Windows and macOS default to case-insensitive filesystems, so Mailer.php and mailer.php are the same file to them. Linux treats them as different files. PSR-4 asks for the exact class name as the filename, so any casing drift that your dev machine forgave will surface the moment you deploy to Linux.
What's the difference between PSR-4 and classmap autoloading?
PSR-4 computes a file path from the namespace at runtime, so new files following the convention are found automatically in development. A classmap is a precomputed lookup table built during composer dump-autoload; it's fast and explicit but won't see new classes until you regenerate it. Most application code uses PSR-4; classmaps suit directories where files don't follow a namespace convention.
The class exists and the namespace is right, so why is it still not found?
Two remaining suspects: a stale Composer map (run composer dump-autoload) or, in Laravel, a stale compiled cache (run php artisan optimize:clear). If both are clean, re-read the error message carefully. The FQCN it prints is exactly what the autoloader searched for, and it will reveal a typo or a doubled prefix you glossed over.
Wrapping up
"Class not found" reads like PHP lost your file, but it's really the autoloader failing to translate a name into a path it's permitted to load. Fix it by working the translation backwards: read the FQCN in the error, check that a matching file exists at the PSR-4 path with matching casing, confirm the namespace and use statements line up, and regenerate the map with composer dump-autoload. On Laravel, add php artisan optimize:clear for stale caches. Keep the checklist above nearby, and this error stops being a mystery and becomes a 30-second diagnostic.