mermshaus: PHP - Fehlerbehandlung im Autoloader

Beitrag lesen

Der Witz bei dieser Geschichte ist, dass Klassennamen (hier der Einfachheit halber verwendet als Oberbegriff für Klassen, Interfaces, … mit oder ohne Namespace) in PHP case-insensitive sind.

Die new-Statements in diesem Code laufen völlig problemlos:

<?php

namespace foo {
    class Bar {}
}

namespace {
    var_dump(new foo\BAR(), new FOO\bar());
}

Das ist im Kontext von Autoloading durchaus ein Thema, über das diskutiert wird:

Aus Composer-Sicht besteht das Problem in diesem SELFHTML-Thread hier eigentlich nicht in den Verzeichnis- und Dateinamen, da mit der Classmap-Funktionalität problemlos (wie in „macht das Tool automatisch“) eine Zuordnung von Klassennamen zu Dateien geschaffen werden kann.

Composer generiert beispielsweise so was:

<?php

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Bar' => $baseDir . '/src/Qux.php',
    'Foo' => $baseDir . '/src/foo.php',
    'Rez' => $baseDir . '/src/Rez.php',
    'RezInterface' => $baseDir . '/src/rezinterface.php',
);

Wenn du, Rolf, die Klassennamen im Code immer in der Groß-/Kleinschreibung nutzt, wie du sie in den Sourcecode-Dateien deklariert hast, kannst du Composer mit einer Classmap für deinen eigenen Code nutzen und ganz normal Composer als Autoloader einbinden (require __DIR__ . '/vendor/autoload.php') und bist sofort alle Probleme mit falschen Dateinamen los. Auch wenn es vielleicht nicht maximal elegant ist, eine Klasse otto in einer Datei hugo.php zu deklarieren und dergleichen.

Knifflig wird es nur, wenn du dann new Hugo() schreibst statt new hugo(), weil die Klassennamen (die Schlüssel) in der Classmap von Composer case-sensitive sind (siehe dazu die oben verlinkte Diskussion auf GitHub).

Das lässt sich lösen, indem man als Fallback noch einen Autoloader in grob dieser Form hinzufügt:

<?php

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

spl_autoload_register(function ($className) {
    static $classmapLower = null;

    if (null === $classmapLower) {
        $classmap = require __DIR__ . '/vendor/composer/autoload_classmap.php';
        $classmapLower = array();
        foreach ($classmap as $key => $value) {
            $keyLower = strtolower($key);
            if (isset($classmapLower[$keyLower])) {
                throw new Exception('Duplicate class name: ' . $keyLower);
            }
            $classmapLower[$keyLower] = $value;
        }
    }

    $classNameLower = strtolower($className);

    if (isset($classmapLower[$classNameLower])) {
        require $classmapLower[$classNameLower];
    }
});

(Eventuell bietet es sich noch an, den Fallback auf bestimmte Namespaces/Verzeichnisse zu beschränken.)

Mit meiner Beispiel-Classmap oben würden mit…

var_dump(
    new rez(),
    new REZ(),
    new bar(),
    new Foo()
);

…dann die Klassen Rez und Bar über den Fallback-Loader geladen, während Foo (und indirekt RezInterface) über den normalen Composer-Loader geladen würden.

Abschließend zur Frage, ob es sinnvoll ist, so vorzugehen: Prinzipiell schließe ich mich @dedlfix an und empfehle, die Groß-/Kleinschreibung im Code und im Dateisystem zu beheben (und in jedem Fall „schlechte“ Windows-Gewohnheiten aufzugeben ;)). Ich erkenne aber an, dass es Szenarien geben kann, in denen das nicht praktikabel ist (erneut der Verweis auf den verlinkten GitHub-Thread). Für diese Fälle halte ich eine Lösung, wie ich sie hier vorgestellt habe (im Grunde auch eine – meines Erachtens relativ saubere – Umsetzung der Idee von @Matthias Apsel), für eine gangbare Alternative, bei der man sich aber darüber bewusst sein sollte, dass sie zusätzliche Komplexität und damit mögliche Fehlerquellen hinzufügt. Zudem funktioniert die vorgestellte Variante nur für Classmap-Autoloader und nicht für PSR-0- oder PSR-4-Loader. (Tipp am Rande: Bei einem Classmap-Loader immer composer update oder einen vergleichbaren Befehl ausführen, sobald sich was an den Klassen-/Dateinamen ändert.)

PS: Der Classmap-Generator von Composer kann auch komplizierte Definitionen von Namespaces und Klassen innerhalb einer Datei korrekt verarbeiten. In meinen Augen ist das eine sehr solide Funktionalität, die in einigen eher kniffligen Fällen extrem hilfreich sein kann. Classmap-Loader gelten zudem als die performance-optimierte Variante von PSR-0- und PSR-4-Loadern. Siehe dazu auch:

--optimize-autoloader (-o): Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default.