PHP PSR-4 Autoloading
Autoloading is how PHP finds and loads a class file automatically the moment you use that class, instead of you writing a long chain of require statements at the top of every script. PSR-4 is the standard that defines exactly how a class’s fully-qualified name should map to a file path, and Composer is the tool that reads your PSR-4 configuration and generates the autoloader that makes it all work. Understanding PSR-4 well means you never have to manually require a class file again, and it explains the “Class not found” errors that trip up almost every PHP beginner.
Overview: How Autoloading Works
Before autoloading existed, every PHP file that needed a class had to require or include the file that defined it. In a large application with hundreds of classes, this became unmanageable: you had to know the exact file path of every dependency and keep the require list in sync as files moved. PHP solves this with autoloading: you register a callback function with the engine, and whenever PHP encounters a class, interface, trait, or enum name that hasn’t been defined yet, it calls your callback with that name as an argument, giving your code one last chance to load the right file before PHP gives up and throws a fatal error.
What PSR-4 Actually Specifies
PSR-4 (a PHP-FIG standard) defines a precise algorithm for turning a fully-qualified class name into a file path. It says: take a class name like App\Services\PaymentProcessor, strip off a registered “namespace prefix” (say App\), map that prefix to a base directory (say src/), and then convert the remaining namespace separators (\) into directory separators (/), appending .php at the end. So App\Services\PaymentProcessor becomes src/Services/PaymentProcessor.php. This is a purely mechanical, predictable transformation — no guessing, no searching the whole filesystem.
Composer’s Role
You rarely write the PSR-4 lookup logic yourself. Instead, you declare your namespace-to-directory mappings in composer.json, and when you run composer install or composer dump-autoload, Composer scans your configuration and (for PSR-4) your classmap/files settings, then generates a set of PHP files inside vendor/composer/ plus a single entry point, vendor/autoload.php. That entry point calls spl_autoload_register() internally, registering Composer’s own autoloader function with the engine. From then on, any unresolved class name is handed to Composer’s generated function, which performs the PSR-4 path calculation and requires the resulting file if it exists.
Why This Matters
Because the mapping from namespace to file path is standardized, any PSR-4-compliant library can be dropped into any PSR-4-compliant project and it just works — Composer merges every package’s autoload rules into one autoloader. This interoperability is a major reason the modern PHP package ecosystem (Packagist, Symfony components, Laravel packages, PHPUnit, and so on) functions as smoothly as it does.
Syntax
PSR-4 rules live in the autoload (or autoload-dev for test-only code) section of composer.json:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
| Part | Meaning |
|---|---|
"psr-4" |
Tells Composer to use the PSR-4 resolution algorithm for this mapping. |
"App\\" |
The namespace prefix. In JSON, a single backslash must be escaped as \\, so App\ in real PHP becomes App\\ in the JSON file. |
"src/" |
The base directory that the prefix maps to, relative to composer.json. |
After editing composer.json, you must run composer dump-autoload (or composer install/composer update) so Composer regenerates the files inside vendor/ that implement this mapping. Every script then only needs one line:
require __DIR__ . '/vendor/autoload.php';
Composer’s own spl_autoload_register() call, wrapped inside that file, has this general form when you write it yourself:
spl_autoload_register(function (string $class): void {
// resolve $class to a file path and require it
});
Examples
Example 1: A Single PSR-4 Mapping
Assume this composer.json:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
And this class saved at src/Mailer.php:
<?php
namespace App;
class Mailer
{
public function send(string $to, string $subject): string
{
return "Sending '{$subject}' to {$to}";
}
}
Now the entry script:
<?php
require __DIR__ . '/vendor/autoload.php';
use App\Mailer;
$mailer = new Mailer();
echo $mailer->send('alice@example.com', 'Welcome!');
Output:
Sending 'Welcome!' to alice@example.com
Composer never had to be told about Mailer specifically. Because App\ maps to src/, and Mailer lives directly under src/ with the namespace App, the PSR-4 formula resolves App\Mailer to src/Mailer.php automatically the first time the class is referenced.
Example 2: Nested Namespaces and Subdirectories
PSR-4 handles nested namespaces by nesting directories the same way. Here’s src/Services/PaymentProcessor.php:
<?php
namespace App\Services;
enum PaymentStatus: string
{
case Success = 'success';
case Failed = 'failed';
case Pending = 'pending';
}
class PaymentProcessor
{
public function process(float $amount): PaymentStatus
{
return match (true) {
$amount <= 0 => PaymentStatus::Failed,
$amount > 1000 => PaymentStatus::Pending,
default => PaymentStatus::Success,
};
}
}
Using it:
<?php
require __DIR__ . '/vendor/autoload.php';
use App\Services\PaymentProcessor;
$processor = new PaymentProcessor();
$status = $processor->process(250.00);
echo "Payment status: " . $status->value;
Output:
Payment status: success
The namespace App\Services mirrors the directory src/Services/ exactly, one level deeper than the previous example. Note that both the PaymentStatus enum and the PaymentProcessor class share the same file here — that’s allowed by PHP, but PSR-4 autoloading is only guaranteed to find the class whose name matches the filename (PaymentProcessor.php), so in real projects you should still give every class, interface, trait, and enum its own file.
Example 3: Constructor Promotion and Named Arguments with Autoloading
src/Models/User.php:
<?php
namespace App\Models;
class User
{
public function __construct(
public readonly string $name,
public readonly string $email
) {
}
public function greeting(): string
{
return "Hello, {$this->name}!";
}
}
Entry script:
<?php
require __DIR__ . '/vendor/autoload.php';
use App\Models\User;
$user = new User(name: 'Bob', email: 'bob@example.com');
echo $user->greeting();
Output:
Hello, Bob!
This example shows that autoloading is completely orthogonal to modern PHP syntax — readonly promoted properties and named arguments work exactly the same whether the class was required manually or found through PSR-4. The path App\Models\User → src/Models/User.php follows the identical rule as before, just one directory level deeper.
Under the Hood: What Composer’s Autoloader Actually Does
Stripped down, Composer’s generated autoloader is doing something close to this by hand:
<?php
spl_autoload_register(function (string $class): void {
$prefix = 'App\\';
$baseDir = __DIR__ . '/src/';
if (!str_starts_with($class, $prefix)) {
return;
}
$relativeClass = substr($class, strlen($prefix));
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
Walking through what happens when you write new App\Services\PaymentProcessor():
- The Zend Engine looks up
App\Services\PaymentProcessorin its internal class table and doesn’t find it — the class hasn’t been defined by any file loaded so far. - Instead of immediately raising a fatal error, PHP calls every function registered via
spl_autoload_register(), in registration order, passing the class name as the argument. - Composer’s registered callback strips the matching namespace prefix (
App\), converts the rest of the namespace into a path (Services/PaymentProcessor), and appends.php. - It checks whether that file exists and, if so,
requires it — which defines the class and lets execution continue. - Control returns to the engine, which re-checks the class table. If the class is now defined, instantiation proceeds normally. If no autoloader found the class, PHP throws a fatal
Error: Class "..." not found.
In real Composer projects, this logic is generated automatically into vendor/composer/autoload_psr4.php (a array mapping prefixes to directories) and vendor/composer/ClassLoader.php (the engine that reads that array and performs the lookup, plus caching optimizations for production).
Common Mistakes
Mistake 1: Namespace Doesn’t Match the Directory Structure
This file is saved at src/Services/Payment.php, but its namespace declaration doesn’t reflect the Services subdirectory:
<?php
namespace App;
class Payment
{
public function amount(): float
{
return 99.99;
}
}
This is syntactically valid PHP, so it won’t raise a parse error — but PSR-4 expects App\Services\Payment to live at src/Services/Payment.php. Since the namespace here is just App, Composer’s autoloader will look for this class at src/Payment.php instead, and referencing App\Services\Payment anywhere will fail with Class "App\Services\Payment" not found. The fix is to make the namespace match the real directory path:
<?php
namespace App\Services;
class Payment
{
public function amount(): float
{
return 99.99;
}
}
Mistake 2: Case Mismatches Between Namespace and Filesystem
PSR-4 requires the class name’s casing to match the file and directory names exactly. On a case-insensitive filesystem (common on Windows or default macOS), a class declared as namespace App\Services; saved in a folder literally named services (lowercase) may still autoload locally without complaint. But Linux production servers are almost always case-sensitive, so the same code that worked perfectly on a developer’s machine throws Class "App\Services\Payment" not found the moment it’s deployed. Always name directories and files with the exact same casing as the namespace segments and class name they represent, and don’t rely on local testing alone to catch this class of bug.
Mistake 3: Forgetting to Regenerate the Autoloader
Adding a new PSR-4 mapping, renaming a namespace prefix, or switching a package to use classmap/files autoloading in composer.json has no effect until you actually run composer dump-autoload (or composer install/update). The generated files inside vendor/composer/ are a snapshot taken the last time Composer ran — editing composer.json by hand does not update them automatically. If new classes suddenly report “not found” right after you edit the autoload section, regenerating the autoloader is almost always the fix.
Best Practices
- Make your namespace hierarchy mirror your directory hierarchy exactly, including capitalization, for every PSR-4-mapped class.
- Put exactly one class, interface, trait, or enum per file, and name the file after that symbol (
PaymentProcessorlives inPaymentProcessor.php). - Use
autoload-devincomposer.jsonfor test-only namespaces (e.g.Tests\) so test code is never shipped or autoloaded in production. - Run
composer dump-autoload -o(optimized) or-a(authoritative classmap) as part of your production deployment for faster class resolution — this avoids repeated filesystem checks on every request. - Never manually edit files inside
vendor/composer/; they are regenerated and any hand edits are silently discarded. - Avoid mixing manual
require/includecalls for classes that are already PSR-4 autoloadable — it defeats the purpose and can cause “cannot redeclare class” errors. - Commit
composer.jsonandcomposer.lockto version control, but keep thevendor/directory out of it — it should be rebuilt withcomposer install.
Practice Exercises
- Create a
composer.jsonthat maps the namespace prefixApp\to asrc/directory. Then write a classApp\Greetingwith a methodhello(string $name): stringthat returns a greeting string, and a small entry script that autoloads and calls it. Hint: remember to runcomposer dump-autoloadafter creating the mapping. - Add a nested namespace
App\Utilswith a classStringHelpercontaining a static methodslugify(string $text): stringthat lowercases the text and replaces spaces with hyphens. Place the file at the directory PSR-4 requires, then callApp\Utils\StringHelper::slugify('Hello World')and confirm it outputshello-world. - Deliberately rename a class’s namespace so it no longer matches its file path (as in Common Mistake 1), predict the exact fatal error message PHP would produce when the class is used, and then correct the namespace to fix it.
Summary
- Autoloading lets PHP find and load class files automatically instead of requiring them manually.
- PSR-4 defines a precise algorithm: strip a namespace prefix, map it to a base directory, and turn the remaining namespace separators into directory separators plus
.php. - Composer reads the
autoload.psr-4section ofcomposer.jsonand generatesvendor/autoload.php, which registers its resolver viaspl_autoload_register(). - Namespace structure must mirror directory structure exactly, including case, or autoloading silently fails to find the class.
- Any change to
composer.json‘s autoload section requires runningcomposer dump-autoloadto take effect. - In production, use optimized or authoritative classmap generation for faster autoloading performance.
