PHP-Tutorial: Bilder-Archiv
mados

Als mir ein Ferienjob angeboten wurde, bei dem es um PHP ging, nahm ich diesen spontan an, obwohl ich zu diesem Zeitpunkt noch keine einzige Zeile dieser serverseitigen Skriptsprache angefasst hatte. Ich kannte nur Perl und dachte mir, das kann doch nicht so viel anders sein. War es auch nicht, im Gegenteil. PHP ist für mich zur wichtigsten Skriptsprache überhaupt geworden. Anhand eines konkreten, funktionierenden Beispieles möchte ich an dieser Stelle Techniken erläutern, Tricks und Kniffe aufzeigen und einfach das Interesse an PHP wecken.

An erster Stelle stand die Auswahl eines sinnvollen Beispieles. Ein Counter? Selbst mit einer eingebauten Reload-Sperre wäre ein solches Skript nicht einmal ein Dutzend Codezeilen lang. Ein Gästebuch? Die gibt es im Netz bereits tausendfach, und auch sonst sind Gästebücher im Allgemeinen ziemlich abgedroschen. Ich entschied mich schließlich für ein Foto- bzw. Bilder-Archiv. Dafür werden nicht nur die üblichen Formulare mit Texteingaben benötigt, es werden auch Dateien hochgeladen, Vorschaubilder generiert, und es müssen Daten gespeichert und verwaltet werden.

Dem Besucher soll sich das Ganze als Liste von Vorschaubildern präsentieren. Ein Klick auf eines der Bilder bringt es in Originalgröße und mit detailierteren Informationen auf den Bildschirm. Weiterhin soll es möglich sein, neue Bilder einfach über den Webbrowser hinzuzufügen, sowie die Informationen zu bereits vorhandenen Bildern zu editieren oder sie ganz zu löschen. Die Idee dabei ist, dass das Hochladen von Bildern im einfachsten Fall allen Besuchern der Webseite erlaubt sein soll, die administrativen Funktionen jedoch auf den Webmaster beschränkt sind.

Im Folgenden werden diverse Codezeilen für reichlich Verwirrung sorgen, was nicht unbedingt dem undurchsichtigen Programmierstil zu verschulden ist, den mir manch einer nachsagt. Vor allem die kunterbunte Mischung aus HTML- und PHP-Code ist anfangs sehr gewöhnungsbedürftig. Hat man sich jedoch daran gewöhnt, wird man gerade diese Eigenheit von PHP lieben und nutzen lernen.

Generell gilt, dass die Programme linear von vorn nach hinten abgearbeitet werden. Sobald die Zeichenkombination `<?php` auftaucht, wird in den PHP-Modus und mit `?>` wieder zurück in den HTML-Modus geschaltet. (Oft wird auch das kürzere aber nicht XML-konforme `<?` und `?>` benutzt.) Natürlich werde ich nicht jeden einzelnen der verwendeten Befehle erläutern. Es ist äußerst empfehlenswert, parallel zu diesem Text passende Nachschlagewerke bereit zu halten, zum Beispiel das offizielle PHP-Handbuch, das es unter www.php.net auch in einer komfortablen WinHelp-Version gibt. Beim HTML-lastigen Teil des Codes leistet wie immer SelfHTML gute Dienste.

1.) Dateiablage

Wie schon erwähnt, soll der Anwender selbst Bilddateien zum Archiv hinzufügen können. Der Übersichtlichkeit halber werde ich diese Dateien in einem Unterverzeichnis "bilder/" ablegen, die dazu gehörigen Thumbnails im Verzeichnis "bilder/vorschau/". Wichtig und interessant ist in diesem Zusammenhang die Frage der Dateirechte, die unter Unix/Linux wesentlich weitreichender als unter Windowssystemen ist. Aus Sicht des Webservers werden die Schreibvorgänge nämlich nicht von einer namentlich bekannten Person ausgeführt, sondern von einem Standarduser, der in der Regel "www" oder ähnlich heißt. Wir als Besitzer der Webseite müssen diesem User das Schreiben auf die normalerweise nur lesbare Datendatei und die beiden Verzeichnisse erst explizit erlauben, indem wir deren Zugriffsrechte verändern. Mein Beispielskript legt zu gegebener Zeit die erwähnten Verzeichnisse selbst an und setzt die Rechte einfach so, dass jedem alles erlaubt ist. Das ist in der Regel nicht zu empfehlen, an dieser Stelle jedoch das einfachste.

