Websiteentwicklung: PHP: Autoloading

Aus Wikibooks

Autoloading[Bearbeiten]

Mit einer wachsenden Anzahl an Klassen werden die Aufrufe von require_once immer häufiger und über das gesamte Projekt verteilt. So brauchen wir, um eine Welt, eine Kastanie und einen Helden zu Erzeugen, diesen Code:

<?php

require_once 'Kastanie.php';
require_once 'Welt.php';
require_once 'Foo.php';

$baum = new Kastanie();
$welt = new Welt($baum);
$held = new Foo();

Zudem muss in der Datei "Kastanie.php" auch ein require_once auf die Elternklasse Baum erfolgen. Wir verstoßen damit aber gegen das Prinzip der Losen Kopplung: Wenn wir jetzt alle unsere Klassen in ein Verzeichnis classes verschieben (was sinnvoll ist), müssen wir alle unsere Pfade in allen Dateien anpassen. Es wäre doch praktischer, wenn wir PHP mitteilen könnten, sich selbst die Klassendatei zu suchen, wenn die Klassendefinition noch nicht geladen wurde. Genau das funktioniert mittels Autoloading.

Autoload-Funktion[Bearbeiten]

PHP benötigt dafür eine Autoload-Funktion. Diese Funktion hat einen Parameter, die den kompletten Klassen- oder Interfacenamen (inklusive eventuell aufgelöster Namespaces) entgegen nimmt. PHP ruft dann diese Funktion auf, wenn eine Klasse mit noch nicht vorhandener Definition angefordert wird, z.B. durch Erzeugung eines neuen Objektes dieser Klasse oder durch Zugriff auf eine Klassenkonstante.

Der alte Weg[Bearbeiten]

Einen solchen Autoloader kann mit der magischen Funktion __autoload() bereitstellen.

<?php

function __autoload($klassenname) {
  $file = 'classes/' . $klassenname . '.php';
  
  if (file_exists($file)) {
    /**
    * Wer ausschließlich Autoloading verwendet, kann auf "_once" verzichten,
    * da die Funktion nur einmal pro Klasse aufgerufen wird.
    */
    require_once $file;
  } else {
    return false;
  }
}

$held = new Foo();
$baum = new Kastanie();

//...

PHP wird nun bei der Erzeugung von "Foo" automatisch die Funktion __autoload aufrufen. Diese bindet dann die Datei 'classes/Foo.php', falls existent, ein. Nun wird PHP nochmal versuchen, ein Objekt der Klasse "Foo" zu erzeugen. Gelingt dies immer noch nicht, wird ein Standardfehler angezeigt, dass die Klasse nicht gefunden wurde.

Einen großen Nachteil hat die Verwendung von __autoload: Es kann immer nur eine einzige Funktion mit dem Namen __autoload geben. Man kann damit also keine fremden Pakete verwenden, die ihrerseits ein __autoload beinhalten.

spl_autoload_register (PHP-Version >= 5.1.2)[Bearbeiten]

Die Standard PHP Library (SPL) bietet die Möglichkeit, mehrere Autoloader in einem Stapel zu registrieren. Diese werden dann einer nach dem anderen abgearbeitet, bis die Klasse gefunden oder das Ende des Stapels erreicht wurde.

Obwohl es möglich ist, __autoload selbst zu registrieren, sollte man besser eine anders benannte Funktion verwenden. Das kann, wegen des callback-Typs, auch eine Objekt- oder statische Methode sein:

<?php
// Datei: classes/fantasyAutoloader.php

class fantasyAutoloader {

  public static function load($klassenname) {
    $file = __DIR__ . DIRECTORY_SEPARATOR . $klassenname . '.php';
  
    if (file_exists($file)) 
    {
      require_once $file;
    } 
    else 
    {
      return false; 
    }
  }
}
<?php
// Datei: index.php

require_once 'classes/fantasyAutoloader.php';

spl_autoload_register(array('fantasyAutoloader', 'load'));

$held = new Foo();

//...

Somit können andere Pakete ebenfalls ihre Autoloader mit spl_autoload_register registrieren.

Lösungen für Verzeichnisbäume[Bearbeiten]

