PSR-0 turns an underscore in a class name into a directory

PSR-0 is remembered as “the namespace maps to the path”, and that is only the first of its two rules. The second is a PEAR compatibility clause: each underscore in the class name also becomes a directory separator. It is the reason one autoloader can find both AppReportBuilder and Zend_Db_Adapter_Pdo_Mysql, and the reason a class you name carelessly cannot be found at all.

// AppReportBuilder          =>  src/App/Report/Builder.php
// Zend_Db_Adapter_Pdo_Mysql   =>  src/Zend/Db/Adapter/Pdo/Mysql.php
// AppReportCsv_Writer       =>  src/App/Report/Csv/Writer.php   <- the surprise

spl_autoload_register(function ($class) {
    $class = ltrim($class, '\');
    $file  = '';

    if ($pos = strrpos($class, '\')) {
        $file  = str_replace('\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR;
        $class = substr($class, $pos + 1);
    }

    // the underscore rule applies to the class name only, never the namespace
    $file .= str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php';

    require __DIR__ . '/src/' . $file;
});

Note where the underscore substitution happens in that function: after the namespace has been split off. So AppCsv_ReportBuilder keeps its underscore — the directory is genuinely called Csv_Report — while AppReportCsv_Writer is looked for two levels down. Composer’s psr-0 block implements exactly this, which is why a class that loads under a hand-written autoloader can stop loading under Composer, or the reverse. The practical consequence is a rule rather than a workaround: once a project is on PSR-0, underscores in class names are reserved, and a name like Order_Line is a directory declaration whether or not anyone meant it that way. The other cost is depth — the standard requires the vendor and package segments to appear inside src/ as well, so every file sits two directories lower than it needs to.