eo

PHP-Includes: Niemals ohne __DIR__

Includes sind ein Grundbaustein von PHP-Anwendungen, ohne den eine sinnvolle Gliederung des Codes kaum möglich ist. Selbst moderne Frameworks mit Autoloading kommen nicht ohne mindestens einen entsprechenden Befehl aus, über den weitere Skript-Ressourcen eingebunden werden (include, include_once, require, require_once). Hinter Include-Konstrukten steckt aber auch ein gewisses Maß an Funktionalität, das nicht immer völlig trivial zu durchschauen ist und das verschiedene Vorgehensweisen bei der Anwendung zulässt.

Include-Anweisungen sind Sprachkonstrukte. Sie lassen im Unterschied zu Funktionen nur ein einziges „Argument“ zu, den Pfad der einzubindenden Ressource. Sämtliche Informationen, die für den Include-Vorgang relevant sind, müssen somit entweder in diesem Pfad enthalten sein oder müssen global für die Umgebung konfiguriert werden (beispielsweise über die Zusammensetzung des Include-Pfads). Diese globalen Konfigurationen können zwar zumeist von konkreten Anwendungen überschrieben werden. Bei Libraries oder allgemein bei Software, die ein Anwender selbst auf einem Zielsystem installiert, ist es aber nicht immer unproblematisch, auf korrekte Voreinstellungen zu setzen.

Ablauf von Includes

Die PHP-Dokumentation beschreibt, was bei einem Include passiert:

Files are included based on the file path given or, if none is given, the include_path specified. If the file isn’t found in the include_path, include will finally check in the calling script's own directory and the current working directory before failing. […]

If a path is defined — whether absolute […] or relative to the current directory […] — the include_path will be ignored altogether. For example, if a filename begins with ../, the parser will look in the parent directory to find the requested file.

Einige Definition dazu:

absoluter Pfad
Ein absoluter Pfad ist ein Pfad, der mit / oder einem Laufwerksbuchstaben (Windows) beginnt, beispielsweise /includes/test.php oder c:/includes/test.php.
relativer Pfad
Ein relativer Pfad ist ein Pfad, der mit ./ oder ../ beginnt, beispielsweise ./includes/test.php oder ../includes/test.php.
Dateipfad
Ein Dateipfad (file path) ist hier ein absoluter oder relativer Pfad.
virtueller Pfad
Ein virtueller Pfad ist hier in Abgrenzung dazu ein Pfad, der kein Dateipfad ist, der also nicht absolut oder relativ ist. Ein Beispiel ist der Pfad includes/test.php.
Arbeitsverzeichnis
Das Arbeitsverzeichnis (working directory) ist üblicherweise das Verzeichnis, in dem die erste aufgerufene PHP-Datei liegt. Das Verzeichnis ändert sich auch dann nicht, wenn diese Datei weitere Dateien aus Unterverzeichnissen inkludiert.
Skriptverzeicnis
Das Skriptverzeichnis ist aus Sicht einer konkreten PHP-Datei das Verzeichnis, in dem sich diese Datei befindet. Für eine inkludierte Datei ist es das Verzeichnis, aus dem die Datei inkludiert wird. Das Skriptverzeichnis ist äquivalent zum Wert der magischen Konstanten __DIR__.
Include-Pfad
Der Include-Pfad ist eine global konfigurierbare und ansprechbare Liste von Verzeichnissen, von denen ausgehend PHP nach Entsprechungen für virtuelle Pfade sucht (Definition in der PHP-Dokumentation).

PHP beziehungsweise das Dateisystem benötigt für die Durchführung eines Includes letztlich immer einen absoluten Pfad. Nur damit lässt sich eine Datei im Dateisystem zweifelsfrei lokalisieren. Andere Arten von Pfadangaben müssen im Zuge des Include-Vorgangs in absolute Pfadangaben umgewandelt werden.

Für Dateipfade ist diese Umwandlung leicht, denn die Angaben sind bereits so spezifisch, dass der Include-Pfad ignoriert werden kan. Absolute Dateipfade sind bereits absolute Pfade, relative Dateipfade müssen lediglich ausgehend vom Arbeitsverzeichnis aufgelöst werden.

Für virtuelle Pfade ist die Umwandlung komplexer.

  1. Zuerst wird nacheinander jedes Verzeichnis des globalen Include-Pfads nach einer Entsprechung durchsucht.
  2. Wird die nachgefragte Datei dort nicht gefunden, wird daraufhin das Skriptverzeichnis durchsucht. Das ist nicht das Arbeitsverzeichnis, sondern dasjenige Verzeichnis, in dem sich die Datei befindet, die den Include-Befehl enthält.
  3. Wird auch dort nichts gefunden, wird zuletzt noch ausgehend vom Arbeitsverzeichnis gesucht.