Der oben gezeigte Autoloader funktioniert – solange sich alle Klassen im Verzeichnis "classes" befinden und als Dateinamen den Klassennamen tragen. Es ist jedoch bei großen Projekten nicht unüblich, dass 100 und mehr Klassen geschrieben werden. Diese Klassen möchte man jedoch in eine saubere Verzeichnisstruktur einordnen. So könnten wir alle unsere Baumklassen in das Verzeichnis "classes/Baum/", alle Heldenklassen in das Verzeichnis "classes/Person/Held/" usw. ablegen. Wie können wir aber nun dem Autoloader mitteilen, dass er genau in diesem Verzeichnis nach der Klasse "Foo" suchen soll? Keine Lösung wäre es, bei jedem Autoloading alle Verzeichnisse zu durchsuchen, denn Dateisystemoperationen sind ressourcenintensiv. Mit der Zeit haben sich verschiedene Lösungen etabliert, die hier kurz vorgestellt werden:

Pfad im Klassennamen[Bearbeiten]

Das ist die am weitesten verbreitete Lösung (sie wird beispielsweise vom Zend-Framework verwendet). Alle Klassen tragen ihren Pfad, durch Unterstriche getrennt, im Klassennamen. Unser Held hätte dann folgende Definition (vorausgesetzt, er erbt von einer abstrakten Heldenklasse):

<?php

class Person_Held_Foo extends Person_Held_Abstract {

//...

}

Dementsprechend funktioniert der Autoloader so:

<?php

function pfadImKlassennamen($klassenname) {
  $ordner = explode('_', $klassenname);
  $file = 'classes/' . implode('/', $ordner) . '.php';

  if (file_exists($file)) {
    require_once $file;
  } else {
    return false;
  }
}

Diese Methode hat allerdings auch einen Nachteil: Die Klassennamen wachsen zu unnatürlichen, schwer lesbaren Gebilden heran. Spätestens bei "Service_HTTP_Request_POST_Parameter_Option" sollte man diese Lösung überdenken.

Pfad im Namespace[Bearbeiten]

Mit der Version 5.3 wurden in PHP die Namespaces eingeführt. Sie verhindern Überschneidungen in der Benennung von Klassen und bieten sich durch ihre Pfadstruktur geradezu für das Autoloading an:

<?php

namespace org\wikibooks\de\PHP;

function pfadImNamespace($klassenname) {
  if (0 !== strpos($klassenname, __NAMESPACE__)) {
    // Dieser Autoloader ist für diesen Namespace nicht zuständig
    return false;
  }

  $sub_namespace = substr($klassenname, strlen(__NAMESPACE__));

  $teile = explode('\\', $sub_namespace);
  
  $file = implode(DIRECTORY_SEPARATOR, $teile) . '.php';

  if (file_exists($file)) {
    require_once $file;
  } else {
    return false;
  }
}

Generierte Liste der Klassen[Bearbeiten]

Die beiden oben vorgestellten Lösungen erfordern wieder eine genaue Kenntnis der Verzeichnisstruktur, da sie entweder im Namespace oder im Klassennamen abgebildet wird. Verschiebt man eine Klasse, muss man diese in jeder Verwendung umbenennen. Dieser Nutzen von Autoloading geht somit wieder verloren.

Eine weitere Lösung besteht darin, die Pfade aller Klassen und Interfaces im Projekt in einem Array zu speichern:

<?php

function generierterAutoloader($klassenname) {
  static $klassen = array ('Foo' => 'classes/Helden/Foo.php',
                           'Kastanie' => 'classes/Baeume/Kastanie.php',
                           'AlternativerDateiname' => 'classes/alternativ.class.php');

  if (!isset($klassen[$klassenname])) return false;

  require_once $klassen[$klassenname];
}

Das Schlüsselwort static sorgt dafür, dass das Array, unabhängig von der Zahl der Funktionsaufrufe, nur einmal pro Aufruf erzeugt wird, und das bereits bei der Kompilierung, was einen zusätzlichen Geschwindigkeitsvorteil bedeutet.

Die Pfade zu den Klassen muss man nicht von Hand eintragen. Es bietet sich an, ein Skript zu schreiben, das ein gegebenes Verzeichnis nach Klassen und Interfaces durchsucht und einträgt. Wenn du magst, kannst du das als Übung einmal versuchen. Du brauchst Verzeichnisfunktionen, Rekursion und die Funktion token_get_all. Alternativ kannst du auf eine der zahlreichen fertigen Umsetzungen zurückgreifen.

Der Vorteil dieser Methode ist die Rückkehr zu verständlichen Klassennamen bzw. die Unabhängigkeit von Namespace und Pfad, Nachteil der zwingende Aufruf des Generators für neue Klassen und Interfaces.

PHP-Dokumentation[Bearbeiten]

Folgende Abschnitte der PHP-Dokumentation wurden in diesem Kapitel behandelt: