PHPAutoloading

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
<?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
<?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
<?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
<?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
<?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

JSON
{
    "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
<?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.

Class name and file name must match exactly
PSR-4 autoloading (both hand-written and Composer's) depends entirely on the class name matching the file path, including letter case on case-sensitive filesystems like most production Linux servers. A class declared as `App\Services\Mailer` must live in a file named exactly `Mailer.php` inside `src/Services/` — a file named `mailer.php` or a class accidentally declared as `App\Service\Mailer` (missing the "s") will fail to autoload with a "class not found" error that can be confusing to track down on a machine where the filesystem is case-insensitive, like Windows or default macOS, because the mismatch won't show up until deployment.
  • 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.php from the autoload.psr-4 section of composer.json, replacing hand-written autoloading in real projects.

  • PSR-4 autoloading requires the class name and file path to match exactly, including case.

Tip
Even on a personal or throwaway project, set up Composer and a one-line `autoload.psr-4` mapping before writing more than two or three classes. It costs a `composer.json` file and one `require __DIR__ . '/vendor/autoload.php'` line, and it means you never have to think about `require` statements again as the project grows — including for every dependency you add later.