`mkdir('bilder', 0777);`

Die Bedeutung der oktalen Zahl kann unter dem Stichwort "chmod" in jedem Unix/Linux-Handbuch nachgeschlagen werden. Natürlich kann man auch schon vorher beide Verzeichnisse von Hand anlegen, darf dann jedoch nicht vergessen, sie zum Schreiben zu öffnen. Dies ist im FTP-Programm in der Regel sehr komfortabel möglich.

2.) Datenbank ohne Datenbank

Jedes Bild im Archiv besteht aus mehr als nur der Bilddatei selbst. Zu den textuellen Informationen gehört neben einem kurzen Titel auch der Name des Autors, der Zeitpunkt, zu dem das Bild hochgeladen wurde und ein längerer Beschreibungstext. Auch die Breiten und Höhen des Bildes und seiner Vorschau speichere ich ab, obwohl man diese Werte genauso gut aus den Bilddateien selbst extrahieren könnte. Vor allem aus Geschwindigkeitsgründen sollte so etwas jedoch vermieden werden, da die Arbeit mit Dateien zu den zeitraubendsten Aktionen in PHP zählt.

Die für das Bilderarchiv benötigte Tabelle würde also etwa so aussehen:

Spalte 0: Dateiname,
Spalte 1: Breite des Bildes,
Spalte 2: Höhe des Bildes,
Spalte 3: Breite des Vorschaubildes,
Spalte 4: Höhe des Vorschaubildes,
Spalte 5: Bildtitel,
Spalte 6: Beschreibungstext,
Spalte 7: Autor,
Spalte 8: Datum.

Jeder, der jetzt an eine Datenbank (zum Beispiel das weit verbreitete MySQL) denkt, denkt richtig. Leider steht nicht jedem ein Server mit einer Datenbank zur Verfügung, erst recht nicht, wenn das Ganze gratis sein soll. Obwohl PHP inzwischen selbst bei kostenlosen Anbietern sehr weit verbreitet ist, lässt man sich dieses I-Tüpfelchen auf der dynamischen Webprogrammierung nach wie vor teuer bezahlen. Aber es geht auch ohne - wir basteln uns unsere Datenbank einfach selbst.

Eine Textdatei bildet die Basis. (In Skriptsprachen arbeitet man eigentlich nie mit Binärdateien, wie es manch einer aus anderen Programmiersprachen vieleicht gewöhnt ist.) In meiner Textdatei repräsentiert jede Zeile einen Datensatz. Die Felder des Datensatzes sind durch Tabulatoren getrennt - ein Zeichen, das in den Feldern selbst praktisch nie vorkommt und zur Sicherheit auch explizit entfernt wird.

`preg_replace('/\t/', ' ', $beschr);`

Los geht es in allen Skripten mit dem Befehl `$bild = @file('bilder.txt')`, der die gesamte Datei in einem Rutsch einliest und ihren Inhalt zeilenweise im Array $bild ablegt, das ich der Einfachheit halber als globale Variable allen Prozeduren zur Verfügung stelle. Das "@" kann man übrigens auch weg lassen. Es sorgt lediglich dafür, dass eventuelle unschöne Fehlermeldungen unterdrückt werden. Die darauf folgende Schleife

    for ($i = 0; $i < count($bild); $i++)
    {
      $bild[$i] = explode("\t", chop($bild[$i]));
    }

arbeitet dieses Array nun zeilenweise ab, entfernt mit Hilfe von chop() den Zeilenumbruch an den Enden und splittet den Datensatz in seine Elemente auf. Aus dem eindimensionalen Array $bild wird somit ein zweidimensionales, wobei die erste Ebene quasi die Zeilen- und die zweite die Spaltennummer angibt (immer beginnend mit Null). Der Zugriff auf den Titel des dritten Bildes erfolgt also mit $bild[2][5]. Diese Methode gewinnt mit Sicherheit keinen Schönheitspreis. Ein Zugriff in der Art von $bild['beispiel.jpg']['titel'] wäre mit dieser Methode ebenso denkbar, jedoch ungleich aufwendiger.

