__autoload() is a single global function, so a process can have exactly one of them. That was tolerable when an application was the only thing in the tree and became untenable the moment a second library wanted the same hook. spl_autoload_register() replaced it with a queue, and it is what Composer’s generated autoloader registers itself on.
// only one of these can exist anywhere in the process
function __autoload($class)
{
require __DIR__ . '/legacy/' . $class . '.php';
}
// a stack, tried in order until something defines the class
spl_autoload_register(function ($class) {
$path = __DIR__ . '/legacy/' . str_replace('_', '/', $class) . '.php';
if (is_file($path)) {
require $path;
}
});
// registering anything at all takes __autoload out of the chain — put it back
spl_autoload_register('__autoload');
The last line is the part that catches people mid-migration: as soon as anything calls spl_autoload_register(), PHP stops invoking __autoload() unless it too has been registered. So adding Composer to an application that still has one produces a wave of class-not-found errors from code that was working an hour ago. A registered handler should also fail quietly rather than require unconditionally — the next handler in the queue may well be the one that knows about this class, and a fatal from the first one never gives it the chance. $prepend, the third argument, puts a handler at the front when order genuinely matters.