Automatikgetriebe

Arbeiten mit __autoload() in PHP
Kommentare

Eine der wichtigsten Erkenntnisse bei der Entwicklung von Software ist, dass man die Wiederverwendbarkeit und Übersichtlichkeit deutlich steigern kann, wenn man das Projekt in einzelne Komponenten unterteilt und diese dann je nach Bedarf einbindet. Dass man zumindest das Nachladen ganz einfach von PHP automatisiert erledigen lassen kann, zeigt dieser Artikel.

Mit der nun schon einige Jahre zurückliegenden Freigabe der 5er Reihe von PHP wurde sie eingeführt: Die magische Methode __autoload() und mit ihr die Möglichkeit, schier endlose require– undrequire_once-Aufrufe am Anfang eines jeden Skripts ersatzlos zu streichen. Zugegeben, um diese Funktionalität nutzen zu können, muss die Anwendung objektorientiert entwickelt worden sein und sollte idealerweise in einer strukturierten und somit leicht automatisierbaren Form vorliegen.

Bevor wir uns jedoch den möglichen Implementierungen einer Autoload-Funktionalität widmen können, stellt sich zunächst einmal die Frage, wann genau diese Methode eigentlich gebraucht und aufgerufen wird. Steht bei Verwendung einer Klasse, egal ob im Zuge einer Instanziierung oder durch Verwendung von extends oder auch implements, die zugehörige Definition noch nicht zur Verfügung, versucht die PHP-Engine, diesem Umstand durch Aufrufen der __autoload()-Funktion abzuhelfen. Dabei obliegt es dem Entwickler, aus dem übergebenen Klassennamen auf die vermeintlich noch einzubindende Quelldatei zu schließen und so die benötigte Struktur per gewohntem require oder include bereitzustellen.

Minimalismus

Die wohl minimal mögliche Form einer derartigen Autoload-Funktion ist in Listing 1 abgedruckt und verwendet den übergebenen Klassennamen, ohne eine weitere Prüfung, lediglich mit einem angehängten Suffix php als Dateinamen.

Schon nach kurzem Überlegen fallen jedoch erste Schwachpunkte an einer derartigen Implementierung auf: So werden im Unix-Umfeld Dateien aufgrund der Unterscheidung von Groß- und Kleinschreibung im Dateisystem gewöhnlich durchgehend klein geschrieben, Klassennamen in PHP aber gerne in gemischter Form (CamelCase). Ebenfalls unbeachtet bleibt in dieser einfachsten Umsetzung die Möglichkeit, dass die gewünschte Datei gar nicht im aktuellen Verzeichnis, dafür aber vielleicht an einer anderen Stelle im Dateisystem gefunden werden kann. Selbstverständlich ließe sich dieses Problem umgehen, indem man alle möglichen Orte im Include-Pfad (include_path) angibt, doch sich blind darauf zu verlassen, dürfte nur in den seltensten Fällen zum Erfolg führen.

Gefährlich ist diese Minimalimplementierung auch deshalb, weil keinerlei syntaktische Prüfung des übergebenen Klassennamens vorgenommen wird: Dank der Möglichkeit, in PHP auch über variable Namen Klasseninstanzen zu erzeugen (Syntax: =new $Classname;), könnte – je nach Anwendung – der Inhalt der Variablen Classname durchaus ein externer URL oder anderweitige lokale Pfadangaben beinhalten, die so natürlich ebenfalls eingebunden werden würden. An die Stelle der Minimalfunktion aus Listing 1 tritt daher die deutlich aufgewertete Methode aus Listing 2: Nach einer Überprüfung des Klassennamens auf syntaktische Gültigkeit wird ein auf Kleinbuchstaben basierender Dateiname samt geschütztem File-Systempfad erzeugt und – sofern die Datei auch gefunden wurde – wie gewohnt per require geladen.

getMessage());
}

}

Als abschließenden Schutz und für besseres Fehler-Handling testen wir noch, ob unsere Arbeit auch von Erfolg gekrönt war und die gewünschte Klasse jetzt wirklich zur Verfügung steht.

Da PHP innerhalb des __autoload() systembedingt keine nach „außen“ gehenden Exceptions verwalten kann, müssen wir uns eines eventuell auftretenden Problems selbst annehmen. Im Produktivbetrieb ermöglicht dies eine saubere Fehlerseite für den Benutzer – ohne potenziell gefährliche Informationen über die Systemarchitektur herauszugeben.

[ header = Seite 2: Hochstapler ]

Hochstapler

Bei zunehmend komplexen Anwendungen werden die bisher vorgestellten trivialen Autoload-Mechanismen schnell an ihre Grenzen stoßen: Bei Unterstützung von nur einem Include-Verzeichnis geht zum einen die Übersichtlichkeit verloren, zum anderen dürfte es in der Praxis häufig vorkommen, dass logische Einheiten gebildet und zusammen an einem dazu passenden Ort abgelegt werden. Das Erweitern der Implementierung auf mehrere mögliche Pfade und Teilbäume wäre sicher eine Option, erfordert aber immer noch das gesamte Wissen über diese Strukturen in einer zentralen Funktion. Gerade unter Beachtung des Aspekts der Wiederverwendbarkeit verdirbt die Aussicht, sämtliche in allen möglichen Projekten vorkommenden Pfade vorhalten zu müssen, schnell jegliche Motivation. Dank der SPL ist dies zum Glück jedoch auch gar nicht notwendig: An die Stelle einer festen Autoload-Methode tritt eine ganze Reihe mittels SPL registrierter, eigener Funktionen, die somit nur jeweils einen kleinen Teilbereich abdecken müssen. Erst wenn alle registrierten Handler keinen Erfolg vermelden konnten, bricht die SPL respektive PHP die Verarbeitung ab.

Der als Listing 3 abgedruckte Quelltext verwendet die Methode spl_register_autoload(), um sowohl eine statische globale Funktion als auch eine über eine Klasseninstanz aufzurufende Methode im SPL-Autload-System anzumelden.


Aufgerufen werden die so registrierten Implementierungen in der Reihenfolge ihrer Anmeldung, bis entweder die gewünschte Klasse geladen oder die Suche erfolglos eingestellt wurde. Um sicherzugehen, dass bei einem eventuellen Wegfall der Instanz der Beispielklasse die SPL nicht mit ungültigen Referenzen arbeitet, entfernt der Code via __destruct() seinen Eintrag aus der Liste der zu verwendenden Funktionen.

Wem das Schreiben eigener Handler zu aufwändig ist, der kann durch Einsatz der spl_autoload_register()-Funktion ohne weitere Parameter auch den SPL-Defaulthandler verwenden. Dieser versucht standardmäßig, in allen im Includepath angegebenen Pfaden eine Datei mit den gesuchten Klassenbezeichner in Kleinbuchstaben sowie dem Suffix .php oder .inc zu finden. Sollte diese Liste an getesteten Suffixen nicht dem eigenen Geschmack entsprechen, so kann man diese natürlich auch überschreiben: spl_autoload_extensions(‚.class.php‘); Mögliche Alternativen sind per Komma voneinander getrennt, aber als ein String zu übergeben, ein leerer Aufruf liefert die aktuelle Einstellung zurück.

Klassenfahrt

Allen bisher gezeigten Ansätzen ist gemein, dass mit zunehmender Komplexität der Anwendung die Menge der im Zweifel zu prüfenden Pfade zunimmt. Jeder Dateisystemzugriff kostet aber wertvolle Systemzeit, und auch das Abarbeiten von massenhaft registrierten Suchmethoden wirkt sich negativ auf die Gesamt-Performance aus. Auch die Code-Redundanz aufgrund gleichartiger Methoden, die sich nur marginal, nämlich durch abweichende Pfade, unterscheiden, ist nicht unerheblich und führt bei aufwändigeren Konstruktionen schnell zu unnötigem Pflegeaufwand.

Die Wahrscheinlichkeit, dass der Codeteil, der eine Klasse instanziiert, eine ungefähre Ahnung hat, woher die Definition der gewünschten Klasse kommt, dürfte in der Praxis bei strukturiertem Arbeiten erfreulich hoch sein. Ausgehend von dieser Annahme, könnte so optional jede Klasse zum Beispiel selbst einen Autoload-Handler bereitstellen und das Gesamtsystem die Suche möglicherweise auf diese Weise deutlich abkürzen. Ein Beispiel, wie dieser Ansatz im Quelltext aussehen könnte, zeigt Listing 4: Sofern der Aufruf der globalen __autoload()-Funktion durch eine fehlende Klassendefinition innerhalb einer Instanz einer anderen Klasse ausgelöst wurde, lässt sich dies unter Zuhilfenahme der Funktion debug_backtrace() leicht erkennen.

autoloadHandler($classname);
}

// Pfad-Array um Standardpfade erweitern
$candidates[]=INCLUDEBASE.'/'.strtolower($classname).'.php';


// Flag für Errorhandling - pessimistisch
$found=false;

// Loop über Kandidaten
foreach($candidates as $fname) {

// Existenz Prüfen
if (file_exists($fname)) {
// Quelldatei laden
require_once($fname);

// Treffer bestätigen
$found=true;

break;
}
}

