Autoload: PHP-Klassen ohne includes automatisch laden

Autoload: PHP-Klassen ohne includes automatisch laden

Viele PHP-Entwickler dürften mit dem Befehl require_once ausreichend und regelmäßig Bekanntschaft machen: Möchte man eine Klasse einbinden die sich in einer eigenen Datei befindet (etwa class_user.php), führte früher kein Weg daran vorbei, diese vorher selbst einzubinden. Ansonsten fliegt einem die Webanwendung um die Ohren, weil man auf eine Klasse zugreift, die PHP überhaupt nicht kennt. Zumindest bei der ersten Verwendung einer Klasse war daher ein Konstrukt wie das folgende unumgänglich:

require_once DIR . '/includes/class_user.php';
$user = new User();

Im Gegensatz zur Funktion require muss der Entwickler zwar nicht mehr höllisch aufpassen, bloß nicht aus versehen eine Datei zweimal einzubinden. Denn auch das nimmt einem PHP übel. Dennoch verursacht das ständige einbinden Arbeit und bläht den Code unnötig auf, ohne einen Mehrwert zu bieten.

PHP hat das Problem erkannt und bereits in Version 5.1.2 ein tolles Feature nachgerüstet: Die Funktion spl_autoload_register. Mit ihr lassen sich Klassen automatisch einbinden – und zwar erst dann, wenn sie gebraucht werden. Die Funktionsweise ist einfach: Man registriert einen Handler, der aufgerufen wird, wenn eine Klasse benötigt wird (etwa durch eine Instanziierung). Dieser Handler kann die benötigte Datei dann laden. Im einfachsten Fall lässt sich dies mit lediglich 3 Zeilen Code realisieren:

spl_autoload_register(function ( $className ) {
    include 'includes/class_' . $className . '.php';
});

Warum also nicht eine universelle Klasse dafür entwickeln, die sich in verschiedenen Projekten mit unterschiedlichen Namenskonvensationen nutzen lässt?

<?php

/**
 * Läd Klassen automatisch anhand bestimmter Regeln
 */
class AutoLoader {
    private $loadRules = array();
    private $loadedClasses = array();

    /**
     * Gibt einen Array zurück der die Pfade aller geladenen Dateien enthält (Für Debugzwecke)
     * @return array
     */
    public function getLoadedClasses() {
        return $this->loadedClasses;
    }

    /**
     * Definiert eine Regel, anhand dieser Dateien geladen werden
     * @param      $beforeClassName     Präfix für den Dateinamen (z.B. "class_")
     * @param      $afterClassName      Ende des Dateinamens inklusive Erweiterung (z.B. ".php")
     * @param      $folder              Voller Pfad des Ordners, in dem die Datei gesucht werden soll
     * @param bool $recursive           Gibt an ob nur in $folder gesucht werden soll oder auch rekursiv in Unterordnern von $folder
     */
    public function addLoadRule( $beforeClassName, $afterClassName, $folder, $recursive = true) {
        $this->loadRules[] = array(
            'beforeClassName' => $beforeClassName,
            'afterClassName' => $afterClassName,
            'folder' => $folder,
            'recursive' => $recursive
        );
    }

    /**
     * Startet den AutoLoader, sodass ab diesem Zeitpunkt alle verwendeten Klassen die nicht geladen wurden über die Regeln des AutoLoaders geladen werden
     */
    public function startWork() {
        spl_autoload_register( array( $this, 'loadClass') );
    }

    /**
     * Läd eine von spl_autoload_register angeforderte Klasse
     * @param $className    Name der zu ladenden Klasse (Groß/Kleinschreibung wird ignoriert)
     */
    private function loadClass( $className ) {
        foreach( $this->loadRules as $loadRule ) {
            // Dateiname ermitteln und Datei laden
            $fileName = $loadRule['beforeClassName'] . strtolower( $className ) . $loadRule['afterClassName'];
            $fullFilePath = self::searchFile( $loadRule['folder'], $fileName, $loadRule['recursive'] );
            // Datei einbinden, sofern sie gefunden wurde
            if( false !== $fullFilePath) {
                $this->loadedClasses[] = $fullFilePath;
                require_once $fullFilePath;
            }
        }
    }

    /**
     * Durchsucht einen Ordner nach einer Datei
     * @param      $dir         Vollständiger Pfad des Ordners, der durchsucht werden soll
     * @param      $fileName    Name der Datei, die gesucht wird
     * @param bool $recursive   Gibt an ob nur in $dir gesucht wird oder auch rekursiv in dessen Unterordnern
     * @return bool|string      Vollständiger Pfad der gefundenen Datei oder FALSE, wenn die Suche kein Ergebnis lieferte
     */
    public static function searchFile( $dir, $fileName, $recursive = true) {
        // Symlinks für übergeordnete Verzeichnisse entfernen
        $files = array_diff( scandir( $dir ), array( '.', '..' ) );
        foreach( $files as $file ) {
            $fullPath = $dir . '/' . $file;
            if( is_dir($fullPath) && $recursive ) {
                // Es handelt sich um einen Unterordner: Rekursiv scannen, sofern gewünscht
                $subdirScanResult = self::searchFile( $fullPath, $fileName );
                if( false !== $subdirScanResult ) {
                    return $subdirScanResult;
                }
            }else {
                // Es handelt sich um eine Datei: Entspricht diese der gesuchten?
                if( strtolower( $file ) == strtolower( $fileName) ){
                    return $fullPath;
                }
            }
        }
        // Keine Datei gefunden
        return false;
    }
}

Die Klasse AutoLoader ist nahezu überall anwendbar, da verschiedene Ordnerstrukturen und Namenskonventionen anhand von Regeln hinterlegt werden können. Werden PHP-Dateien beispielsweise nach dem Schema class_{$KLASSENNAME}.php im Ordner include gespeichert, genügt folgender Aufruf zu Beginn der Anwendung:

define( 'BASE_DIR', __DIR__ );
require_once INCLUDE_DIR . '/AutoLoader.php';
$autoLoader = new Autoloader();
$autoLoader->addLoadRule( 'class_', '.php', INCLUDE_DIR );

Der AutoLoader selbst muss natürlich noch manuell eingebunden werden. Aber danach kann benötigt man dank der Regeln nahezu keine Include oder Require-Once Anweisungen mehr. Die Funktion addLoadRule() akzeptiert als 1. Parameter ein Präfix wie etwa class_ oder Class. Im 2. Parameter folgt das Ende des Dateinamens. So lassen sich auch Namenskonventionen wie user.class.php abdecken, in dem man hier .class.php übergibt. Schlussendlich wird im letzten Parameter der Ordner übergeben, in dem sich die Dateien befinden. Dieser wird automatisch rekursiv durchsucht, also inklusive Unterordner. Die Datei includes/database/database.php wird also auch dann gefunden, wenn lediglich includes als Suchordner übergeben wird. Groß-Kleinschreibung wird nicht berücksichtigt, damit die Klasse auch z.B. für das dynamische Laden von Controllern (MVC) genutzt werden kann.

Natürlich kann man durch mehrfaches Aufrufen von addLoadRule() verschiedene Regeln definieren. Beispielsweise eine für Views und eine für Controller, wenn man nach dem MVC-Muster entwickelt. Richtig eingesetzt kann man das Thema Einbinden von Dateien damit abhaken. Wichtig ist natürlich, dass eine Namenskonvention für Dateinamen besteht und die Verzeichnisstruktur einigermaßen vernünftig eingerichtet wurde.

Die fertige Klasse kann alternativ auch als PHP-Datei hier heruntergeladen werden: AutoLoader.zip herunterladen

Leave a Reply