Sobald sich an den Daten dieses zweidimensionalen Arrays etwas geändert hat, wird die Datei einfach komplett neu geschrieben.

    $zeiger = fopen('bilder.txt', 'w');
    for ($i = 0; $i < count($bild); $i++)
    {
      fputs($zeiger, implode("\t", $bild[$i]) . "\n");
    }
    fclose($zeiger);

Eine selektive Änderung einzelner Zellen kommt bei Textdateien in dieser Form nicht in Frage, so dass sämtliche vorgestellten Skripte tatsächlich darauf angewiesen sind, jedesmal den kompletten Datenbestand in den Arbeitsspeicher zu laden und bei Bedarf wieder zu speichern. Im Detail geschieht hier genau das Gegenteil des Lesevorganges. Zuerst werden die Elemente des Datensatzes mittels implode() miteinander verbunden, ein Zeilenumbruch angehangen und diese Zeile in die Datendatei geschrieben.

Eine wichtige Besonderheit ist der Befehl `flock($zeiger, 2)`, der in den Skripten direkt nach dem schreibenden fopen() auftaucht. Da es sich bei der ganzen Angelegenheit um ein Onlineskript handelt, kann es zumindest theoretisch vorkommen, dass zwei Anwender das Skript zur selben Zeit starten, und dass sich zwei Schreiboperationen zeitlich überlagern. Ein flock() verhindert dies zumnindest teilweise, indem es versucht, die Datei exclusiv für sich zu blockieren. Bemerkt flock(), dass eine solche Blockade bereits existiert, wartet es so lange, bis diese wieder aufgehoben ist. Der aufmerksame Leser wird schnell bemerken, dass dieser Schutz völlig unzureichend ist, auf die theoretischen Grundlagen konkurrierender Prozesse will ich an dieser Stelle jedoch nicht eingehen.

3.) Frontend

Das eigentliche Bilderarchiv - in meinem Beispiel repräsentiert durch die Datei bilder.php - ist interessanterweise das kleinste Skript. Das sollte nicht weiter verwundern, schließlich muss dieser Programmteil nichts weiter tun als die Datenbank mehr oder weniger eins zu eins auszugeben. Hier wird auch klar, warum ich die groben Funktionsblöcke auf mehrere Dateien verteilt habe. Es wäre ebenso möglich gewesen, alles in Form von Prozeduren in einer einzigen Datei zu kombinieren. Da PHP aber nach wie vor eine Skriptsprache ist, würde dabei jedesmal der gesamte Code abgearbeitet werden, selbst wenn die meisten Teile davon gar nicht gebraucht werden. Von lesbarem Code ganz zu schweigen.

Dennoch besteht das Skript bilder.php aus zwei im Grunde unabhängigen Teilen: Die Prozedur bilderarchiv() zeigt die Liste von Vorschaubildern, details() bringt schließlich ein angeklicktes Bild in Originalgröße auf den Bildschirm. Die Auswahl, welche der beiden Funktionen gerade erwünscht ist, wird anhand der Variablen $id am Ende des Skriptes getroffen. Im Normalfall ist sie leer, also werden die Thumbnails angezeigt. Jedes dieser kleinen Bilder wird mit einem eigenen Link versehen.

`<a href="bilder.php?id=3"><img ...></a>`

In der Regel ist es empfehlenswert, an solchen Stellen die vordefinierte Variable $PHP_SELF zu verwenden. Sie enthält den Namen des Skriptes selbst, in diesem Fall also "bilder.php". Der Vorteil besteht einfach darin, dass man die Datei nach Belieben umbenennen kann, ohne dass es plötzlich Fehlermeldung wegen falscher Referenzen hagelt.

`<a href="<?php echo $PHP_SELF; ?>?id=3"><img ...></a>`

Im Browser des Anwenders wird dieser Link natürlich wieder wie oben ankommen. Er klickt darauf und ruft damit die Datei bilder.php, in der er sich ja bereits befindet, noch einmal auf. Dieses Mal allerdings mit einem Parameter, nämlich einer Variablen mit dem Namen "id" und dem Inhalt "3". PHP leistet hier wieder wundervolle Vorarbeit, denn es stellt uns diese - in diesem Fall per GET-Methode - übertragene Variable automatisch zur Verfügung. In $id ist nun ein String enthalten, der die Nummer des anzuzeigenden Bildes enthält. Im Sonderfall des ersten Bildes kann dies auch der problematische String '0' sein.

Dazu muss ich kurz auf den Datentyp boolean eingehen, der in PHP recht eigen ist. Bei der (in der Regel vollautomatischen) Konvertierung eines Strings in einen Wahrheitswert werden folgende Werte als false (unwahr) interpretiert:

- Der Wahrheitswert false selbst,
- die Integerzahl 0,
- die Fließkommazahl 0.0,
- der leere String "" und der String "0",
- ein Array ohne Elemente,
- ein Objekt ohne Elemente
- und der spezielle Typ null.

Für alle anderen Werte wird true (wahr) angenommen.

Der Befehl `if ($id >= '0')` nutzt diese Gegebenheiten geschickt aus. Da auf beiden Seiten des Vergleiches Strings auftauchen, tritt die Typkonvertierung nicht in Kraft, die Seiten werden als Text verglichen und false ergibt sich nur noch, wenn die Variable $id gänzlich leer ist. Damit wird sichergestellt, dass auch das Bild mit der Nummer Null korrekt verarbeitet werden kann.

Eines der wenigen Extras des Skriptes sorgt dafür, dass immer nur maximal zehn Vorschaubilder gezeigt werden, durch die man dann seitenweise blättern kann. Realisiert wird das durch einige zusätzliche und modifizierte Codezeilen. Die zentrale Schleife

`for ($i = count($bild); $i >= 0; $i--) { ... }`

wird so abgeändert, dass mit einem durch die Seitennummer (wieder beginnend bei Null) bestimmten Offset begonnen wird, und die Schleife nach spätestens 10 Bildern abbricht. Übrigens wird hier nur deswegen rückwärts gezählt, weil die aktuellsten Bilder als erstes in der Liste erscheinen sollen. Das vereinfacht das Hinzufügen neuer Bilder - diese können einfach ans Ende der Datendatei angehängt werden.

4.) Upload

Noch haben wir nichts, was angezeigt werden kann. Mit Hilfe des Skriptes upload.php soll nun das Hochladen beliebiger Bilddateien möglich gemacht werden. Die zentralen Elemente dafür sind in sämtlichen Newsgruppen und Tutorials tausendfach beschrieben, so dass ich hier nur sehr grob darauf eingehen will. Im ersten Schritt bieten wir dem Anwender ein Formular an, das mit den Parametern `enctype="multipart/form-data"` und `method="post"` uploadfähig gemacht wird. Im zweiten Schritt ist die Datei dank PHP bereits auf dem Webserver gelandet, allerdings in einem temporären Verzeichnis, aus dem wir sie noch retten müssen.

`copy($neu, "bilder/$neu_name");`

(Hinweis: Wenn der soganennte Safe-Mode aktiviert ist, verursacht der Befehl copy() Fehlermeldungen. Er muss in diesem Fall durch move_uploaded_file() ersetzt werden, das aber erst ab PHP4 zur Verfügung steht.)

Die Temporäre Datei $neu wird unter ihrem korrekten Dateinamen $neu_name abgelegt. Um die Aktion abzuschließen und den neuen Datensatz speichern zu können, muss nun noch ein Thumbnail generiert werden.

5.) Thumbnails

Die Vorschaubilder, die wir jetzt erzeugen wollen, sind nicht umsonst auf nahezu jeder Internetseite zu finden. Sie erlauben einen schnellen Blick auf Fotos und Grafiken, ohne dazu dutzende Kilobytes herunter laden zu müssen. Genial wird die Idee aber erst dann, wenn diese Thumbnails vollautomatisch erzeugt werden. Innerhalb von PHP gibt es dafür zwei besonders weit verbreitete Möglichkeiten: Die GD-Bibliothek sowie das externe Programm ImageMagick.