Bemerkenswert ist dabei, dass der Include-Pfad zumeist bereits einen Eintrag . enthält, der auf das Arbeitsverzeichnis verweist. In der Standardkonfiguration von PHP (hier Version 5.4.7) steht dieser Eintrag sogar am Anfang des Include-Pfads („PHP’s default setting for include_path is ‚.;/path/to/php/pear‘“). Das bedeutet, dass Schritt 3 in der Regel zu Beginn der Auswertung noch vor Schritt 1 ausgeführt wird. Das ist vermutlich ein Indiz dafür, dass bei virtuellen Pfaden bereits in Schritt 1 eine Entsprechung erwartet wird und dass vor allem Schritt 2 nicht unbedingt die vorgesehene Funktionalität ist.

Bei Bedarf kann der Pfad aus Schritt 2, der vom Skriptverzeichnis ausgeht, über die magische Konstante __DIR__ als absoluter Pfad erzeugt werden.

$path = __DIR__ . '/includes/test.php';
include $path;

Include mit virtuellen Pfaden

Die Vorgehensweise beim Analysieren virtueller Pfade führt zu Situationen wie dieser:

(Jeder Stichpunkt symbolisiert Speicherort und Inhalt einer Datei eines kleinen Beispielprojekts.)

Ausgabe:

ich komme aus dem Projektverzeichnis
ich komme aus dem Projektverzeichnis
 
ich komme aus dem includepath
ich komme aus dem Projektverzeichnis

Im ersten Include-Block verweisen beide Zeilen auf a.php im Projektverzeichnis, da . der erste Eintrag auf dem Include-Pfad ist. Im zweiten Block wird der virtuelle Pfad wiederum auf das nun veränderte Verzeichnis aus dem Include-Pfad umgeleitet, während der Dateipfad nach wie vor rein lokal ausgewertet wird.

Der Include-Pfad hätte im ersten Block auch auf ein nicht existierendes Verzeichnis gesetzt werden können. Im Zuge von Schritt 2 wäre dann ebenfalls ./a.php eingebunden worden. Ein völlig leerer Include-Pfad (set_include_path("")) ist technisch nicht möglich.

Dieses Verhalten von Includes mit virtuellen Pfadangaben kann beabsichtigt sein, um beispielsweise zu ermöglichen, durch ein Umschreiben des Include-Pfads Funktionalität auszutauschen, ohne die Originaldateien zu verändern. Umgekehrt besteht aber genauso die Gefahr, dass eine einzubindende Datei a.php aufgrund einer Namenskollision mit einem anderen Projekt unbeabsichtigt von der falschen Stelle eingebunden wird.

Beim Einsatz virtueller Pfade ist es also unabdingbar, den Aufbau des Include-Pfads zu kontrollieren. Das stellt eine Voraussetzung dar, die zwar nicht schwierig zu erfüllen ist, die aber in vielen Fällen den Einsatz einer Anwendung eher verkomplizieren als erleichtern dürfte.

Die „richtige“ Pfadangabe bei Includes

In diesem Abschnitt wird ein Vorschlag für eine möglichst optimale Lösung für Pfadangaben für Includes in Standardanwendungen formuliert. Ungeachtet dessen ist die geschickteste Art der Pfadangabe jedoch immer diejenige, die für den konkreten Anwendungsfall am besten geeignet ist.

Die These ist, dass eine Pfadangabe umso robuster ist, je weniger sie von externer Konfiguration abhängt. Durch diese Verringerung globaler Abhängigkeiten wird Quellcode leichter wiederverwendbar und einfacher austauschbar. Die beiden wesentlichen externen Faktoren, die Pfadangaben bei Includes beeinflussen, sind der Include-Pfad und das Arbeitsverzeichnis.

Der Include-Pfad und virtuelle Pfade sind sicherlich ohnehin aus der Mode gekommen. Sie dienen vor allem dazu, Abhängigkeiten von Klassen aufzulösen. Ein Beispiel aus einer PEAR-Klasse (HTML_BBCodeParser, Version 1.2.3):

require_once 'HTML/BBCodeParser/Filter.php';
 
class HTML_BBCodeParser_Filter_Basic extends HTML_BBCodeParser_Filter
{
    /* ... */
}

Beim Laden der Filter-Klasse wird – sofern noch nicht geschehen – die Elternklasse ebenfalls als Include mit eingebunden. Das setzt voraus, dass das entsprechende Include-Basisverzeichnis (hier das von PEAR) im Include-Pfad enthalten ist. Diese Funktionalität wird heute zumeist von Autoloadern übernommen, wodurch die Abhängigkeit zu einem bestimmten Eintrag im Include-Pfad wegfällt.

Für die wenigen verbliebenen Include-Zeilen ist es üblicherweise empfehlenswert, Pfadangaben ausgehend vom aktuellen Skriptverzeichnis mit __DIR__ zu bilden, um die Anwendung weiterhin von der Notwendigkeit eines korrekt gesetzten Arbeitsverzeichnisses unabhängig zu machen.

Dazu zwei Beispiele:

Eine Formulierung der Pfadangaben beginnend mit __DIR__ löst derlei Probleme. Der Code wird sowohl vom Include-Pfad als auch vom Arbeitsverzeichnis unabhängig, da die Pfadangabe, die an den Include-Mechanismus übergeben wird, bereits absolut ist.

Verweise: