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/:
<?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.