Vorwort
Mit PHP 5, das mittlerweile bereits 3 Jahre alt ist, haben auch viele neue Funktionen Einzug in den Programmierer Alltag gehalten. Einige der neuen Funktionen haben auf Anhieb den Weg in's Programmierer Herz gefunden, während andere Neuerungen nach wie vor unbeachtet bleiben.
Eine dieser genialen, aber wenig beachteten Neuerungen ist die Standard PHP Library kurz SPL. Mit wenigen Worten zu beschreiben was die SPL ist, ist nicht einfach. Prinzipiell versucht die SPL, wie es der Name schon vermuten läßt, für gewisse wiederkehrende Probleme einen einheitlichen Lösungs-Standard zu schaffen.
Der Entwickler der SPL beschreibt es auf seiner Seite so:
SPL - Standard PHP Library SPL is a collection of interfaces and classes that are meant to solve standard problems and implements some efficient data access interfaces and classes.
In dieser SPL-Einführung wollen wir als Anschauungsobjekt eine relativ primitive Kernfunktion eines Datei-Backup Scripts erstellen das Verzeichnisse rekursive ausliest, bestimmte Datei-Typen herausfiltert und dann, unter Beibehaltung der Verzeichnisstruktur, ein Zip-Archiv der Dateien erstellt. Bei dieser Aufgabe lassen wir uns von den Iterator-Klassen der SPL helfen. Das hört sich im ersten Moment vielleicht schwierig und kompliziert an, aber wie wir sehen werden ist das, dank der SPL, (fast) so einfach wie eine Email per PHP zu versenden!
Doch bevor wir anfangen sollte sich jeder der noch keine Ahnung hat was die SPL ist einen ersten Überblick verschaffen:
Standard PHP Library Homepage
Sollte man nach diesem Artikel auf den Geschmack gekommen sein, so sollte dieser Link in die Bookmarks aufgenommen werden. Leider ist die SPL (noch) sehr schlecht dokumentiert auf der PHP-Webseite und vieles findet man nur durch trial&error heraus. Die Übersicht auf der Entwicklerseite bietet noch den besten Überblick was mit der SPL möglich ist.
Voraussetzungen
Dieses Einführung richtet sich an fortgeschrittene PHP Programmierer. Dementsprechend sollten folgende Voraussetzungen erfüllt sein um diesem Artikel folgen zu können:- fundierte PHP Kenntnisse
- ein gutes Verständnis der OOP
- Webserver oder Xampp mit PHP 5 - optimalerweise das aktuelle PHP 5.2.3
- aktivierte SPL und ZipArchive Klasse (sollte standardmäßig aktiv sein)
Um zu prüfen ob die benötigten Klassen zur Verfügung stehen einfach eine PHP Datei mit dem Inhalt
PHP-Code:
echo "<pre>" .print_r( get_declared_classes(), true ). "</pre>";
auf dem Server aufrufen und schauen ob folgende Klassen-Namen aufgeführt sind.- ArrayObject
- ArrayIterator
- RecursiveDirectoryIterator
- RecursiveIteratorIterator
- FilterIterator
- ZipArchive
Sind alle Voraussetzungen erfüllt kann es los gehen. Die Erklärungen in dieser Einleitung fallen etwas knapper aus, als es in meinen anderen Tutorials der Fall ist. Grund dafür ist, daß der Artikel auch ohne detaillierte Erläuterungen recht lang ist und darüber hinaus ist der Stoff nicht schwer zu verstehen, wenn man die oben erwähnten Voraussetzungen mitbringt.
Was ist ein Iterator?
Der Ausdruck Iterator ist jetzt schon häufiger gefallen, deswegen sollten wir vielleicht erst einmal klären was ein Iterator überhaupt ist. Am besten trifft wohl die Bezeichnung Zeiger als Übersetzung zu. Wenn wir uns durch ein Objekt bewegen (iterate), verschieben wir dabei einen Zeiger (Iterator) innerhalb des Objekts um auf bestimmte Inhalte zuzugreifen. Dabei ist es uns egal wie die Struktur der Daten aussieht, alles was uns interessiert ist deren Inhalt. In allen untenstehenden Beispielen werde ich den Ausdruck Zeiger verwenden, damit man die Funktionsweise des Iterator besser nachvollziehen kann.
Unser erstes Iterator-Beispiel
Das klassische Anwendungsgebiet für Iteratoren sind Arrays. Also erstellen wir ein kleines Array, leiten ein Objekt davon ab und schauen uns an was wir damit anstellen können.
PHP-Code:
$webtechnologien = array( "PHP", "HTML", "CSS", "MySQL", "Ruby", "JavaScript", "Flash" );
$objekt = new ArrayObject( $webtechnologien );
$zeiger = $objekt->getIterator();
Unser Array habe ich jetzt mal webtechnologien genannt, unabhängig davon ob jemand mit dem Inhalt des Array nicht einverstanden ist.
In Zeile 2 sprechen wir bereits die Klasse ArrayObject an und übergeben unser Array an diese Klasse. Das zurück gelieferte Objekt legen wir in $objekt ab. Die Klasse ArrayObject stellt zahlreiche Methoden zur Verfügung, die uns den Umgang mit einem Array erleichtert. So kann man z.B. auf zahlreiche klassische Sortier-Funktionen zugreifen, wie etwa- $objekt->asort()
- $objekt->ksort()
- $objekt->uasort()
- $objekt->uksort()
- $objekt->natsort()
- $objekt->natcasesort()
Eine weitere Methode die man häufig braucht ist $objekt->count(), die einem die Anzahl der im Array befindlichen Elemente liefert. Benötigt wird das u.a. um zu wissen wie viele Schleifen-Durchläufe wir machen müssen, wie wir weiter unten noch sehen werden.
Zusammen mit der Klasse ArrayObject werden automatisch weitere Klassen geladen die ihrerseits Methoden zur Verfügung stellt. Eine dieser Klassen ist die Iterator Klasse und bildet mit ihren Methoden den Kern des Ganzen. Wer sich mal "kurz" einen kleinen Überblick verschaffen möchte in welchem Zusammenhang die Iterator Klasse zu den anderen Klassen steht, dem möchte ich dieses niedliche Diagramm an's Herz legen.
Aber schauen wir uns mal die Methoden der Iterator Klasse an:- current() - Beinhaltet den Wert des aktuellen Elements auf dem sich der Zeiger befindet
- key() - Beinhaltet den Schlüssel des aktuellen Elements auf dem sich der Zeiger befindet
- next() - Bewegt den Zeiger zum nächsten Element
- rewind() - Bewegt den Zeiger an den Anfang des Objekts/Arrays
- valid() - Prüft ob ein Element existiert, nachdem mit next() oder rewind() die Zeigerposition verändert wurde.
In unserem Beispiel oben ermitteln wir also mit $zeiger = $objekt->getIterator(); die Position des aktuellen Elements und übergeben diese an unseren Zeiger. Ab sofort stellt unser Zeiger ($zeiger) ein Objekt dar und wir können mit den Methoden der Klassen ArrayObject und Iterator arbeiten. Machen wir das doch einfach mal, um das etwas zu verdeutlichen:
PHP-Code:
$webtechnologien = array( "PHP", "HTML", "CSS", "MySQL", "Ruby", "JavaScript", "Flash" );
$objekt = new ArrayObject( $webtechnologien );
$zeiger = $objekt->getIterator();
$zeiger->rewind(); // setzt den Zeiger an die erste Position im Array
echo $zeiger->count(). "<br>"; // gibt 7 aus, da 7 Elemente in unserem Array sind
echo $zeiger->current(). "<br>"; // gibt PHP aus, da sich der Zeiger auf dem ersten Element befindet
$zeiger->next(); // bewegt den Zeiger zur nächsten Position
if ($zeiger->valid()) // prüft ob sich der Zeiger auf einem gueltigen Element befindet.
{
echo $zeiger->key(). "<br>"; // gibt 1 aus, da sich der Zeiger auf dem 2. Element befindet
}
Soweit mal die absoluten Basics. Jeder mit etwas PHP Erfahrung wird hier nicht wirklich etwas neues gesehen haben, abgesehen davon das wir ein Array objektorientiert angesprochen haben. Einzig rewind() ist vielleicht etwas neu, aber die Funktionsweise entspricht der von reset(), die hinlänglich bekannt sein sollte.
Array Iteration in der Praxis
Ist ja alles schön und gut, nur wühlt sich niemand manuell durch ein Array, deswegen sollten wir uns einmal anschauen welche Möglichkeiten wir haben ein Array mit einer Schleife auszugeben. Zu dem Zweck basteln wir uns erst mal ein neues, assoziatives Array mit einigen fiktiven Angaben und erzeugen das benötigte Objekt.
PHP-Code:
$person = array( "Vorname" => "Max",
"Nachname" => "Mustermann",
"Alter" => 39,
"Haarfarbe" => "Braun",
"Beruf" => "Rauhhaardackelzüchter" );
$PersonObjekt = new ArrayObject( $person );
$zeiger = $PersonObjekt->getIterator();
Soweit gibt's hier nichts neues zu sehen. Schauen wir uns jetzt an, wie wir an unsere Daten kommen.
Als foreach()-Schleife
PHP-Code:
$zeiger->rewind();
foreach ($zeiger as $schluessel => $wert)
{
echo $schluessel. ": " .$wert. "<br>";
}
Als for()-Schleife
PHP-Code:
for ($zeiger->rewind(); $zeiger->valid(); $zeiger->next())
{
echo $zeiger->key(). ": " .$zeiger->current(). "<br>";
}
Als while()-Schleife
PHP-Code:
$zeiger->rewind();
while ($zeiger->valid())
{
echo $zeiger->key(). ": " .$zeiger->current(). "<br>";
$zeiger->next();
}
Wie wir sehen bleibt sich bei foreach() alles beim alten. Kein Wunder, ist doch foreach() eigentlich die Ausgabemethode für Arrays.
Bei der for()-Schleife setzen wir den Zeiger auf den Array-Anfang ($zeiger->rewind()) zurück und übernehmen diese Position als Startwert für die Schleife. Als Bedingung benutzen wir hier $zeiger->valid() um zu prüfen ob die aktuelle Zeigerposition auf ein valides Array-Element zeigt. Mit $zeiger->next() legen wir den Intervall fest. Die Ausgabe ist dann Element-Schlüssel ($zeiger->key()) und der Element-Wert ($zeiger->current()).
Bei der while()-Schleife müssen wir etwas aufpassen, damit wir nicht durch Nachlässigkeit eine Endlosschleife produzieren. Hier wird nämlich als Bedingung geprüft: solange $zeiger->valid() True ist, führe die Schleife aus. Deswegen ist es zwingend notwendig, daß wir innerhalb der Schleifen den Zeiger immer um eine Position nach vorn schieben ($zeiger->next()), wenn die Ausgabe erfolgt ist. Nur so können wie sicherstellen das die Bedingung irgendwann False ist - nämlich dann, wenn wir an der letzten Position im Array angekommen sind und den Zeiger "über das Ende hinaus" schieben.
Alles bis hierhin war (hoffentlich) einfach nachvollziehbar. Zugegeben, einen wirklichen Durchbruch gegenüber der prozeduralen Herangehensweise ist noch nicht so ganz zu erkennen. Allerdings ist es für komplexere Aufgaben zwingend erforderlich das man den Umgang mit den Basisfunktionen beherrscht. Denn ganz gleich welche Iterator Klassen aus der SPL wir benutzen, in allen wird auf eine sehr ähnliche, bzw. identische Weise navigiert. Schauen wir uns nun ein mehr praxisbezogenes Beispiel an.
Mit Verzeichnissen arbeiten - DirectoryIterator
Was für Arrays die ArrayIterator Klasse ist, ist für Verzeichnisse die DirectoryIterator Klasse. Diese Klasse stellt Methoden zu Verfügung mit der wir ohne großen Aufwand gängige Verzeichnis-Aktionen ausführen können. Schauen wir uns im Schnelldurchlauf mal die wichtigsten Methoden an, die weitestgehend selbsterklärend sind. Neben den üblichen 5 Verdächtigen der Iterator Klasse (siehe oben) stehen weiterhin zur Verfügung:- getFilename() - Der Dateiname ohne Pfad
- getMTime() - Datum der letzten Modifikation
- getPath() - Der Pfad zu einer Datei
- getRealPath() - Der absolute Pfad zu einer Datei
- getSize() - Dateigröße
- isDir() - Gibt True zurück, wenn der Name ein Verzeichnis ist
- isDot() - Gibt True zurück, wenn "." oder ".." gefunden wird
- isFile() - Ist True, wenn es sich um eine Datei handelt
- isReadable() - Ist True, wenn eine Datei lesbar ist
- isWritable() - Ist True, wenn eine Datei schreibbar ist
Die DirectoryIterator Klasse umfasst etwa dreimal soviele Methoden wie die hier gezeigten, aber das sind meiner Meinung nach die wichtigsten.
Das Auslesen eines Verzeichnis ist sehr einfach und mit wenigen Zeilen Code erledigt.
PHP-Code:
$verzeichnis = new DirectoryIterator( "testdateien/" );
while ($verzeichnis->valid())
{
echo $verzeichnis->current(). " (" .$verzeichnis->getSize(). ")<br>";
$verzeichnis->next();
}
Wir erzeugen ein Objekt von der DirectoyIterator Klasse und übergeben dabei den Verzeichnisname dessen Inhalt wir ausgeben lassen möchten. Die Struktur der while()-Schleife sollte nun bekannt sein. Zusätzlich lassen wir uns noch zu jeder Datei die Größe ausgeben, was für uns die Methode getSize() der DirectoyIterator Klasse übernimmt. Das Ergebnis sieht allerdings nicht sonderlich ansprechend aus, wie man im folgenden Listing sieht:
Code:
. (0)
.. (0)
bilder (0)
controller.php (4901)
includes (0)
m3_saz.jpg (72517)
mixed (0)
whois.php (21563)
whois.zip (3718)
Da sind Dateien mit Ordnern vermischt und auch "." ".." sehen nicht sonderlich gut aus. Wir bekommen ausserdem nur die Dateien aus dem angegebenen Ordner, was meistens wenig nützlich ist. Um herauszufinden was in den anderen Ordnern steckt müssen wir anfangen eine umständliche Funktion zu schreiben, die uns rekursive durch die Verzeichnisse navigieren läßt. Für diesen Zweck gibt es ja auch die nötigen Boardmittel wie isDot() oder auch isDir().
Aber muß es wirklich so kompliziert werden oder kommt uns da vielleicht wieder die SPL zu Hilfe? In der Tat wurde auch daran gedacht und die SPL stellt für das rekursive arbeiten die entsprechenden Klassen zur Verfügung. Um es genau zu sagen bietet die SPL zu jedem Iterator-Typ auch eine Recursive-Version der Klasse an. Wie praktisch das ist zeigt das nächste Beispiel...
Verzeichnisse rekursive auslesen mit der RecursiveDirectoryIterator Klasse
Die RecursiveDirectoryIterator Klasse bringt im Vergleich zur DirectoryIterator Klasse nur 2 neue Methoden mit, aber die sind überaus nützlich. Diese beiden Methoden sind:- hasChildren() - Liefert True, wenn ein "Kind" (Unterverzeichnis) vorhanden ist
- getChildren() - Liefert den Namen vom "Kind" (Unterverzeichnis)
Mit diesen beiden zusätzlichen Methoden können wir jetzt wirklich ganz bequem eine sehr übersichtliche Funktion erstellen, um rekursive durch Verzeichnisse zu navigieren. Here we go...
PHP-Code:
function VerzeichnisBaum( RecursiveDirectoryIterator $zeiger )
{
echo '<ul>';
for ($zeiger->rewind(); $zeiger->valid(); $zeiger->next())
{
if ($zeiger->isDir() && !$zeiger->isDot())
{
echo '<li><strong>' .$zeiger->getFilename(). '</strong></li>';
if ($zeiger->hasChildren())
{
$unterverzeichnis = $zeiger->getChildren();
echo '<ul>' . VerzeichnisBaum( $unterverzeichnis ) . '</ul>';
}
} elseif ($zeiger->isFile())
{
echo '<li><em>'. $zeiger->getFilename() . '</em></li>';
}
}
echo '</ul>';
}
VerzeichnisBaum( new RecursiveDirectoryIterator( 'testdateien/' ) );
Die Basisversion dieser Funktion habe ich aus dem Buch "PHP 5 - Grundlagen und Profiwissen von Jörg Krause". Allerdings habe ich die Funktion etwas abgewandelt um sie verständlicher zu machen.
Die Funktionsweise ist einfach und weitestgehend selbsterklärend. Beim Funktionsaufruf (ganz unten) wird das Objekt der RecursiveDirectoryIterator Klasse übergeben. Der Klasse selbst geben wir nur den Pfad und Verzeichnisname mit den wir durchlaufen möchten.
Wie in der Funktion zu sehen ist prüfen wir ob der Zeiger auf einem Verzeichnis steht ($zeiger->isDir()) und geben den Element-Name ($zeiger->getFilename()) fett aus. Anschließend wird geprüft (mit $zeiger->hasChildren()) ob das Element (Verzeichnis) ein Kind (Unterverzeichnis) hat - trifft das zu, lesen wir den Namen des Kindes aus ($zeiger->getChildren()) und übergeben diesen Namen an die Funktion, die sich selbst mit dem Unterverzeichnisname aufruft. Am besten zwei oder drei mal diese Funktion durchlesen und nachvollziehen um das Prinzip zu verinnerlichen. Viel einfacher kann man Verzeichnisse nicht rekursive auslesen, oder?! Hmm, oder vielleicht doch!?!