Die GD-Library kann man nur dann nutzen, wenn das auf dem Server vorhandene PHP darüber verfügt. Herausfinden kann man das leicht mit einem Testskript, das nur aus der Zeile `<?php phpinfo(); ?>` besteht. Bei dieser Gelegenheit sollte man auch darauf achten, welche Bildformate unterstützt werden. In aktuellen Versionen ist GIF aus lizenzrechtlichen Gründen entfernt und durch PNG ersetzt worden. Die entsprechende Zeile im Upload-Skript sollte dementsprechend auskommentiert oder entfernt werden.

Das Skript berechnet im ersten Schritt aus den originalen Bildmaßen und der gewünschten Vorschaugröße eine Verhältniszahl, wobei diese maximal 1 sein kann. Eventuell kleinere Bilder werden also keinesfalls vergrößert.

`$verhaeltnis = min(160 / $groesse[0], 120 / $groesse[1], 1);`

Die Thumbnails sind hier auf maximal 160 mal 120 Pixel begrenzt, andere sinnvolle Größen wären beispielsweise 100 x 75 oder 200 x 150. Das Bild wird anschließend mit dem passenden ImageCreateFrom-Befehl eingelesen, und nach der eigentlichen Neuberechnung durch ImageCopyResized() kann das Vorschaubild gesichert werden. Da GIF oder PNG für derartige Bilder praktisch keinen Sinn machen, verwende ich für die Thumbnails generell das JPG-Format.

Eine wesentliche Erhöhung der Bildqualität wird durch die Befehle ImageCreateTrueColor() und ImageCopyResampled() erreicht, die aber leider erst ab PHP 4.0.6 und GD 2.0.1 zur Verfügung stehen. Das Skript übernimmt diese Überprüfung selbst und benutzt die Befehle nur dann, wenn das möglich ist.

Steht die GD-Bibliothek gar nicht zur Verfügung, dann hat man vieleicht Glück und auf dem fraglichen Webserver ist das Softwarepaket ImageMagick installiert.

`$convert = "/usr/X11R6/bin/convert -geometry 160x120\\> $neu $vorschauname";
exec($convert);`

Es handelt sich hier um das externe Programm convert, das als reines Kommandozeilen-Tool auch direkt unter Unix/Linux nutzbar ist. Aufgerufen wird es mit dem Parameter "-geometry" und der gewünschten neuen Bildgröße. Das angehängte ">" besagt, dass nur Bilder behandelt werden sollen, die größer sind als angegeben. Auf diese Weise verhindert man ähnlich wie bei der GD das Vergrößern besonders kleiner Bilder. Die letzten beiden Parameter geben schließlich den alten und neuen Dateinamen an.

6.) Administration

Dass der Besucher der Seite das besonders aufwendige Skript admin.php nie zu sehen bekommt, ist traurig, aber bei PHP im allgemeinen und bei sogenannten Content Managment Systemen im speziellen fast immer so. Im Prinzip sind hier noch einmal alle Funktionen der anderen Skripte vereint. Man kann durch die Liste der Bilder blättern, alle Textinformationen prüfen, bearbeiten und speichern oder auch einzelne Bilder komplett löschen. Die Struktur ist durch die Aufteilung in Funktionsgruppen relativ leicht verständlich.

Eine völlig neue Möglichkeit, die in den anderen beiden Skripten nicht existiert, ist das Verändern des Datums. Dies macht an dieser Stelle nicht unbedingt Sinn, ist als Beispiel aber sehr gut geeignet. Da intern ausschließlich mit Zeitangaben im Unix Timestamp-Format gearbeitet wird, muss die Ausgabe für das Formular besonders formatiert werden.

`echo date('d.m.Y', $bild[$id][8]);`

Der dabei entstandene String im Format "Tag.Monat.Jahr" wird nach der Bearbeitung im Unterprogramm bild_speichern() wieder zerlegt und in einen Timestamp zurück verwandelt.

`preg_match('/(\d+).(\d+).(\d+)/', $datum, $match);
$bild[$id][8] = mktime(12, 0, 0, $match[2], $match[1], $match[3]);`

Direkt danach muss sortiert werden. Bei einer Datenbank wäre das natürlich nicht notwendig. Unsere Textdatei ist jedoch darauf angewiesen, dass die Einträge in chonologischer Reihenfolge abgelegt sind. Der Befehl usort() verwendet die Funktion vergleich() zur Entscheidung, welches von je zwei Elementen des Arrays kleiner oder größer ist, wobei intern die Zwei-Dimensionalität des Ganzen beachtet wird. Sortiert wird nach dem Datum (Spalte 8) in aufsteigender Reihenfolge.

    function vergleich($a, $b)
    {
      if ($a[8] == $b[8]) return 0;
      return $a[8] > $b[8] ? 1 : -1;
    }
    usort($bild, vergleich);

Abschließend alles in der Datendatei speichern, und die Aufgabe ist gelöst.

7.) Mögliche Erweiterungen

Das ganze Beispiel ist sehr einfach gehalten und auf die wichtigste Funktionalität beschränkt. Besonders auffällig ist, dass in dieser Konstellation jedermann auf die administrativen Funktionen zugreifen kann. Idealerweise sollte man das dadurch beheben, dass man die Datei admin.php in ein Unterverzeichnis verschiebt und dieses z.B. mittels `.htaccess` (je nach Serversoftware) mit einem Passwort schützt.

Denkbar wären viele weitere Optimierungen.

- Der Versuch, Dateien im Format GIF (oder PNG, je nach Version der GD) hochzuladen, sollte mit einer vernünftigen Meldung abgeblockt werden.
- Auch völlig falsche Dateitypen sollten abgefangen werden. Hierfür bietet sich die beim Upload automatisch erzeugte Variable $neu_type an.
- Eine Beschränkung der Dateigröße auf z.B. maximal 200 Kilobyte wäre sicher empfehlenswert. Prüfen kann man das am sichersten mit der Variable $neu_size, die ebenfalls nach dem Upload erzeugt wird.
- Auch eine Beschränkung der Bildgröße auf z.B. maximal 1024 mal 768 Pixel wäre möglich. Diese Angaben können mit Hilfe der Funktion getImageSize() ermittelt werden.
- Was passiert mit Dateinamen, die bereits existieren? Denkbar wäre eine Fehlermeldung oder ein automatisches Durchnummerieren gleichnamiger Bilder, um ein unbeabsichtigtes Überschreiben zu verhindern.
- Um zu verhindern, dass anstößige Bilder hochgeladen werden, bietet sich eine Freischaltung durch den Administrator an. Jeder neue Eintrag in der Datendatei würde mit einer zusätzlichen Variable versehen, die dafür sorgt, dass das Bild im Skript bilder.php übersprungen wird. Zusätzlich erhält der Administrator eine Benachrichtigung per E-Mail, so dass er die neuen Datensätze freischalten kann.
- Um das Archiv nicht endlos aufzublähen, ist ein automatisches Löschen veralteter Bilder denkbar. Auch eine maximale Gesamtgröße in Megabyte könnte vorgegeben werden. Die Prüfung dieser Grenzen sollte sinnvollerweise direkt vor oder nach dem Upload eines neuen Bildes eingefügt werden.
- Die Datenbank könnte um weitere Spalten erweitert werden. Interessant wäre unter Umständen die Dateigröße (zu ermitteln mit filesize()), ein Link oder eine eMail-Adresse, oder auch die IP-Adresse der Person, die das Bild hochgeladen hat (zu ermitteln mit $GLOBALS['REMOTE_ADDR']).
- Man könnte zählen, wie oft schon auf die einzelnen Thubnails geklickt wurde.
- Das neueste Bild des Archives könnte in einer gesonderten Box auf der Startseite der Homepage angezeigt werden.
- Außerdem fehlen in den Skripten an vielen Stellen ordentliche Fehler- und Bestätigungs-Meldungen.
- ...

mados/WildMag