Autoloading

Autoloading with spl_autoload_register

When code touches an undefined class, interface, trait or enum (new, a static call, extends, class_exists()), PHP passes its fully qualified name to each function registered with spl_autoload_register(), in order, until one defines it. A loader that does not recognize the name just returns, so every library can register its own. A dozen lines serve src/:

A hand-written autoloader for the Acme\ShopMoney prefix (shop-money/demo.php)PHP
<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'Acme\\ShopMoney\\';                    // this namespace prefix ...
    if (!str_starts_with($class, $prefix)) {
        return;                                        // not ours: the next loader tries
    }
    $relative = str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
    echo "  [autoload] $class -> src/$relative\n";
    if (is_file($file = __DIR__ . '/src/' . $relative)) {   // ... maps to this directory
        require $file;
    }
});
use Acme\ShopMoney\{Currency, Money};
$total = Money::of('19.99')->plus(new Money(250, Currency::USD));
echo $total->format(), "\n";
var_dump(class_exists('Acme\ShopMoney\Coupon'));
Output
  [autoload] Acme\ShopMoney\Money -> src/Money.php
  [autoload] Acme\ShopMoney\Currency -> src/Currency.php
$22.49
  [autoload] Acme\ShopMoney\Coupon -> src/Coupon.php
bool(false)

Loading is lazy: CurrencyMismatch is read only if plus() ever throws it. Every miss costs a disk check (pass false as class_exists()'s second argument to skip the loaders), and loaders must not throw, since another may still succeed. The old magic __autoload() was removed in 8.0.