Autoloading
Every PHP class has to be defined before it's used, which usually means the file containing it has to be required or included first. That's manageable when a script uses two or three classes. It stops being manageable the moment a project grows to dozens or hundreds of classes spread across just as many files — maintaining a hand-written block of require_once statements at the top of every entry point becomes fragile, easy to forget, and painful to reorder whenever files move. Autoloading solves this by letting PHP load a class's file automatically, on demand, the first moment that class name is actually used — no manual require list at all.
The problem, without autoloading
To feel the pain autoloading removes, here's what a small project looks like without it. Every single class file needs its own require_once line, and that list has to be kept in sync by hand forever as the project grows.
index.php — manual requires that don't scale
<?php require_once __DIR__ . '/classes/Logger.php'; require_once __DIR__ . '/classes/Database.php'; require_once __DIR__ . '/classes/UserRepository.php'; require_once __DIR__ . '/classes/OrderRepository.php'; require_once __DIR__ . '/classes/Mailer.php'; // ...and one more line for every class the project ever gains. $logger = new Logger(); $db = new Database(); $users = new UserRepository($db, $logger);
This works, but it has real costs: forgetting one line causes a fatal "class not found" error, deleting a class means remembering to delete its require line too, and every new contributor has to remember to add a line whenever they add a class. None of this scales past a small handful of files.
Writing a PSR-4-style autoloader by hand
PHP lets you register a callback with spl_autoload_register(). Whenever PHP encounters a class name it hasn't loaded yet, it calls every registered autoloader callback in turn, passing the fully qualified class name, until one of them successfully defines that class (typically by requiring the right file). You can write a simple version of this yourself: map a namespace prefix to a base directory, and translate the rest of the namespaced class name into a file path, mirroring the PSR-4 convention where App\Services\Mailer lives at src/Services/Mailer.php.
A hand-written PSR-4-style autoloader
<?php
// autoload.php
spl_autoload_register(function (string $class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/src/';
// Only handle classes under the "App" namespace prefix.
if (!str_starts_with($class, $prefix)) {
return;
}
// Strip the prefix, then turn namespace separators into slashes.
$relativeClass = substr($class, strlen($prefix));
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (is_file($file)) {
require $file;
}
});With that autoloader registered, a class like App\Services\Mailer resolves to __DIR__ . '/src/Services/Mailer.php' automatically. Here's the file that class lives in, and a script that uses it without ever calling require on it directly.
src/Services/Mailer.php
<?php
namespace App\Services;
class Mailer
{
public function send(string $to, string $body): void
{
echo "Sending to {$to}: {$body}\n";
}
}index.php — no require for Mailer.php anywhere
<?php
require_once __DIR__ . '/autoload.php';
use App\Services\Mailer;
$mailer = new Mailer();
$mailer->send('team@example.com', 'Deploy finished.');Sending to team@example.com: Deploy finished.
Only autoload.php itself is required manually, once, at the top of the entry point. Every class under the App namespace — no matter how many get added later — is found automatically the first time it's referenced, because the autoloader derives the file path from the class name instead of relying on a list someone maintains by hand.
Multiple autoloaders can coexist
spl_autoload_register() can be called more than once. PHP keeps a queue of registered autoloaders and tries each one in order until the class gets defined, which is exactly how a project can autoload its own code and a vendor library's code side by side, each with a different prefix-to-directory mapping.
Two independent autoloaders registered together
<?php
spl_autoload_register(function (string $class) {
$prefix = 'App\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$file = __DIR__ . '/src/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
if (is_file($file)) {
require $file;
}
});
spl_autoload_register(function (string $class) {
$prefix = 'Vendor\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$file = __DIR__ . '/lib/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
if (is_file($file)) {
require $file;
}
});This is basically the mechanism Composer's autoloader is built on internally — a registered callback (or, in Composer's case, a highly optimized lookup structure) that turns a namespaced class name into a file path the moment that class is first needed.
How Composer replaces all of this
Hand-writing an autoloader is a great way to understand the mechanism, but essentially no real project does it this way in practice — they use Composer, PHP's dependency manager, which generates a complete autoloader for you. You describe the mapping once, declaratively, in composer.json, under the autoload.psr-4 key, instead of writing the resolution logic yourself.
composer.json — declaring the PSR-4 mapping
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}Running composer dump-autoload (or just composer install, which does it automatically) reads that mapping and generates a file at vendor/autoload.php. Requiring that one file replaces every hand-written require_once line and every custom spl_autoload_register() call a project would otherwise need — for its own code and for every third-party package installed through Composer.
index.php — the entire autoloading setup, using Composer
<?php
require __DIR__ . '/vendor/autoload.php';
use App\Services\Mailer;
$mailer = new Mailer();
$mailer->send('team@example.com', 'Deploy finished.');Sending to team@example.com: Deploy finished.
The behavior is identical to the hand-written version above, but vendor/autoload.php also transparently wires up autoloading for every package the project installed via composer require — each with its own namespace-to-directory mapping declared in its own composer.json, all merged together into one generated autoloader that PHP consults through spl_autoload_register() under the hood.
Without autoloading, every class file needs a manual
require_once, which becomes unmanageable as a project grows.spl_autoload_register()registers a callback that PHP calls automatically the first time an unknown class name is used.A hand-written PSR-4-style autoloader strips a namespace prefix, converts the remaining backslashes to slashes, and requires the resulting file path.
Multiple autoloaders can be registered at once; PHP tries each in order until one of them defines the class.
Composer generates
vendor/autoload.phpfrom theautoload.psr-4section ofcomposer.json, replacing hand-written autoloading in real projects.PSR-4 autoloading requires the class name and file path to match exactly, including case.