Autoloading

Autoloading loads a class file the moment the class is first used, so you stop writing require lines by hand.

Syntaxspl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool

Without autoloading, every file begins with a stack of require lines. You have to know which classes a file needs, know which files define them, and get the order right when one class extends another. You also load files on every request that the request never touches.

How PHP asks for help

When PHP meets a class, interface, trait or enum name it does not know, it does not fail straight away. It first calls every function you registered with spl_autoload_register(), passing the fully qualified name as a string — Shop\Billing\Invoice, with no leading backslash. Your function's job is to work out the file and require it. If some registered function defines the name, execution continues as if nothing happened. If none does, PHP throws Error: class not found.

An autoloader that cannot handle a name should simply return. Returning lets the next registered autoloader try. Throwing from inside one breaks class_exists() for every caller.

PSR-4

Nothing dictates how to turn a name into a path, so the community agreed on one rule, PSR-4: a namespace prefix maps to a base directory. Strip the prefix, turn each remaining backslash into a directory separator, add .php. With Shop\ mapped to src/, Shop\Billing\Invoice is src/Billing/Invoice.php.

Composer generates exactly this autoloader for you, which is why in practice you write one line, require __DIR__ . '/vendor/autoload.php';, and never write an autoloader again. The hand-written version below is what that one line replaces.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Delete the block of require lines at the top of every entry point in an existing project.
  • Load only the classes a given page touches instead of parsing the whole src/ tree per request.
  • Add a new class to src/ and use it immediately with no registration step anywhere.
  • Autoload your test classes from tests/ under their own namespace prefix.

More examples

What you write without an autoloader, and what it costs

The top half is the problem, not a style preference. Every new class means editing every entry point that uses it; forget one and you get a fatal error in production only on the page nobody tested. Reordering a class hierarchy silently breaks the list. And four of those six files are parsed on a request that needs two of them. The bottom half is the same program after Composer generates the autoloader.

Example · php
<?php
// public/index.php - the manual version

declare(strict_types=1);

// Order matters: the parent must be loaded before the child.
require __DIR__ . '/../src/Billing/Document.php';     // abstract parent
require __DIR__ . '/../src/Billing/Invoice.php';      // extends Document
require __DIR__ . '/../src/Billing/Receipt.php';      // extends Document
require __DIR__ . '/../src/Billing/Refund.php';       // never used on this page
require __DIR__ . '/../src/Reporting/MonthlyTotal.php';
require __DIR__ . '/../src/Reporting/YearlyTotal.php'; // never used on this page
// ... forty more lines as the project grows

$invoice = new Shop\Billing\Invoice('INV-1', 1250);


// The autoloaded version, complete:

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

$invoice = new Shop\Billing\Invoice('INV-1', 1250);

A PSR-4 autoloader that handles several prefixes

This is the shape of the autoloader Composer ships, reduced to its core. Two details are worth copying: register() passes a first-class callable, $this->loadClass(...), rather than an array callable, and loadClass() returns the moment it requires a file so a later prefix cannot load a second definition of the same name. Reach for this only when Composer is genuinely unavailable, for instance in a single-file script you distribute on its own.

Example · php
<?php

declare(strict_types=1);

final class Psr4Autoloader
{
    /** @var array<string, list<string>> prefix => base directories */
    private array $prefixes = [];

    public function addNamespace(string $prefix, string $baseDir): void
    {
        $prefix = trim($prefix, '\\') . '\\';
        $this->prefixes[$prefix][] = rtrim($baseDir, '/') . '/';
    }

    public function register(): void
    {
        spl_autoload_register($this->loadClass(...));
    }

    public function loadClass(string $class): void
    {
        foreach ($this->prefixes as $prefix => $baseDirs) {
            if (!str_starts_with($class, $prefix)) {
                continue;
            }

            $relative = str_replace('\\', '/', substr($class, strlen($prefix)));

            foreach ($baseDirs as $baseDir) {
                $file = $baseDir . $relative . '.php';

                if (is_file($file)) {
                    require $file;
                    return;
                }
            }
        }
    }
}

$loader = new Psr4Autoloader();
$loader->addNamespace('Shop\\', __DIR__ . '/src');
$loader->addNamespace('Shop\\Tests\\', __DIR__ . '/tests');
$loader->register();

The Composer autoload block that replaces all of it

Declare the mapping once here and run composer dump-autoload; Composer writes vendor/autoload.php and your classes resolve. Two things to notice. The double backslashes are JSON escaping, not part of the namespace, so Shop\\ in the file means Shop\ in your code. And psr-4 is lazy, one file loaded per class actually used, while files is eager and loads on every request, so it is only for plain functions that have no class name for PHP to ask about. autoload-dev entries are excluded when you install with --no-dev.

Example · json
{
    "name": "acme/shop",
    "autoload": {
        "psr-4": {
            "Shop\\": "src/"
        },
        "files": [
            "src/Support/helpers.php"
        ]
    },
    "autoload-dev": {
        "psr-4": {
            "Shop\\Tests\\": "tests/"
        }
    }
}

Discussion

  • Be the first to comment on this lesson.