| SPL - Die Standard PHP Library |
VorwortMit 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. 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. VoraussetzungenDieses Einführung richtet sich an fortgeschrittene PHP Programmierer. Dementsprechend sollten folgende Voraussetzungen erfüllt sein um diesem Artikel folgen zu können:
Um zu prüfen ob die benötigten Klassen zur Verfügung stehen einfach eine PHP Datei mit dem Inhalt echo "<pre>" .print_r( get_declared_classes(), true ). "</pre>"; auf dem Server aufrufen und schauen ob folgende Klassen-Namen aufgeführt sind.
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-BeispielDas 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.
$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.
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. Aber schauen wir uns mal die Methoden der Iterator Klasse an:
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: $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 $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 $zeiger->rewind(); foreach ($zeiger as $schluessel => $wert) { echo $schluessel. ": " .$wert. "<br />"; } Als for()-Schleife for ($zeiger->rewind(); $zeiger->valid(); $zeiger->next()) { echo $zeiger->key(). ": " .$zeiger->current(). "<br />"; } Als while()-Schleife $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 - DirectoryIteratorWas 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:
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. $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: . (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(). 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:
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... function VerzeichnisBaum( RecursiveDirectoryIterator $zeiger ) { echo '<ul>'; for ($zeiger->rewind(); $zeiger->valid(); $zeiger->next()) { if ($zeiger->isDir() && !$zeiger->isDot()) { echo '<li><span class="hinweis">' .$zeiger->getFilename(). '</span></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. Zip-Dateien on-the-fly erstellenBevor wir mit den Iterator-Klassen weiter machen, wollen wir einen kurzen Abstecher zu einer anderen, sehr nützlichen, neuen Klasse in PHP 5 machen: ZipArchive Mit der ZipArchive Klasse kann man auf eine wirklich sehr einfache Art und Weise Zip Dateien erstellen, verändern und auch wieder entpacken. Das erforderte früher mehr oder weniger komplizierte externe Klassen mit sehr viel Code, in denen nur die wenigsten verstanden was da eigentlich vor sich geht. Das ändert sich grundlegend mit PHP 5. Die Methoden dieser Klasse sind sehr intuitiv und einfach zu verstehen; hier ein Überblick der wichtigsten Methoden:
Das ist nur ein kleiner Teil der Methoden der ZipArchive Klasse. Desweiteren stehen Methoden zur Verfügung um Kommentare anzufügen/auszulesen, Index und Namen zu verändern, usw. Schauen wir uns ein sehr einfaches Beispiel an, wie man mit sehr wenigen Zeilen Code eine Zip-Datei erstellen und auch wieder entpacken kann. Das kann sehr nützlich sein um Backups zu erstellen und wieder herzustellen. Das Listing um eine neue Zip-Datei zu erstellen sieht wie folgt aus: $zip = new ZipArchive; $resource = $zip->open( 'testarchiv.zip', ZipArchive::CREATE ); if ($resource === TRUE) { $zip->addFromString( 'test.txt', 'Hier steht ein beliebiger Text.' ); $zip->addFile( 'testdateien/controller.php', 'config/controller.php' ); $zip->addFile( 'testdateien/m3_saz.jpg', 'bilder/blindschleiche.jpg' ); $zip->addFile( 'testdateien/whois.php', 'whois.php' ); $zip->addFile( 'testdateien/whois.zip', 'whois.zip' ); $zip->close(); } In Zeile 1 erzeugen wir eine neue Instanz der ZipArchive Klasse. Anschliessend erstellen wir mit der Zeile $resource = $zip->open( 'testarchiv.zip', ZipArchive::CREATE ); eine neue Zip-Datei und merken uns den Status (True oder False) von dem Vorgang. Wichtig hier ist der 2. Parameter in $zip->open(). Durch den Zusatz ZipArchive::CREATE teilen wir der Klasse mit, daß wir eine neue Zip-Datei erstellen möchten. Ohne diesen Zusatz würde die Klasse versuchen die Datei testarchiv.zip zu öffnen, was unweigerlich in einem Fehler endet, da diese Datei nicht existiert. Möchten wir hingegen eine bereits existierende Zip-Datei öffnen, weil wir z.B. Datei hinzufügen wollen, muß der Zusatz weggelassen werden, da sonst die Zip-Datei überschrieben wird! Nachdem geprüft wurde ob die Datei erfolgreich angelegt wurde, fügen wird Dateien zu unserem Zip-Archiv hinzu. Als ersten benutzen wir $zip->addFromString() um eine Datei aus einer Variablen anzufügen. Wie wir sehen bestimmen wir als ersten Parameter den Dateinamen in der Zip-Datei und als 2. Parameter übergeben wir den String, bzw die Variable die den String beinhaltet. Das entpacken einer Zip-Datei ist genauso einfach: $zip = new ZipArchive; if ($zip->open( 'testarchiv.zip' ) === TRUE) { $zip->extractTo( 'testdateien/' ); $zip->close(); } Als erstes wieder ein neues Objekt von der ZipArchive Klasse erzeugen. In der if() wird geprüft ob das öffnen ($zip->open()) der Zip-Datei erfolgreich war. (Kein 2. Parameter wie beim erstellen!) Dann entpackt man den Inhalt aus der testarchiv.zip mithilfe der Methode $zip->extractTo() in den Zielordner, was in unserem Fall testdateien ist. Die geöffnete Datei mit $zip->close() schliessen und das war's! Kinderleicht, nicht wahr?! Jetzt wo wir gesehen haben wie einfach es ist Zip-Dateien on-the-fly zu erstellen und wieder zu entpacken, kombinieren wir das mal mit den Fähigkeiten unseren DirectoryIterator Klassen. Mal eine kleine Zwischenfrage: Was glaubst Du wie viele Zeilen Code wir brauchen um uns mithilfe der neuen ZipArchive Klasse und den Iterator Klassen der SPL ein Backup Script zu schreiben das uns ein Datei-Backup von unserer -sagen wir mal- 6-fach verschachtelte Verzeichnisstruktur erstellt? 25? 35? 50? mehr?? Fast, es sind ziemlich genau 7 Zeilen! Die Kernfunktion unseres Datei-Backup ScriptsOk zugegeben, das mit den 7 Zeilen war nicht leicht zu erraten, weil dazu eine weitere Iterator Klasse erforderlich ist die wir uns noch gar nicht angeschaut haben. Ebenso wurde auch noch nicht erwähnt das man Objekte einer Iterator Klasse beim Instanziieren einer anderen Iterator Klasse an diese übergeben kann, was den Umgang mit den Klassen wesentlich erleichtert und Schreibarbeit spart. Das Listing des Backup Scripts zum erzeugen einer Zip Datei einer verschachtelten Verzeichnisstruktur: $verzeichnis = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( 'testdateien/' ), true ); $zip = new ZipArchive; $resource = $zip->open( 'backup.zip', ZipArchive::CREATE ); if ($resource === TRUE) { foreach ( $verzeichnis as $datei ) { $zip->addFile( $datei, $datei ); } } $zip->close(); Im Prinzip ist das ganze Listing weitestgehend identisch mit dem Zip-Archiv Beispiel von eben. Einzig die erste Zeile ist etwas anders im Vergleich zu den vorherigen Beispielen, aber hier geschieht die eigentliche Arbeit. Aufgrund der Länge der Zeile habe ich sie umgebrochen um sie übersichtlicher zu machen. Werfen wir mal einen genaueren Blick darauf... $verzeichnis = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( 'testdateien/' ), true ); Die innere Instanziierung der RecursiveDirectoryIterator Klasse ist nicht neu, das hatten wir weiter oben schon unter die Lupe genommen. Was hier jetzt aber neu ist, ist die Klasse RecursiveIteratorIterator der, bei dessen Instanziierung, das Object der RecursiveDirectoryIterator Klasse übergeben wird.
Das True beim Aufruf könnte man in diesem Fall auch weg lassen, da es für das erstellen der Zip-Datei nicht erforderlich ist. Wird beim Aufruf True angegeben, wird auch der Name des Kindes (Unterverzeichnis) ausgegeben. Das kann nützlich und sinnvoll sein wenn z.B. eine Ordnerstruktur auf dem Screen ausgeben werden soll. Schon beeindruckend wie einfach man so ein kleines Backup Script schreiben kann. Allerdings ist die Arbeitsweise doch etwas unbefriedigend. Das Script würde nämlich gnadenlos alles in die Zip-Datei packen was ihr auf dem Weg bei der rekursiven Iteration über den Weg läuft. Wirklich sinnvolle Backups kann man aber eigentlich nur dann machen, wenn wir bestimmte Dateien ausklammern könnten (z.B. htaccess Dateien, Log-Files, usw.) oder nur bestimmte Dateien in's Backup aufnehmen. Inhalte filtern mit der FilterIterator KlasseDie FilterIterator Klasse kommt verhältnismässig bescheiden daher, denn neben den 5 Standard Iterator Methoden verfügt diese Klasse nur über einen Konstruktor und diese Methoden:
Nichts desto Trotz ist der Einfluß dieser Klasse gewaltig, denn mit ihr können wir genau festlegen was gefiltert wird und was nicht. Um eine (Filter)Bedingung an die FilterIterator Klasse zu übergeben, müssen wir eine eigene Klasse schreiben und die FilterIterator Klasse erweitern. Das Listing der FilterIterator Klasse Erweiterung: class DateiFilter extends FilterIterator { private $erweiterung; private $zeiger; public function __construct( $iterator, $erweiterung ) { parent::__construct( $iterator ); $this->zeiger = $iterator; $this->erweiterung = $erweiterung; } public function accept() { if (!$this->zeiger->isDir()) { if (gettype( $this->erweiterung ) == "array") { $ArrayObjekt = new ArrayObject( $this->erweiterung ); $zeiger = $ArrayObjekt->getIterator(); $zeiger->rewind(); foreach ($zeiger as $schluessel => $wert) { $erweiterung = array_reverse( explode( '.', $this->current() ) ); if ($erweiterung[0] == $wert) $erweiterungen[] = $this->erweiterung; } return $erweiterungen; } else { $erweiterung = array_reverse( explode( '.', $this->current() ) ); return $erweiterung[0] == $this->erweiterung; } } return true; } } Da für diesen Artikel ein gutes Verständnis der OOP vorausgesetzt wird sollten hier eigentlich keine großen Erklärungen nötig sein. Der Konstruktor erwartet zwei Parameter: 1) die zu iterierende Resource und 2) die erlaubte Datei-Endung(en). Um den neuen Filter in Aktion zu sehen rufen wir unsere Klasse mit folgendem Code auf: $verzeichnis = new DateiFilter( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( 'testdateien/' ) ), 'jpg' ); foreach ($verzeichnis as $datei) { echo $datei. "<br />\n"; } Um mehrere Dateiendungen aufzulisten übergeben wir beim Aufruf ein Array: $verzeichnis = new DateiFilter( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( 'testdateien/' ) ), array( 'jpg', 'html' )); foreach ($verzeichnis as $datei) { echo $datei. "<br />\n"; } Die inneren Instanziierungen kennen wir vom letzten Beispiel. Das so erzeugte Objekt übergeben wir, zusammen mit der erlaubten Datei-Endung (jpg), an unsere DateiFilter Klasse. Anschliessend geben wir in einer einfachen foreach() sämtliche gefundene Dateien aus die auf jpg enden. Statt dem echo() in der foreach() könnten wir auch genauso gut, nachdem wir eine Zip-Datei geöffnet/angelegt haben, das $zip->addFile() dort stehen haben oder jede beliebige andere Aktion hier ausführen. Selbstverständlich könnten wir die Methode accept() auch erweitern, so daß z.B. mit $zeiger->getMTime() geprüft wird ob $zeiger->current() neuer oder älter als eine Datei im Zip-Archiv ist und auf diese Weise so etwas wie ein Inkrementelles Backup Script erzeugen. Fazit und SchlußwortAls erstes möchte ich im Schlußwort noch auf eine "Kleinigkeit" hinweisen. Während des ganzen Artikels sprach ich aus Gründen der Einfachheit, um den Lernenden nicht zu verwirren, von der Iterator "Klasse". In Wirklichkeit ist es aber ein Interface das in die anderen Iterator Klassen (wie z.B. DirectoryIterator) implementiert wird. Das ist dahingehend wichtig, als das man sich eigene Iterator Klassen erstellen kann und durch das implementieren des Iterator Interface auf die Iterations-Kernfunktionen zugreifen kann. Es ist also mehr als eine einfache Vererbung. Jetzt aber wieder zurück zum Thema... Dieser Artikel war sehr lang, deswegen möchte ich auf weitere Worte verzichten. Im Anschluß gibt es noch einige Tipps um der SPL wichtige Informationen zu entlocken. Tipps zur SPLDie erste Anlaufstelle um an Informationen zu kommen und um die Zusammenhänge der einzelnen Klassen zueinander zu verstehen ist die Webseite des Entwicklers. Um festzustellen welche Klassen mit PHP installiert wurden einfach eine PHP Datei mit folgenden Inhalt auf dem Server ausführen. echo "<pre>" .print_r( get_declared_classes(), true ). "</pre>"; Alle Methoden einer Klasse ermitteln: echo "<pre>" .print_r( get_class_methods( "DirectoryIterator" ), true ). "</pre>"; Ich hoffe dieser Artikel konnte den ein oder anderen dazu ermutigen sich etwas mit dieser tollen Erweiterung von PHP 5 zu beschäftigen und wünsche Euch allen ein fröhliches iterieren ;) |