// Kein Treffer dabei?
if (!$found) {
throw new Exception("Keine Quelldatei für '$classname' gefunden");
}

// Klasse auch definiert?
if (!class_exists($classname)) {
throw new Exception("Datei '$fname' enthält keine Klasse '$classname'");
}

return true;

} catch (Exception $e) {
die( $e->getMessage());
}

}


class AutoloadDemo {

public function autoloadHelper($classname) {
return array(INCLUDEBASE.'/plugins/'.strtolower($classname).'.plugin.php');
}

public function callTest() {
$obj=new ExternalClass();
$obj->doSomething();
}
}

$demo=new AutoloadDemo;
$demo->callTest();

?gt

Implementiert die „aufrufende“ Klasse, wie im Beispiel, eine eigene Autoload-Methode, so wird dem globalen Suchpfad dessen Rückgabe vorweg geschaltet. Die so bereitgestellte Funktion könnte natürlich auch selbstständig alle notwendigen Schritte zum Laden der fehlenden Klasse unternehmen, im Beispiel wurde allerdings der einfachere Weg der Pfaderweiterung gewählt, da so etwaige Redundanzen im Code entfallen. Für den Fall, dass individuell auf das Fehlen, zum Beispiel eines Plug-ins, reagiert werden soll, müsste die Klasse die Verarbeitung natürlich selbsttätig ausführen.

[ header = Seite 3: Lageristen ]

Lageristen

Wurden mit der Optimierung der Autoload-Funktionalität in Listing 4 für den Idealfall zwar die notwendigen Dateisystemzugriffe schon deutlich reduziert, so finden dennoch unnötige Suchoperationen statt. Bedenkt man, dass Dateisystempfade nicht die Angewohnheit haben, sich ständig zu verändern, könnte man der Autoload-Implementierung eigentlich auch ein Gedächtnis spendieren.

Um einmal gefundene Pfade in einem einfachen Cache zwischenzuspeichern, gibt es diverse Möglichkeiten mit jeweils unterschiedlichen Vor- und Nachteilen. Die vermutlich schnellste Option ist, für jede gefundene Klasse einen Eintrag in einem Array vorzunehmen und dieses bei Beendigung des Skripts mittels var_export() in einer includebaren Datei zu sichern. Auch ein Ablegen der Einträge in einer Datenbank wie SQLite wäre denkbar. Der Einsatz größerer Datenbanken wie MySQL ist zwar technisch durchaus möglich, führt jedoch zu einem unnötigen Overhead und Performance-Verlust.

Spricht die klar bestmögliche Performance noch deutlich für die Verwendung einer dynamischen Include-Datei, da hier nativer PHP-Code generiert wird, so disqualifiziert ein nicht zu unterschätzendes Problem diesen Ansatz für die Praxis fast vollständig: Mögliche Race Conditions. Was passiert, wenn zwei Instanzen gleichzeitig unterschiedliche Änderungen am Cache-Array vornehmen und später speichern wollen? Ohne aufwändiges Locking und Abgleichen wird man hier nicht weiterkommen. Eine praxistaugliche Alternative hingegen könnte sein, beim Start aus einer SQLite-Datenbank alle bekannten Pfade passend zur aktuell aufgerufenen URL in ein Array zu übernehmen. Auf diese Weise stehen keine unnötigen Daten im Speicher, der gewünschte Pfad ist extrem leicht abzugreifen, und nur für den Fall, dass eine neue Klasse in die Anwendung eingefügt wurde, müsste eine Suche durchgeführt werden. Eigentlich fast schon paradiesische Zustände!

Chaostheorie

Was sich in der Theorie schön anhört und für neue Projekte mit Sicherheit auch sauber umsetzen lässt, hat mit gewachsenen Anwendungen in der Praxis leider so seine Probleme. Derartige Anwendungen folgen leider nur selten stringenten Strukturen, und eine an Dateinamen ausgerichtete Autoload-Logik ist folglich schnell zum Scheitern verurteilt. In einem solchen Fall, oder auch wenn im Projekt Klassen mit zur eigenen Dateisystemstruktur inkompatiblem Aufbau verwendet werden, hilft nur ein vorgenerierter, genereller Pfad-Cache mit automatisierter Klassenerkennung.

Was auf den ersten Blick vielleicht noch kompliziert anmutet, ist in der Praxis in erstaunlich wenigen Codezeilen umsetzbar. Aufbauend auf dem obigen Ansatz einer SQLite-Datenbank als Zwischenspeicher, bedarf es ansonsten nur eines einfachen rekursiven Auslesens der Verzeichnisstruktur sowie des PHP Tokenizers zum Parsing der gefundenen Quelldateien. Wird in einem auf diese Weise geöffneten Quellcode eine Klassendefinition gefunden, tragen wir den Klassenbezeichner und den Pfad zur Quelldatei in die Datenbank ein – fertig. Das aktive Gegenstück, die reale Implementierung der __autoload()-Funktion, braucht dann wie im Beispiel zuvor, nur noch einen einfachen Lookup in der Datenbank vornehmen.

Da diesem vorgenerierten Cache keine dynamischen Einträge hinzugefügt werden, könnte man hier auch das eingangs verworfene Konzept der Include-Datei wieder aufgreifen: Aufgrund des manuellen Aufrufs des Parsers ist eine Race Condition eher nicht zu erwarten. Als einziger Nachteil dieses Ansatzes fällt auf, dass hier natürlich jede Änderung an Anzahl oder Namen der eingesetzten Klassen eine manuelle Aktualisierung des Caches zur Folge haben muss, da sich das System in der Praxis blind auf die gespeicherten Angaben verlassen muss. Wem diese Notwendigkeit nicht den Spaß am vorgestellten Konzept verdorben hat, der findet in Listing 5 eine einfache Beispiel-Implementierung sowohl des Cache-Erzeugers als auch dessen Anwendung.

');
}
}

// Die von PHP aufgerufene Autoload Funktion
public function autoload($classname) {
	
// kein Eintrag -> Abbrechen
if (!isset($this->knownClasses[$classname])) {
return false;
}

// Bekannten Pfad zur Klasse verwenden
require_once($this->knownClasses[$classname]);
}

// Private Methode zum Aufbau des Caches
private function buildCache($path) {

// Iterator für den Inhalt des in $path gesetzten Pfad
$dir = new DirectoryIterator($path);

// Loop über alle Einträge
foreach($dir as $item) {

// . und .. ausfiltern
if ($item->isDot()) continue;

// Verzeichnisse rekursiv abarbeiten
if ($item->isDir()) {
$this->buildCache($item->getPathname());
continue;
}

// Dateieinträge parsen
if ($item->isFile()) {
$this->parseFile($item->getPathname());
}
}
}

private function parseFile($fname) {

// Angegebene Datei laden und in Tokens parsen
$tokens = token_get_all(file_get_contents($fname));

// Arbeitsflag
$flag=false;

// Loop über alle gefundenen Tokens
foreach ($tokens as $bucket) {
// Kein PHP Token? Weiter zum Nächsten
if (!is_array($bucket)) continue;

// Nur Interface und class sind relevant - Flag setzen
if ($bucket[0]==T_INTERFACE || $bucket[0]==T_CLASS) {
$flag=true;
continue;
}

// Der erste String nach Interface/Class ist der Name...
if ($flag && ($bucket[0]==T_STRING)) {
// Eintrag speichern ...
$this->classes[strtolower($bucket[1])]=$fname;
// ... und Flag zurücksetzen
$flag=false;
}
}
}
}

// Handlerklasse registrieren
spl_autoload_register( array(new classCache(), 'autoload') );
	
?>

 '/home/theseer/autoload/listing5.php',
'cdemo' => '/home/theseer/autoload/listing3.php',
'autoloaddemo' => '/home/theseer/autoload/listing4.php',
); ?>

Geschmackssache

Egal, welche der hier vorgestellten Varianten zum Einsatz kommen, alle kosten selbstverständlich mehr Zeit in der Ausführung als statisch implementierte include()– und require()-Anweisungen. Ob dieser Performance-Impakt in der Praxis und unter Verwendung etwaiger optimierter Logiken, wie hier beschrieben, jedoch eine Rolle spielt, muss jeder selbst entscheiden.

Der von Kritikern gerne ins Feld geführte Verlust der Übersicht, wann welche Klasse wie geladen wird, ist ein ebenfalls nicht ganz unberechtigter Kritikpunkt: Sollte eine Klasse aus ihrem bisherigen Umfeld heraus in einem anderen Projekt Verwendung finden, so sind etwaige Abhängigkeiten nur durch ein genaues, im Zweifel sogar rekursives, Codestudium herauszufinden.

Freunde des „sloppy loadings“, wie die Unterstützung von Autolaod-Techniken auch gern genannt wird, lassen sich davon jedoch nicht abschrecken: Bei sauber strukturiertem Code mit zum Teil gemeinsam genutzten Klassenbibliotheken überwiegen in der Entwicklungspraxis die Vorteile, Klassen einfach „so“ verwenden zu können – im festen Vertrauen darauf, dass das jeweilige Autoload-Backend sich um die Bereitstellung kümmern wird.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -