[ PHP-Schulungen durch NetUSE AG ]de.comp.lang.php FAQ
| Home
| Danke, Kristian! | Setup | Kommentare | Sitemap | Webmaster |

Kommentare
0 anzeigen
Gesamtübersicht
Login

E-Mail 

Paßwort 
Register

de.comp.lang.php FAQ

Ansprechpartner für diese FAQ ist die Mailingliste german-faq@lists.netuse.de


     Build Date: Sat Apr 21 06:25:33 CEST 2001

     $Id: faq.xml,v 1.4 2001/04/19 21:16:59 mjansen Exp $
     $Id: about.xml,v 1.26 2001/04/07 15:31:40 mjansen Exp $
     $Id: arrays.xml,v 1.10 2001/04/09 11:42:46 hartmut Exp $
     $Id: cms.xml,v 1.5 2001/02/12 18:56:58 j-f Exp $
     $Id: code.xml,v 1.9 2001/02/10 17:39:06 j-f Exp $
     $Id: database_mysql.xml,v 1.12 2001/02/08 10:08:34 wuerfl Exp $
     $Id: database_oracle.xml,v 1.12 2001/04/06 19:39:46 kosch Exp $
     $Id: databases.xml,v 1.9 2001/02/06 00:45:10 j-f Exp $
     $Id: datetime.xml,v 1.7 2001/03/02 20:29:47 mjansen Exp $
     $Id: functions.xml,v 1.12 2001/04/07 17:59:01 j-f Exp $
     $Id: install.xml,v 1.11 2001/02/07 19:45:20 sbergmann Exp $
     $Id: mail.xml,v 1.9 2001/02/21 11:12:26 guido Exp $
     $Id: objects.xml,v 1.6 2001/02/03 11:15:06 j-f Exp $
     $Id: open_exec.xml,v 1.16 2001/04/04 20:10:31 j-f Exp $
     $Id: openpub.xml,v 1.5 2001/03/18 17:54:16 kk Exp $
     $Id: pear.xml,v 1.2 2001/04/19 21:06:21 mjansen Exp $
     $Id: phpinterpreter.xml,v 1.8 2001/02/24 15:25:39 j-f Exp $
     $Id: phplib.xml,v 1.9 2001/02/10 00:09:50 j-f Exp $
     $Id: phpmyadmin.xml,v 1.1 2000/12/29 20:40:22 kk Exp $
     $Id: regexp.xml,v 1.6 2001/03/02 09:06:15 mjansen Exp $
     $Id: scripts.xml,v 1.16 2001/04/14 02:08:01 j-f Exp $
     $Id: strings.xml,v 1.7 2001/02/26 07:17:48 kk Exp $
     $Id: version4.xml,v 1.3 2001/01/16 20:51:03 j-f Exp $
     $Id: version4_session.xml,v 1.13 2001/03/20 18:28:56 mjansen Exp $
     $Id: webserver.xml,v 1.4 2001/02/06 00:44:12 j-f Exp $
     $Id: webvariablen.xml,v 1.5 2001/01/13 11:14:51 kk Exp $
     $Id: xml.xml,v 1.3 2001/04/14 10:33:20 mjansen Exp $


Inhaltsverzeichnis

1. Über diese FAQ

1.1. Was ist das hier?
1.2. Wie ist die Charta dieser Newsgroup?
1.3. Was ist PHP?
1.4. Welche Version von PHP ist aktuell?
1.5. Was bedeutet LAMP, WAMP und so weiter?
1.6. Wo finde ich die aktuelle Version dieser FAQ?
1.7. Kann ich eine Kopie der FAQ per Mail zugesendet bekommen?
1.8. Du hast doch für die FAQ geschrieben. Ich habe da eine Frage zu PHP...
1.9. Das ist eine tolle FAQ! Kann ich die als Unterrichtsmaterial verwenden? Kann ich sie drucken?
1.10. Was soll ich tun, wenn ich einen Fehler in der FAQ gefunden habe?
1.11. Kann ich selber für diese FAQ schreiben?
1.12. Wo finde ich weitere Informationen über PHP?
1.13. Welche Bücher gibt es über PHP?
1.14. Soll ich Jobangebote in de.comp.lang.php posten?
1.15. Wer kann mir einen Provider empfehlen?
1.16. Warum bekomme ich Ermahnungsmails, wenn ich Autoren in der Gruppe auf Netiquetteverstöße aufmerksam mache?
1.17. Warum bekomme ich Ermahnungsmails?
1.18. Warum sind Flames sinnlos?
1.19. Ich verwende Outlook Express und keiner hat mich lieb.
1.20. Was ist TOFU? Wieso finden die Anderen meine Artikel schwer zu lesen?
1.21. Wie verweise ich auf die FAQ?
1.22. Wie stelle ich meine Frage an die Newsgroup am sinnvollsten?

2. Installation und Inbetriebnahme

2.1. Suse Linux: Wie installiere ich PHP?
2.2. Suse Linux 6.2 und 6.3: Warum funktionieren die libgd-Funktionen nicht korrekt?
2.3. Wie compiliere ich ein aktuelles PHP auf Linux mit Apache Server?
2.4. Ich habe Probleme PHP3 selbst zu compilieren.
2.5. Wie installiere ich PHP auf Unix mit Netscape Server?
2.6. Wie installiere ich CGI-PHP auf einem Apache-Server?
2.7. Wie installiere ich PHP auf Windows?
2.8. Was ist PHP/FI und wo kann ich es bekommen? Was ist phtml?
2.9. Linux: Meine shared libraries werden nicht gefunden.

3. Allgemeine Fragen zu PHP

3.1. Wie vergleicht sich PHP mit anderen bekannten Webentwicklungssystemen?
3.2. Wie vergleicht sich die Performance von PHP zu Perl?
3.3. Wie kann ich mein ASP-Programm in PHP übersetzen?
3.4. CGI PHP oder Modul?
3.5. PHP-Scripte von Windows nach Unix portieren?
3.6. Welche Editoren sind für PHP geeignet?
3.7. Zeitgesteuerte PHP-Scripte und Shellscripte
3.8. Wie bette ich PHP in HTML ein? (Beispielprogramm)
3.9. Wie finde ich heraus, wie mein PHP-Interpreter konfiguriert ist?
3.10. Wo finde ich die php3.ini bzw. die php.ini?
3.11. Wie kann ich auf Umgebungsvariablen zugreifen?
3.12. Wie kann ich auf den HTTP-Request-Header zugreifen?
3.13. Gibt es noch mehr interessante Variablen im Environment?
3.14. Ich verwende PHP (Version 3) als Apache-Modul. Wie kann ich dies konfigurieren?
3.15. Was bedeuten master value und local value in phpinfo()?
3.16. Welche Konfigurationsvariablen kann ich nicht in .htaccess-Dateien verwenden?
3.17. Was genau bewirkt safe_mode und ist das sicher?
3.18. "Fatal error: Maximum execution time exceeded"
3.19. Was ist --enable-force-cgi-redirect? Warum enthält $PHP_SELF den Pfad zum CGI-Interpreter?
3.20. Warum funktioniert set_time_limit() nicht wie angepriesen?
3.21. Was ist das für ein @-Zeichen vor einigen Funktionsaufrufen?
3.22. Wie kann ich auf Kommandozeilen-Argumente zugreifen?
3.23. Wie kann ich einen Parameter von einer PHP-Seite an eine andere weitergeben?
3.24. Wie kann ich eine PHP-Präsentation auf CD brennen?
3.25. Werden meine PHP-Seiten von einer Suchmaschine indiziert?
3.26. Wie kann ein Besucher meiner Seite den PHP-Code im Browser sehen?
3.27. Gibt es für PHP einen "Dokumentationsgenerator" ähnlich Javadoc für Java?
3.28. Wie kann ich die Ausgabe meines Scriptes in einen anderen Frame umlenken?
3.29. Warum ist es schlecht, mit dem Referer zu arbeiten?

4. Typen und Funktionen

4.1. Welche Variablenarten gibt es in PHP und wie greife ich auf sie zu?
4.2. Welche Datentypen gibt es in PHP?
4.3. Wie schreibe ich eine Funktion mit einer variablen Anzahl von Argumenten?
4.4. Wie gebe ich mehrere Werte mit einer Funktion zurück?
4.5. Wie schreibe ich ein Script, das beliebige Parameter verarbeitet?
4.6. Variable Variablen
4.7. Was ist der Unterschied zwischen isset() und einem Vergleich auf den Leerstring?
4.8. Wie kann ich JavaScript-Funktionen aus PHP heraus aufrufen?

5. Stringfunktionen

5.1. Was ist besser, print() oder echo?
5.2. Wie zerlege ich einen String?
5.3. Wie zerlege ich eine URL?
5.4. Wie gebe ich eine Zahl formatiert aus?
5.5. Wie kann ich Zeilenumbrüche in <br> umwandeln?
5.6. Wie breche ich einen String nach x Zeichen um?

6. Reguläre Ausdrücke

6.1. Wie kann ich mehr über reguläre Ausdrücke lernen?
6.2. Soll ich ereg() oder preg() verwenden?
6.3. Wie verwende ich die preg()-Funktionen?
6.4. Was sind reguläre Ausdrücke?
6.5. Welche Bauelemente kommen in regulären Ausdrücken vor?
6.6. Wie teste ich auf die Existenz mehrerer Suchworte in einem String/Array?
6.7. Wie isoliere ich Suchstrings aus einem größeren Text?
6.8. Wie finde ich alle Links in einer HTML-Datei?
6.9. Wie ersetze ich alle relativen Links in einer HTML-Datei?
6.10. Wie überprüfe ich einen String auf seinen Inhalt?

7. Arrays und Arrayvariablen

7.1. Wie kann ich ein Element an ein Array anfügen?
7.2. Wie kann ich ein Array aufzählen?
7.3. Wie greife ich auf ein mehrdimensionales Array zu?
7.4. Wie kann ich ein Array nach einem beliebigen Kriterium sortieren lassen?
7.5. Wie kann ich Duplikate aus einem Array entfernen?
7.6. Wie kann ich ein Array von einer Seite auf eine andere transportieren?

8. Klassen und Objekte

8.1. Warum Klassen und Objekte benutzen?
8.2. Wie definiere ich eine Klasse? Wie erzeuge ich ein Objekt?
8.3. Was ist $this?
8.4. Was ist extends? Was ist Vererbung?
8.5. Was ist ein Konstruktor?
8.6. Was sind polymorphe Funktionen? Kann ich sie simulieren?
8.7. Wie kann ich Metainformationen über eine Klasse bekommen?

9. Variablen und Formulare

9.1. Wie übergebe ich Variablen aus einem Formular an ein PHP-Script?
9.2. Wie kann ich ohne Formular Variablen an ein Script übergeben?
9.3. Wie viele Formularelemente kann ich auf einer Seite haben?
9.4. Sollte ich besser GET oder POST verwenden?
9.5. Wie kann man ein <select multiple> verarbeiten?
9.6. Wie kann man Checkboxen verarbeiten?
9.7. Wie funktioniert Datei-Upload über HTML-Formulare?
9.8. Wie kann ich aus einer Datenbanktabelle einen <Select> erzeugen?
9.9. Wie verarbeite ich <input type=image>?

10. Dateifunktionen und Programmausführung

10.1. Wie kann ich eine Datei auslesen?
10.2. "Warning: Supplied argument is not a valid File-Handle resource"
10.3. Wie kann ich ein externes Programm von PHP aus starten?
10.4. Wie realisiere ich einen Dateidownload mit PHP?
10.5. Wie kann ich in einer Datei eine Zeile einfügen oder löschen?
10.6. Wie kann ich einen Datei-Upload per FTP durchführen?
10.7. Unix: Welche Zugriffsrechte brauche ich, um eine Datei anzulegen?
10.8. Wie kann ich mit PHP auf die serielle Schnittstelle zugreifen?
10.9. Warum funktioniert unlink() unter Windows nicht?

11. Datums- und Kalenderprobleme

11.1. Wie kann ich das aktuelle Datum bekommen?
11.2. Wie kann ich ein deutsches Datum in MySQL-Format umwandeln (und umgekehrt)?
11.3. Wie kann ich die Anzahl der Tage zwischen zwei Daten bestimmen?
11.4. Wie kann ich das Datum des Vortages bestimmen?
11.5. Wieviel Tage hat der aktuelle Monat?

12. Mail lesen und schreiben

12.1. Was ist SMTP?
12.2. Was ist das Domain Name System?
12.3. Unix: Wie funktioniert der Mailversand?
12.4. Windows: Wie funktioniert der Mailversand?
12.5. Windows: Wo finde ich Mailserver, die ich bei mir installieren kann?
12.6. Wie kann ich eine HTML-Mail versenden? Wie kann ich den Absender meiner Mail festlegen?
12.7. Wie kann ich ein Attachment mit einer Mail versenden?
12.8. Wie kann ich eine Mail effizient an sehr viele Empfänger versenden?
12.9. Wie kann ich die Gültigkeit einer Mailadresse testen?
12.10. Wie kann ich überprüfen, ob eine versendete Mail tatsächlich angekommen ist?
12.11. Wie kann ich feststellen, ob eine Mailadresse äußerlich gültig ist?
12.12. Wie versende ich SMS mit PHP?

13. Datenbanken

13.1. Wie kann ich mehr über SQL lernen?
13.2. Wieso kann ich mehrere, durch Semikolon getrennte Statements nicht ausführen?
13.3. Ist es sinnvoll, Bilder in einer Datenbank abzulegen?
13.4. Windows: Jeder Zugriff auf meine Datenbank dauert eine halbe Minute!
13.5. Wie kann ich meine Datenbankperformance steigern?
13.6. Wie kann ich zwei Tabellen miteinander verknüpfen?
13.7. Was ist Aggregation? Was ist GROUP BY?
13.8. Was ist der Unterschied zwischen connect und pconnect?
13.9. Wie kann ich mein Datenbankpaßwort gegen Spionage sichern?
13.10. MySQL oder PostgreSQL?
13.11. Wie komme ich bei meinem Provider an die Datenbank?
13.12. Wie kann ich auf einen ODBC-Server (MSSQL, Access) zugreifen?

14. Datenbanken: MySQL

14.1. Kommt MySQL mit mehr als x Datensätzen pro Tabelle klar? Wie stabil ist MySQL?
14.2. Wie greife ich auf eine MySQL-Datenbank zu?
14.3. "0 is not a MySQL result index"
14.4. Mein Script verbraucht so viel Speicher beim Datenbankzugriff.
14.5. Windows: "Call to unsupported or undefined function: mysql_connect()"
14.6. Unix: "Call to unsupported or undefined function: mysql_connect()"
14.7. "Call to unsupported or undefined function: mysql_errno()"
14.8. "MySQL-Server has gone away"
14.9. Wie kann ich eine CSV-Datei in MySQL importieren?
14.10. Wie kann ich eine CSV-Datei aus MySQL exportieren?
14.11. Wie kann ich die Datensätze der letzten 2 Wochen listen?
14.12. Wie kann ich eine Tabelle nach IP-Nummern sortieren lassen?
14.13. Wie lösche ich alle Datensätze, die älter als n Tage sind?
14.14. Wie kann ich Bilder in einer MySQL-Datenbank speichern?
14.15. Wie kann ich einen zufälligen Eintrag aus einer MySQL-Tabelle auswählen?
14.16. Ich habe eine Tabelle mit n Einträgen und möchte auf jeder Seite m davon anzeigen.
14.17. Wozu ist auto_increment nützlich? Wie erfahre ich den Wert des letzten Inkrements?
14.18. Wie lege ich den Initialwert des auto_increment fest? Läuft dieser Wert über?
14.19. Wie realisiere ich eine Volltextsuche mit MySQL?
14.20. Meine Datenbankabfrage/Mein SQL-Statement funktioniert nicht.

15. Datenbanken: Oracle

15.1. Ora oder OCI?
15.2. Ich habe Oracle-Support mit --with-oci8 in PHP eincompiliert, nun startet der Apache nicht mehr.
15.3. Unix: "Call to unsupported or undefined function: OCILogon()"
15.4. "Warning: ORA-12154: TNS:could not resolve service name"
15.5. Der Webserver verbraucht jetzt viel mehr Speicher als ohne Oracle, mache ich was falsch?
15.6. Umlaute, die in die Datenbank eingetragen wurden, werden nicht korrekt dargestellt.
15.7. "Warning: ORA-12705: invalid or unknown NLS parameter value specified"
15.8. Gibt es auto_increment unter Oracle?
15.9. Ich verwende das obige Beispiel. Wie kann ich nun mysql_insert_id() emulieren?
15.10. Wie selectiere ich nur bestimmte Zeilen (LIMIT unter MySQL)?
15.11. Wie speichere ich Datensätze mit mehr als 2000 Zeichen ab?
15.12. Wie bearbeite ich LOBs mit PHP?
15.13. Wie nenne ich Spalten um?
15.14. Wie kann ich SQL Skriptdateien in Oracle ausführen?
15.15. Welche freien Tools gibts für Oracle?
15.16. Ich bekomme ein Oracle Fehlernummer ORA-XXXXX, wo stehen die Fehlercodes?
15.17. Welche Bücher zu Oracle sind empfehlenswert?

16. phpMyAdmin

16.1. Was ist phpMyAdmin?
16.2. Ich bin kein MySQL-Administrator. Wie kann ich phpMyAdmin nur für mich selbst installieren?
16.3. Ich bin MySQL-Administrator und möchte ein Exemplar phpAdmin für alle meine User installieren.
16.4. Wieso kann ich den Inhalt meiner Tabelle nicht editieren?
16.5. Wieso werden TIMESTAMP-Felder nicht auf die aktuelle Zeit gesetzt, wenn ich eine neue Zeile einfüge?
16.6. Wieso kann ich in phpMyAdmin mehrere durch Semikolon getrennte SQL-Statements ausführen, nicht aber mit normalen PHP-Funktionen?

17. PHPLIB

17.1. Was ist PHPLIB?
17.2. Wo kann ich PHPLIB bekommen?
17.3. Mein Provider hat PHPLIB nicht installiert.
17.4. Ich habe keinen Zugriff auf die php.ini.
17.5. "Oops, php3_SetCookie called after header has been sent!"
17.6. GET-Mode oder Cookie-Mode? Sind Cookies böse?
17.7. Was ist das Sevenval-Patent?
17.8. Warum verwendet PHPLIB nicht die IP-Nummer des Browsers als Schutz gegen eine Übernahme der Session?
17.9. Warum sind die Session-IDs von PHPLIB so lang?
17.10. Was schreibe ich denn nun in meine local.inc?
17.11. ERROR 1146: Table 'xyz.active_sessions' doesn't exist!
17.12. Wie kann ich mit PHPLIB und Frames arbeiten?
17.13. Internet Explorer: Meine Seiten werden nicht aktualisiert.
17.14. Wie kann ich Reloads durch den User erkennen und verhindern?
17.15. Wie kann ich meine Variablen initialisieren und registrieren?
17.16. Wie kann ich auto_init benutzen, um Session-Statistiken zu erfassen?
17.17. Wie kann ich eine Datei mit einem Paßwort schützen?
17.18. Wie kann ich mich gegen einen LDAP-Server authentisieren?
17.19. Wie kann ich Zugriffsrechte in PHPLIB definieren?
17.20. Wie kann ich einen Warenkorb realisieren?
17.21. Wie kann ich eine Menünavigation erzeugen?
17.22. Was sind Templates? Warum sind Templates nützlich?

18. Webserver und PHP

18.1. Apache: Kann ich PHP auch auf .html-Dateien anwenden?
18.2. Apache: Wie kann ich ein Verzeichnis mit einem Paßwort schützen?
18.3. Apache: Wie kann ich ein Verzeichnis mit PHP mit einem Paßwort schützen?
18.4. Kann ich mit CGI PHP ein Verzeichnis mit einem Paßwort schützen?
18.5. Wie kann ich mit PHP die Bildschirmauflösung des Browsers herausfinden?
18.6. Wie kann ich das Caching einer Seite verhindern?
18.7. "Document contains no data"
18.8. Wie erzeuge ich mit PHP einen Redirect auf eine andere Seite?
18.9. Was sind Sessions und warum sind sie nützlich?
18.10. Wie kann ich mit PHP WAP-Seiten erzeugen?
18.11. Wie bringe ich eine Suchmaschine dazu, meine Seiten zu indizieren?

19. Content Management Systeme

19.1. Was ist ein Content Management System? Warum ist es nützlich?
19.2. Welche PHP-basierten Content Management Systeme gibt es?

20. Häufig nachgefragte Standardscripte

20.1. Wie kann ich eine schummelsichere Abstimmung codieren?
20.2. Wie kann ich einen HTTP POST-Request absenden?
20.3. Wie kann ich eine Volltextsuche realisieren?
20.4. Wie kann ich mit PHP News lesen und schreiben?
20.5. Wie kann ich einen Onlineshop mit PHP realisieren?
20.6. Wie kann ich die IP des Users erfahren?
20.7. Wie kann ich ein JPEG-Bild verkleinern?
20.8. Wie kann ich die Performance zweier Befehle vergleichen?
20.9. Wie kann ich den Inhalt eines Verzeichnisses samt dem Inhalt aller Unterverzeichnisse ausgeben?
20.10. Wie kann ich aus einem Zahlenbereich von x bis y, zufällig n Zahlen auswählen, so daß keine Zahl doppelt vorkommt?
20.11. Wie kann ich zählen, wie oft auf einen Link geklickt wurde?
20.12. Wie kann ich das Datum der letzten Änderung einer Datei erfahren?
20.13. Wie kann ich ein Forum mit PHP realisieren?
20.14. Wie biete ich meine Seiten mehrsprachig an?
20.15. Wie kann ich ermitteln, wieviele Besucher gerade meine Seite betrachten?
20.16. Wie erstelle ich eine Webmail-Oberfläche mit PHP?
20.17. Wie überprüfe ich Links auf ihre Gültigkeit?

21. Guter Code

21.1. Vermeide globale Variablen.
21.2. Halte Code links. Verwende Wächter statt Schachtel-if.
21.3. 'or' und 'and' sparen Klammern.
21.4. Prüfe importierte Parameter. Traue niemandem.
21.5. Von HTML zu PHP: Schreibe Formularverarbeitungen in Normalform.
21.6. Trenne Aussehen und Inhalt.

22. PHP 4

22.1. Ist PHP 4 stabil?
22.2. Wo bekomme ich PHP4?
22.3. Wie übersetze ich PHP4?
22.4. Was ist neu in PHP4?
22.5. Wie kann ich PHP4 (CGI und Apache-Modul) konfigurieren?

23. PHP4: Sessions

23.1. Wie realisiere ich Sessions mit PHP4?
23.2. Was ist eine Session-ID? Was ist PHPSESSID?
23.3. Wie stelle ich fest, ob der Client die Cookie-Annahme verweigert?
23.4. Wie übergebe ich Session-IDs ohne Cookies an eine andere Seite? Was ist Fallback?
23.5. Warning: Cannot send session cookie - headers already sent...
23.6. Wie kann ich den Namen der Session ändern, ohne in die php.ini einzugreifen?
23.7. Wie schütze ich Sessiondaten zusätzlich?
23.8. Wie groß darf die Menge an Daten sein, die ich in einer Session speichern darf?
23.9. Wie kann ich mir den Inhalt der Sessiondaten anzeigen lassen?
23.10. Wie kann ich mir den Inhalt der Cookiedaten anzeigen lassen?
23.11. Sessiondaten werden nach session_destroy() nicht gelöscht. Wie kann ich sie trotzdem löschen?
23.12. Was geschieht im Filesystem des Servers wenn ich Sessions benutze?
23.13. Wie benutze ich die Session-Funktionen unter Windows?

24. PEAR

24.1. Was ist PEAR?
24.2. Wo kann ich PEAR downloaden?
24.3. Wie installiere ich PEAR?
24.4. Wo finde ich weitere Informationen zu PEAR?

25. Open Publication License

25.1. Englische Version
25.2. Deutsche Version


1. Über diese FAQ

1.1 Was ist das hier?

Antwort von Kristian Köhntopp

Dies ist die FAQ (FAQ = Frequently Asked Questions) für die Newsgruppe de.comp.lang.php. Sie erklärt den Zweck der Newsgruppe, auf welche Weise man hier am einfachsten an sinnvolle Antworten kommt und dient als Sammlung von Antworten auf häufig gestellte Fragen in der Gruppe.

Wenn Du Kommentare oder Vorschläge zu diesem Artikel hast oder wenn Du selber einige Abschnitte in diesen Artikel einbringen möchtest, wendest Du Dich am besten per Mail an die Mailingliste zur de.comp.lang.php FAQ, <german-faq@lists.netuse.de>.

1.2 Wie ist die Charta dieser Newsgroup?

Antwort von Kristian Köhntopp

Diese Newsgruppe richtet sich an alle Benutzer und Programmierer von PHP, einer Programmiersprache mit Schwerpunkt auf der Entwicklung von Webanwendungen. Es können alle Themen rund um PHP besprochen werden, seien es nun Probleme mit der Installation, der Anwendung oder Programmierung in PHP oder der Erweiterung des PHP- Interpreters selbst.

1.3 Was ist PHP?

Antwort von Kristian Köhntopp

Die Abkürzung PHP steht offiziell für "PHP: Hypertext Preprocessor". Dies ist eine rekursive Abkürzung im Stile des GNU -Projektes. PHP ist eine Scriptsprache zur dynamischen Erstellung von Webseiten. Die Anweisungen der Sprache sind dabei in den HTML-Code einer Webseite eingebettet, d.h. jede HTML-Seite ist auch ein gültiges PHP-Programm. Die Syntax von PHP ist ähnlich wie die von C, Java oder JavaScript. Die Sprache zeichnet sich vor allen Dingen durch ihre leichte Erlernbarkeit, ihre ausgezeichneten Datenbankanbindungen und Internet-Protokolleinbindungen und die Unterstützung zahlreicher weiterer Funktionsbibliotheken aus. PHP stellt so für den Web-Entwickler das ideale Werkzeug zur Erstellung von dynamischen Inhalten dar.

PHP ist freie Software im Sinne der Debian Free Software Guidelines (DFSG). Quelltext und Binaries des PHP-Interpreters sind frei erhältlich und können für alle kommerziellen und nichtkommerziellen Zwecke eingesetzt werden; jeder kann den PHP-Quelltext weiterentwickeln und die Änderungen an das PHP-Projekt zurückfließen lassen. Der genaue Lizenztext ist in der Datei license-php.txt enthalten, die Bestandteil der PHP-Distribution ist.

PHP läuft auf allen gängigen Unix-Versionen und auf den verschiedenen Windows-Versionen (Windows 95/98/ME/NT/2000). Als CGI-Programm kann PHP mit jedem Webserver zusammenarbeiten. Für einige Webserver, allen voran Apache, stehen auch Modulversionen zur Verfügung, die sehr viel effizienter ausgeführt werden.

Die Homepage des PHP-Projektes ist http://www.php.net. Mirrors dieser Site sind in vielen Ländern vorhanden, unter anderem auch in Deutschland unter der URL http://www.php3.de oder http://de.php.net. Von dort kann man die jeweils aktuelle Releaseversion des Interpreters sowie Binaries für eine Reihe von Plattformen herunterladen. Ebenso finden sich dort das Handbuch sowie Archive der englischen Mailinglisten.

1.4 Welche Version von PHP ist aktuell?

Antwort von Kristian Köhntopp

Die aktuelle Produktionsversion von PHP ist Version 4.0.4pl1. Nur diese Version sollte auf Produktionsmaschinen eingesetzt werden. Die letzte Version von PHP 3 ist Version 3.0.17. PHP 3 wird nicht mehr weiterentwickelt, es finden nur noch marginale Bugfixes statt.

Die aktuellste Version ist immer auf der offiziellen PHP Homepage verfügbar.

1.5 Was bedeutet LAMP, WAMP und so weiter?

Antwort von Kristian Köhntopp

LAMP ist die Abkürzung für Linux, Apache, MySQL und PHP. Sie beschreibt ein System zur Entwicklung und zum Betrieb von Webanwendungen, bestehend aus Betriebssystem, Webserver, Datenbankserver und Programmiersprache.

Analog steht die Abkürzung WAMP für Windows, die Windows-Version von Apache, die Windows-Version von MySQL und die Windows-Version von PHP. Viele PHP-Anwender entwicklen lokal auf WAMP und überspielen die fertigen Seiten dann auf einen LAMP- oder SAMP (Solaris, Apache, MySQL, PHP)-Server bei einem Provider.

1.6 Wo finde ich die aktuelle Version dieser FAQ?

Antwort von Kristian Köhntopp

Eine Downloadversion dieser FAQ im HTML-Format findet sich unter der URL http://www.koehntopp.de/php/faq-html.tar.gz.

Windows-Anwender können die FAQ auch im CHM- oder HLP-Format laden, die in der jeweils aktuellsten Version unter der Adresse http://www.koehntopp.de/php/faq.chm oder http://www.koehntopp.de/php/faq.hlp abgelegt ist. (Zum Ansehen von CHM-Dateien ist eine hinreichend neue Version des Microsoft Internet Explorer notwendig).

Die aktuelle Version dieser FAQ ist unter der URL http://www.koehntopp.de/php zu lesen. Eine Version in einer einzigen Datei befindet sich unter der URL http://www.koehntopp.de/php/faq-single.html.

Die XML-Quelltexte dieser FAQ sind unter der URL http://www.koehntopp.de/php/faq.tar.gz zu finden.

1.7 Kann ich eine Kopie der FAQ per Mail zugesendet bekommen?

Antwort von Kristian Köhntopp

Die FAQ wird nicht als Mail versendet. Die Frage Wo finde ich die aktuelle Version dieser FAQ? beschreibt, wie und in welchen Formaten die FAQ bezogen werden kann.

1.8 Du hast doch für die FAQ geschrieben. Ich habe da eine Frage zu PHP...

Antwort von Kristian Köhntopp

Es ist sinnlos, Fragen per Mail an einen der Autoren dieser FAQ zu senden. Du belastest damit eine einzelne Person mit Arbeit, statt die Arbeit auf die Newsgroup zu verteilen. Außerdem ist diese Arbeit verschwendet, denn die Antwort wird nur von Dir und nicht von den anderen Lesern der Newsgroup gelesen. Auch kann die Antwort nicht vom FAQ-Team weiterverarbeitet werden.

Sende Deine Frage bitte an die Newsgroup. Keiner der Autoren der FAQ wird Dir privaten Support per Mail leisten.

1.9 Das ist eine tolle FAQ! Kann ich die als Unterrichtsmaterial verwenden? Kann ich sie drucken?

Antwort von Kristian Köhntopp

Dieser Text ist wie alle Werke urheberrechtlich geschützt. Er ist jedoch unter den Bedingungen der Open Publication License, Version 0.4 oder höher verfügbar. Die genaue Lizenz findet sich in Open Publication License.

Wenn dieser Text reproduziert oder verwendet wird, bitten die Autoren um Meldung eines solchen Angebotes an german-faq@lists.netuse.de unter Angabe einer Kontaktadresse. Diese Kontaktperson ist herzlich eingeladen, sich auf der Mailingliste german-faq@lists.netuse.de anzumelden, um über Aktualisierungen des Textes informiert zu werden.

1.10 Was soll ich tun, wenn ich einen Fehler in der FAQ gefunden habe?

Antwort von Martin Jansen

Wenn Du einen Fehler in einem der Texte in der FAQ gefunden hast, dann bitten wir Dich, uns diesen mitzuteilen. Dazu schickst Du am besten eine E-Mail an german-faq@lists.netuse.de. Unter dieser Adresse erreichst Du die Mailingliste der de.comp.lang.php-FAQ, welche alle Autoren der FAQ abonniert haben.

1.11 Kann ich selber für diese FAQ schreiben?

Antwort von Kristian Köhntopp

Ja, sofern Du die in Open Publication License beschriebene Lizenz für Dich akzeptieren kannst. Diese Lizenz bedeutet im wesentlichen, daß Du an Deinen eigenen Texten das volle Urheber- und Verwertungsrecht behältst, aber jedermann das Recht einräumst, die FAQ zu nutzen und unverändert und mit Hinweis auf die Originalquelle und die Originalautoren zu reproduzieren.

Rein technisch benötigst Du die folgenden Utensilien:

Es gibt eine Mailingliste german-faq@lists.netuse.de, die Nachrichten über Änderungen an der FAQ enthält und bei der man Hilfe für Autoren bekommt. Man kann die Mailingliste unter der Adresse german-faq-subscribe@lists.netuse.de bestellen.

Es existiert ein CVS-Archiv, aus dem die aktuelle Version der FAQ bezogen werden kann. Die CVSROOT dieses Archives ist :pserver:cvsread@phplib.netuse.de:/repository mit dem Paßwort cvsread. Das Modul heißt german-faq. Ein CVSWEB-Zugang zu dem Archiv ist unter http://phplib.netuse.de/cgi/cvsweb.cgi zu finden.


$ cvs -d :pserver:cvsread@phplib.netuse.de:/repository login
Password: cvsread
$ cvs -d :pserver:cvsread@phplib.netuse.de:/repository checkout german-faq
...

# Aktualisieren der Version mit
$ cd german-faq
$ cvs -z9 update -dAP

Diese Version ist immer aktueller als die auf dem Webserver veröffentlichte Version.

1.12 Wo finde ich weitere Informationen über PHP?

Antwort von Kristian Köhntopp

Zu PHP gibt es zahlreiche Informationsquellen in deutscher und englischer Sprache.

Deutsche Ressourcen im WWW

Internationale Ressourcen im WWW

Fertige Anwendungen in PHP

Ein Verzeichnis von Projekten, die PHP verwenden, findet man im Projektverzeichnis der offiziellen Homepage.

1.13 Welche Bücher gibt es über PHP?

Antwort von Martin Jansen

In Deutsch:

In Englisch:

Weitere nützliche Titel:

Ein weiteres Bücherverzeichnis findet sich auf der offiziellen Homepage von PHP: http://www.php.net/books.php

1.14 Soll ich Jobangebote in de.comp.lang.php posten?

Antwort von Kristian Köhntopp

Eine kurze Umfrage im Januar 2000 in de.comp.lang.php hat ergeben, daß Jobangebote in der Newsgroup toleriert werden, auch wenn sie nach Charta streng genommen off-topic sind - solange sie folgenden Ansprüchen an die äußere Form genügen:

Wenn Sie als Arbeitgeber nicht in der Lage sind, im USENET intelligent, kooperativ und regelkonform aufzutreten, sollten Sie andere Medien für Ihre Personalaquise verwenden, die sich Ihnen leichter erschließen. Ihre Corporate Identity wird es Ihnen danken.

1.15 Wer kann mir einen Provider empfehlen?

Antwort von Kristian Köhntopp

Eine kurze Umfrage im Februar 2000 in de.comp.lang.php hat ergeben, daß Fragen nach Providern oder Providerspezifika in dieser Newsgroup nicht willkommen sind. Die korrekte Newsgroup für diese Frage ist de.comm.provider.webspace.

Ebenso unerwünscht sind providerspezifische Fragen wie Wie komme ich bei xyz an die MySQL-Datenbank?. Der korrekte Ansprechpartner für solche Fragen wäre der Support des betreffenden Providers bzw. dessen FAQ. Werden derartig providerspezifische Fragen dennoch in die Newsgroup gestellt, ist es höflich, Folloup-To: poster zu setzen und hinterher eine Zusammenfassung der eingegangenen Mails zu posten. Eine Zusammenfassung besteht nicht darin, die Texte der eingegangenen Mails hintereinanderzuhängen und in die Gruppe zu werfen, sondern idealerweise in einem Text, der vergleichsweise schmerzlos in diese FAQ integriert werden kann.

Eine Providerdatenbank wird unter anderem bei Dynamic Web Pages und beim PHP-Center betrieben.

1.16 Warum bekomme ich Ermahnungsmails, wenn ich Autoren in der Gruppe auf Netiquetteverstöße aufmerksam mache?

Antwort von Kristian Köhntopp

Du hast vollkommen Recht: Manche Autoren in der Newsgroup verstoßen gegen die Netiquette, wie sie in de.newusers.infos gepostet wird. Sie tun dies etwa, indem sie ohne vollen Realnamen schreiben, inkorrekte Mailadressen ("nospam", "deletethis") angeben oder Artikel mit HTML oder Netscape-Visitenkarten versenden. Du sollst das auch nicht hinnehmen.

In einer Newsgroup ist der Ton jedoch genauso wichtig wie der Inhalt. Die Regulars von de.comp.lang.php sind stolz auf den freundlichen und hilfsbereiten Ton in ihrer Newsgroup. Wenn Du also einen anderen Autor an die Netiquette erinnern möchtest, dann tue dies bitte unbedingt per Mail und nicht öffentlich in der Gruppe. Auch die Netiquette, auf deren Einhaltung Du bestehst, fordert dies - Du kannst nicht auf der einen Seite auf der Einhaltung der Netiquette bestehen und andererseits selbst dagegen verstoßen, ohne Glaubwürdigkeit zu verlieren.

Und bitte: Halte Deinen Ton auch in der Mail freundlich. Du wirst leichter verstanden und erreichst das gewünschte Ziel viel eher.

Wenn Du meinst, Deinen Artikel dennoch öffentlich posten zu müssen, etwa um einen Autoren an die korrekte Newsgroup zu verweisen, oder weil die angegebene Mailadresse nicht erreichbar ist, oder weil sich der Autor per Mail nicht einsichtig zeigt und sich niemand sonst bisher darum gekümmert hat, dann halte Deinen Beitrag bitte freundlich und konstruktiv. Das bedeutet: Beantworte die gestellte Frage oder löse das Problem des Fragers so gut Du kannst und weise dann auf die Netiquette hin. Wenn Du zu dem Problem des Fragers nichts beizutragen hast, dann poste lieber gar nichts - oder schreibe eine Mail. Du bist nicht allein in der Gruppe und Du mußt die Welt nicht selbst retten. Ein anderer, der antworten kann, wird antworten und dabei wahrscheinlich auch auf korrektes Verhalten hinweisen.

Regeldiskussionen gehören in die dafür vorgesehene Newsgroup, de.soc.netzkultur.umgangsformen, oder sollen mit einem Followup-To: poster versehen werden.

1.17 Warum bekomme ich Ermahnungsmails?

Antwort von Kristian Köhntopp

Du wirst nicht nur in de.comp.lang.php, sondern in den meisten anderen deutschen Newsgroups auf korrektes Verhalten in den Newsgroups hingewiesen, wenn ohne einen vollständigen Namen postest, Artikel ohne gültige Absenderadresse schreibst, Artikel mit Werbung absetzt, HTML oder Netscape-Visitenkarten in Deinen Artikeln versendest oder mutwillig Artikel in die falschen Newsgroups schreibst.

Diejenigen von uns, die schon länger in den USENET News aktiv sind, haben sich diese Regeln und Verhaltenformen nicht aus Spaß ausgedacht. USENET existiert schon seit mehreren Jahrzehnten und die Verhaltensnormen, auf deren Einhaltung bestanden wird, haben sich in langen Jahren entwickelt und bewährt. Es gibt einen guten Einführungstext aus de.newusers.infos mit dem Titel Warum soll ich mich an die Regeln halten? der erklärt, warum die Dinge so sind, wie sie sind.

Wenn Du von de.comp.lang.php Ergebnisse möchtest, also technische Hilfe bei Deinen Problemen mit der Programmiersprache PHP, dann tust Du gut daran, Deinen Texten auch eine akzeptable äußere Form zu geben.

1.18 Warum sind Flames sinnlos?

Antwort von Kristian Köhntopp

Newbies, die sich nicht an geltende Netzkulturen halten oder schlecht formulierte Fragen stellen, kommen meist mit einem konkreten Problem nach de.comp.lang.php. Diese Leute bekommen dann allerdings häufig keine vernünftige Antwort, sondern werden mit Flames überhäuft. Der Grund liegt darin, daß beide Parteien mit unterschiedlichen Erwartungen und unterschiedlichen Kommunikationszielen in den Thread gegangen sind, und sie nicht bereit waren, von diesen Zielen abzuweichen. So ist keine sinnvolle Kommunikation zustande gekommen.

Eine sinnvolle Antwort auf ein schlecht formuliertes oder unhöfliches Posting unterscheidet sich in den folgenden Punkten:

Zunächst einmal versucht sie freundlich zu bleiben, ohne in der Sache nachzugeben.

Dann geht sie unmittelbar auf das Problem des Posters ein, d.h. sie hilft ihm auf eine konstruktive Weise, sein unmittelbares Problem zu lösen, um ihn wieder arbeitsfähig zu machen. Dies ist der wichtigste Aspekt der Nachricht aus der Sicht des Newbies oder Posters: Es ist egal, wie unsystematisch und offtopic die Nachricht von ihm oder Deine Antwort ist - wenn Du mit ihm etwas anfangen willst, mußt Du zuerst seinen unmittelbaren Block lösen, damit Du sinnvolle Dinge nachschieben kannst.

Nachschieben heißt in diesem Zusammenhang, den Newbie mit weiterführenden Informationen zu versorgen, damit er mehr lernt, als er mit seiner Frage eigentlich bezweckt hatte. "Nachschieben" ist wichtig, denn nur so bekommt man Newbies schrittweise zu Regulars umgebaut.

Erst am Schluß eines Postings gibt es dann die Netiquette, quasi als Dressing obendrauf. Mit dem ganzen Zucker, der vorab geliefert worden ist, schmeckt das dann nicht mehr so bitter und dringt viel tiefer ein. Immerhin ist der Newbie ernst genommen worden und hat produktive Antworten bekommen, obwohl er sich mit seinem unerfahrenen Auftreten in de.comp.lang.php ziemlich lächerlich gemacht hat - das ist wie in Shorts und T-Shirt auf eine Sitzung mit lauter Anzügen und Schlipsen zu kommen: "Selbstverständlich können wir Ihnen die 10.000 Tonnen Schweinehälften liefern, und übrigens Herr Graczoll, fällt Ihnen was an Ihrer Kleidung auf?"

Als Abschluß nocheinmal die Arbeitsschritte für guten technischen Support in de.comp.lang.php als Spickzettel:

1.19 Ich verwende Outlook Express und keiner hat mich lieb.

Antwort von Kristian Köhntopp

Das wird daran liegen, daß Du Dein Outlook Express nicht korrekt konfiguriert hast. Wahrscheinlich setzt Outlook Express nicht den korrekten Absendernamen, veröffentlicht Artikel in HTML oder in HTML- und Text-Versionen in doppelter Ausführung oder macht andere Dinge, die außer Microsoft niemand gut findet.

Bitte lies die Outlook Express FAQ, die für Deine Version von Outlook zutreffend ist und konfiguriere Deinen Newsreader korrekt. Auf http://www.mayn.de/support/os/win95/outlook.htm gibt es eine bebilderte Anleitung, wie man mit Outlook richtige Quotezeichen einstellt und das proprietäre "AW:" in Antworten auf das richtige "Re:" umstellt.

1.20 Was ist TOFU? Wieso finden die Anderen meine Artikel schwer zu lesen?

Antwort von Kristian Köhntopp

Text Oben, Fullquote Unten. Eine Unart, die einen nicht nur in dieser Newsgroup, sondern im ganzen Netz unbeliebt macht. Lies http://learn.to/quote/ von Dirk Nimmich, und speziell Abschnitt 2.3 "Warum soll ich meine Antwort nach dem Zitat plazieren?" und die folgenden.

1.21 Wie verweise ich auf die FAQ?

Antwort von Kristian Köhntopp

Ein Hinweis auf die FAQ sollte niemals einfach nur in der Form "RTFM" oder http://www.koehntopp.de/php erfolgen, sondern immer auf eine konkrete Antwort zeigen. Der Leser sollte außerdem erfahren, welche Antwort er dort findet und wieso das Bezug zu der gestellten Frage hat. In der Newsgroup hat sich folgendes Format eingebürgert:


  1.21 Wie verweise ich auf die FAQ?
  http://www.koehntopp.de/php/faq-about.html#about-21

1.22 Wie stelle ich meine Frage an die Newsgroup am sinnvollsten?

Antwort von Martin Jansen

Um in de.comp.lang.php eine sinnvolle Antwort zu erhalten, die Dir weiterhilft, solltest Du versuchen, Dich an die folgenden Regeln zu halten:


2. Installation und Inbetriebnahme

2.1 Suse Linux: Wie installiere ich PHP?

Antwort von Kristian Köhntopp

Suse Linux enthält bereits ab Werk alle notwendigen Komponenten, um PHP-Anwendungen entwickeln zu können. Es empfiehlt sich jedoch der Einsatz einer aktuellen Version von Suse Linux (derzeit 7.0), damit die eingesetzten Komponenten aktuell sind.

2.2 Suse Linux 6.2 und 6.3: Warum funktionieren die libgd-Funktionen nicht korrekt?

Antwort von Kristian Köhntopp

In Suse Linux 6.2 war der libgd-Support fehlerhaft gebaut. Beim Aufruf von imagegif() stürzte der gesamte Apache-Subprozeß ab.

In Suse Linux 6.3 war PHP 3.0.12 und libgd 1.6 mit FreeType-Support enthalten. Suse Linux selbst enthält libgd 1.7.x. Dies ist inkonsistent. Wenn PHP mit Support fuer libgd übersetzt wird, dann können unabhängig voneinander die Funktionen imagecreatefromgif() und imagegif() bzw. imagecreatefrompng() und imagepng() enthalten sein. In Suse Linux 6.3 waren jedoch weder die einen noch die anderen Funktionen enthalten.

Es war daher notwendig, sich einen eigenen PHP-Interpreter mit den aktuellen Funktionen selbst zu compilieren. Alternativ kann man sich vom Suse Supportserver eine aktualisierte Version von mod_php.rpm holen (Für Suse Linux 6.3 eine Version 3.0.14 mit funktionierendem libgd truetype-Support).

In Suse Linux 6.4 und 7.0 funktioniert alles korrekt.

2.3 Wie compiliere ich ein aktuelles PHP auf Linux mit Apache Server?

Antwort von Kristian Köhntopp

Eine gute Hilfe ist das Apache Compile Kit.

2.4 Ich habe Probleme PHP3 selbst zu compilieren.

Antwort von Kristian Köhntopp

config.cache nicht gelöscht.

Beim Selbstübersetzen von PHP werden gelegentlich neu installierte Bibliotheken nicht oder in der falschen Version gefunden. In diesem Fall ist meistens eine veraltete Datei config.cache aus einem früheren Lauf von configure schuld. Diese Datei muß gelöscht werden, danach werden die neuen Bibliotheken korrekt gefunden.

Antwort von Markus Dobel

PHP 3.0.13 als Modul auf RedHat 6.x.

In RedHat 6.x ist /usr/local/lib nicht im Standardsuchpfad für shared libraries enthalten. Man muß also nach der Installation von Zusatzbibliotheken wie pdflib und anderen die Datei /etc/ld.so.conf um dieses Verzeichnis erweitern und dann den Cache des Loaders aktualisieren, indem man ldconfig -v aufruft.

Außerdem sollte man dringend die Ratschläge in der Datei INSTALL.REDHAT lesen, die den PHP-Quellen beiliegt. Bei einigen Versionen von RedHat ist das Konfigurationsprogramm apxs defekt: Es gibt falsche Pfade aus. Wie in der Datei beschrieben muß dieser Fehler erst korrigiert werden, bevor man mit der eigentlichen Installation fortfahren kann.

2.5 Wie installiere ich PHP auf Unix mit Netscape Server?

Antwort von Kristian Köhntopp

Installation im einzelnen (Solaris 2.6):

Sei der Name der Zielmaschine ghost. Sei die Suitespot- Installation in /opt/local/suitespot-3.0/ mit dem Webserver in /opt/local/suitespot-3.0/https-ghost. Sie die DocumentRoot /opt/local/www/pages, das CGI-Verzeichnis in /opt/local/www/cgi-bin.

2.6 Wie installiere ich CGI-PHP auf einem Apache-Server?

Antwort von Kristian Köhntopp

Schritt 1: Man definiert ein Verzeichnis als CGI Verzeichnis:


<Directory /home/www/servers/phplib.shonline.de/cgi/>
Options ExecCGI
AllowOverride None
</Directory>

Hier wird das Verzeichnis /home/www/.../cgi als Verzeichnis zur Ausführung von CGI-Dateien gekennzeichnet, indem für das Verzeichnis das ExecCGI Attribut gesetzt wird.

Schritt 2: Man definiert für dieses Verzeichnis eine URL


ScriptAlias  /cgi/      /home/www/servers/phplib.shonline.de/cgi/

Die URL /cgi/ wird jetzt auf dieses Verzeichnis /home/www/.../cgi/ abgebildet.

Man kann jetzt in diesem Verzeichnis eine ausführbare Datei (etwa ein Shellscript) ablegen, die den folgenden Text ausgibt, wenn man sie startet.


Content-Type: text/plain

Hallo, Welt.

Wenn man diese Datei unter der URL http://meinserver.de/cgi/name_des-scripts aufruft, muss man den Text "Hallo, Welt." angezeigt bekommen. Gelingt dies nicht, sollte man durch das Error Log toben und nachsehen, was dort schiefgeht.

Schritt 3: Man installiert den PHP-Interpreter im CGI-Verzeichnis und prüft, ob man ihn als CGI-Programm aufrufen kann. Dazu ist der Interpreter nach /home/www/.../cgi zu kopieren und dann unter der URL http://.../cgi/php oder http://.../cgi/php.exe oder wie immer der Interpreter heißt aufzurufen.

Dabei muß die folgende Fehlermeldung kommen:


Security Alert! PHP CGI cannot be accessed directly.

This PHP CGI binary was compiled with force-cgi-redirect
enabled. This means that a page will only be served up if the
REDIRECT_STATUS CGI variable is set. This variable is set, for
example, by Apac he's Action directive redirect.

Kommt die Meldung nicht, ist das PHP-Binary unsicher und sollte durch ein korrekt compiliertes Binary ersetzt werden.

Schritt 4: Man definiert eine Scriptaktion, die das o.a. Binary startet und definiert eine Endung, die durch diese Aktion abgehandelt wird:

2.7 Wie installiere ich PHP auf Windows?

Antwort von Martin Jansen

Bei der Installation von PHP unter Windows hat man ebenso wie unter Unix/Linux die Möglichkeit, PHP als CGI-Version oder als Modul für den Apache Webserver zu installieren. Die Vor- und Nachteile der verschiedenen Versionen werden im Kapitel CGI PHP oder Modul? erläutert.

Sowohl die CGI- als auch die Modul-Version sind auf der offiziellen Downloadseite verfügbar.

Mit der Modul-Version ist es zur Zeit jedoch nicht möglich, die GD-Library bzw. die Sablotron-Library zu nutzen, da diese nicht threadsafe sind.

Daniel Beulshausen, der Initiator der Publizierung von PHP als Apache Modul für Windows, und Andreas Otto bieten auf der Seite http://www.php4win.de neben der jeweils aktuellsten offiziellen Version auch Development-Versionen für experimentierfreudige Anwender an.

Die offizielle Installationsanleitung für die CGI-Version in englischer Sprache findet sich auf dem offiziellen PHP-Server.

In deutscher Sprache findet man zur selben Frage eine umfangreiche WAMP-Anleitung von Thomas Schulz (inzwischen auf Dynamic Webpages umgezogen).

Zur Installation der Modul-Version schrieb Björn Höhrmann am 20.07.2000 in de.comp.lang.php folgendes:


Zur Installation:

WinNT4:         E:\Winnt\
Apache:         E:\winapp\apache\
PHP4 Module:    E:\winapp\apache\php4module\

E:\winapp\apache\conf\httpd.conf:
 + LoadModule php4_module php4module/php4apache.dll
 + AddType application/x-httpd-php .php

E:\winapp\apache\php4module\php.ini:
 > Kopiert nach E:\Winnt\

[...]

E:\winapp\apache>set path=%path%;e:\winapp\apache\php4module\
E:\winapp\apache>apache -k start
Apache/1.3.11 (WunLix) PHP/4.0.2-dev running...

[...]

Darüber hinaus gibt es PHP Triad, das eine komplette Serverumgebung inkl. PHP und MySQL auf einem Windows-Rechner installiert.

2.8 Was ist PHP/FI und wo kann ich es bekommen? Was ist phtml?

Antwort von Kristian Köhntopp

PHP/FI oder auch PHP2 ist der Vorläufer von PHP3. PHP/FI-Dateien haben oft die Endung .phtml. Es wird nicht mehr gewartet oder weiterentwickelt. Von einer Verwendung von PHP/FI in laufenden Projekten ist dringend abzuraten, auch dann, wenn noch alter PHP/FI-Code vorliegt.

2.9 Linux: Meine shared libraries werden nicht gefunden.

Antwort von Kristian Köhntopp

Übersetzt man PHP selbst und bindet dabei auch selbst installierte Module und Bibliotheken mit ein, kann es vorkommen, daß das Modul vom Apache nicht geladen wird, weil diese externen Bibliotheken nicht gefunden werden. Im folgenden wird erläutert, was es mit diesen Bibliotheken auf sich hat.


kris@valiant:~ > ldd /bin/ls
        libc.so.6 => /lib/libc.so.6 (0x4001f000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

Damit man das Kommando ls ausführen kann, müssen die Dateien ld-linux.so.2 und libc.so.6 vorhanden sein. Diese Dateien sucht mein System in den in /etc/ld.so.conf genannten Verzeichnissen. Damit dies schneller geht, legt das Kommando ldconfig einen Cache der Bibliotheken in diesen Verzeichnissen an. Mit diesem Caches wird ein Durchsuchen aller Verzeichnisse in /etc/ld.so.conf beim Start von Programmen vermieden. Stattdessen stehen in /etc/ld.so.cache Paare von Bibliotheksname und Pfadname, die sofort zugreifbar sind.

Ein laufendes ls hat also /lib/ld-linux.so.2 und /lib/libc.so.6 geladen. Das kann man auch nachsehen:


kris@valiant:~ > sleep 3600 &
[1] 27091
kris@valiant:~ > cat /proc/27091/maps
08048000-0804a000 r-xp 00000000 08:15 63535      /bin/sleep
0804a000-0804b000 rw-p 00001000 08:15 63535      /bin/sleep
40000000-40013000 r-xp 00000000 08:15 71682      /lib/ld-2.1.3.so
40013000-40014000 rw-p 00012000 08:15 71682      /lib/ld-2.1.3.so
4001e000-4001f000 rw-p 00000000 00:00 0
4001f000-400f9000 r-xp 00000000 08:15 71690      /lib/libc.so.6
400f9000-400fe000 rw-p 000d9000 08:15 71690      /lib/libc.so.6
400fe000-40101000 rw-p 00000000 00:00 0
bfffe000-c0000000 rwxp fffff000 00:00 0
kris@valiant:~ > ldd `which sleep`
        libc.so.6 => /lib/libc.so.6 (0x4001f000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
kris@valiant:~ > ls -l /lib/ld-linux.so.2
lrwxrwxrwx   1 root     root       11 Apr 11 18:16 /lib/ld-linux.so.2
                                                   -> ld-2.1.3.so
kris@valiant:~ > kill %1
kris@valiant:~ >
[1]+  Terminated              sleep 3600

Tatsächlich gibt die Ausgabe der Memory-Map für den Prozess 27091 (sleep) an, dass /lib/ld-2.1.3.so aka /lib/ld-linux.so.2 und /lib/libc.so.6 geladen sind - man kann deutlich die schreibgeschützten Code-Segmente (r-xp) und die überschreibbaren, nicht ausführbaren Datensegmente (rw-p) dieser Bibliotheken erkennen.

Will ich nun mein PHP4 starten, dann wird


kris@valiant:~ > ldd /usr/lib/apache/libphp4.so
        libgdbm.so.2 => /usr/lib/libgdbm.so.2 (0x4015b000)
        libpam.so.0 => /lib/libpam.so.0 (0x40162000)
        librecode.so.0 => /usr/lib/librecode.so.0 (0x4016a000)
        libdl.so.2 => /lib/libdl.so.2 (0x401e6000)
        libsnmp.so => /usr/lib/libsnmp.so (0x401ea000)
        libpdf.so.0 => /usr/lib/libpdf.so.0 (0x40225000)
        libtiff.so.3 => /usr/lib/libtiff.so.3 (0x4024e000)
        libldap.so.1 => /usr/lib/libldap.so.1 (0x40291000)
        libttf.so.2 => /usr/lib/libttf.so.2 (0x402a6000)
        libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x402cf000)
        libpng.so.2 => /usr/lib/libpng.so.2 (0x402ee000)
        libz.so.1 => /usr/lib/libz.so.1 (0x4030b000)
        libresolv.so.2 => /lib/libresolv.so.2 (0x4031b000)
        libm.so.6 => /lib/libm.so.6 (0x4032a000)
        libcrypt.so.1 => /lib/libcrypt.so.1 (0x40347000)
        libnsl.so.1 => /lib/libnsl.so.1 (0x40374000)
        liblber.so.1 => /usr/lib/liblber.so.1 (0x4038b000)
        libc.so.6 => /lib/libc.so.6 (0x40390000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)

geladen, d.h. ein


kris@valiant:~ > grep LoadModule /etc/httpd/httpd.conf | grep php
LoadModule php4_module /usr/lib/apache/libphp4.so

in der Apache-Konfiguration kann nur dann gelingen, wenn all die o.a. Bibliotheken vorhanden sind. Sind sie es nicht, schlägt das Laden des Moduls fehl:


kris@valiant:~ > su -
Password:
valiant:~ # mv /usr/lib/libpng.so.2 /usr/lib/libpng.so.2.offline
valiant:~ # ldd /usr/lib/apache/libphp4.so
        libgdbm.so.2 => /usr/lib/libgdbm.so.2 (0x4015b000)
        libpam.so.0 => /lib/libpam.so.0 (0x40162000)
        librecode.so.0 => /usr/lib/librecode.so.0 (0x4016a000)
        libdl.so.2 => /lib/libdl.so.2 (0x401e6000)
        libsnmp.so => /usr/lib/libsnmp.so (0x401ea000)
        libpdf.so.0 => /usr/lib/libpdf.so.0 (0x40225000)
        libtiff.so.3 => /usr/lib/libtiff.so.3 (0x4024e000)
        libldap.so.1 => /usr/lib/libldap.so.1 (0x40291000)
        libttf.so.2 => /usr/lib/libttf.so.2 (0x402a6000)
        libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x402cf000)

<<<     libpng.so.2 => not found >>>

        libz.so.1 => /usr/lib/libz.so.1 (0x402ee000)
        libresolv.so.2 => /lib/libresolv.so.2 (0x402fe000)
        libm.so.6 => /lib/libm.so.6 (0x4030d000)
        libcrypt.so.1 => /lib/libcrypt.so.1 (0x4032a000)
        libnsl.so.1 => /lib/libnsl.so.1 (0x40357000)
        liblber.so.1 => /usr/lib/liblber.so.1 (0x4036e000)
        libc.so.6 => /lib/libc.so.6 (0x40373000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)
valiant:~ # mv /usr/lib/libpng.so.2.offline /usr/lib/libpng.so.2
valiant:~ # logout
kris@valiant:~ >

Note: Do not try this at home, kids! Wenn man das mit der falschen Bibliothek macht (ld-linux und libc sind gute Kandidaten), kann man schon mal die Rettungs-CD rauskramen und die Termine am Nachmittag absagen.

Wenn die fehlende Bibliothek also nicht installiert ist, kann es nicht gehen. Wenn die fehlende Bibliothek nicht gefunden wird, dann ist /etc/ld.so.conf unvollständig und muß ergänzt werden. Wenn die fehlende Bibliothek nicht gefunden wird, aber installiert ist und in einem in /etc/ld.so.conf genannten Pfad installiert ist, dann muss ldconfig neu aufgerufen werden, damit /etc/ld.so.cache neu gebaut wird.


3. Allgemeine Fragen zu PHP

3.1 Wie vergleicht sich PHP mit anderen bekannten Webentwicklungssystemen?

Antwort von Kristian Köhntopp

PHP ist in Version 3 eine reine Interpretersprache, in Version 4 ein Bytecode-Compiler, der das Script beim Aufruf compiliert. PHP kann als CGI-Program oder als Bestandteil des Webservers (mod_php) ausgeführt werden. Als CGI-Programm ist es beliebig portabel, als Modul ist es für eine Reihe von populären APIs verfügbar (Apache, Netscape, Microsoft und fhttpd). PHP unterstützt Datenbankzugriffe nicht nur über ODBC (oftmals Treiber von schlechter Performance und mit unvollständigen API-Implementierungen), sondern auch über die Native API einer ganzen Reihe von Datenbanken. Dazu LDAP, IMAP und eine Reihe anderer Datenbanken, außerdem HTTP, FTP-Protokolle (Intelligent Agent-Programmierung) und Direktzugriff auf Dbase und DB/DBM-Dateiformate. Dynamische Generierung von PNG-Bildern mit der libgd und der freetype library. Volltextindices und Suchmaschinen über externe Open Source Programme (htdig und andere). Zahlreiche Spracherweiterungen vorhanden, der Quelltext des Interpreters liegt vor und ist Open Source. Die Syntax folgt der C, Java, Javascript, Perl-Familie von Sprachen. Ausgezeichnete Dokumentation und glänzender Support, wie bei Open Source üblich (Mailinglisten in Deutscher und Englischer Sprache). Sessionmanagement ab Version 4 eingebaut, davor Bestandteil von PHPLIB.

Cold Fusion ist eine Interpreter-Markup-Language. Es handelt sich um eine reine Interpretersprache mit der Option, Seitenquelltext auf der Serverseite durch "Codierung" zu verschleiern (das ausgegebene HTML muß selbstverständlich lesbar sein). Cold Fusion kann als Modul in Apache ausgeführt werden oder als CGI-Programm. Da es nicht Open Source ist, ist seine Verfügbarkeit über Plattformgrenzen beschränkt, Linux-Support ist angekündigt und im Betatest; Solaris wird ebenfalls unterstützt. Datenbankzugriff erfolgt über ODBC. LDAP, IMAP und einige andere Dinge werden unterstützt, andere Protokolle müssen ggf. über Dritthersteller oder Erweiterungen von Herstellerseite zugekauft werden. Intelligent Agents können erstellt werden. Keine Generierung von GIF-Bildern on-the-fly. CF kann Texte mit einer eigenen Engine (Verity) volltextindizieren und dann darauf zugreifen. Zahlreiche Spracherweiterungen vorhanden (Custom Tag Library beim Hersteller). Syntax: Tagschreibweise, um bei Anfängern den Eindruck einer Programmiersprache zu vermeiden. Dieser Eindruck wird unterstützt von CF-Studio, einer Spielversion von Homesite, die um teilweise lächerlich wirkenden Insert-tag Support für die CF-Standardtags erweitert wurde.

Microsoft ASP ist keine Scriptsprache, sondern ein Framework für die Einbindung einer solchen. Es stellt mehr eine API dar, die beliebige Scriptsprachen verwenden können, um Variablen über die Lebensdauer einer Seite hinaus persistent zu machen, um mit dem Webserver und anderen Scriptsprachen kommunizieren zu können und um andere, Microsoft-spezifische Eigenheiten ansprechen zu können. Mit ASP werden zwei Scriptsprachen mitgeliefert (JavaScript und VisualBasic), aber es existieren weitere, von Microsoft unabhängige Sprachen (etwa Perl), die ebenfalls ASP verwenden. ASP ist fester Bestandteil des IIS auf NT, man ist also nicht nur in der Plattform, sondern auch in der Wahl des Webservers vollkommen festgelegt. Da es sich um ein Microsoft-Produkt handelt, sind Supportoptionen, -preise und Qualität sofort klar (Hinweis: Seit Windows 2000 verlangt Microsoft eine zusätzliche Internet Connection License für den IIS, wenn dieser am Internet betrieben werden soll). Unterstützung anderer Plattformen ist nicht möglich, da ASP starken Gebrauch von den OLE/COM/DCOM-Fertigkeiten der MS-Plattform macht: Selbst nach der Portierung (etwa durch Chilisoft oder Software AG) steht man immer noch vor dem Problem, die entsprechenden OLE/COM/DCOM-Objekte auf der Nicht-Microsoft Plattform verfügbar zu machen. LDAP, IMAP und andere Dinge sind über den OLE/COM/DCOM-Mechanismus möglich, entsprechende Objekte sind eventuell von Drittanbietern zu kaufen.

mod_perl und embedPerl sind ein Mechanismus, mit denen man den Perl-interpreter als Bestandteil des Apache-Webservers ausführen kann und mit dem man dann Perl-Programme mit HTML mischen kann. Programme werden proaktiv geladen und compiliert, sodaß die typische Startup-Latenz von Perl-Programmen als CGI wegfällt. Der Speicherverbrauch ist beträchtlich, die Geschwindigkeit ist größer als bei PHP Version 3 - PHP Version 4 und mod_perl liegen in etwa gleichauf. Unterstützt wird der Apache Webserver auf allen Plattformen. mod_perl ist prinzipiell in der Lage, alle Perl-Module auf dem CPAN auszuführen, daher werden alle von Perl unterstützten Datenbanken (einschließlich ODBC und einer Menge nativer Interfaces) unterstützt, ebenso LDAP, IMAP, LibGD. Syntax folgt der von Perl... Negativ fällt hier vor allen Dingen der übermäßige Speicherverbrauch bei größeren Projekten auf und die auf Anfänger abschreckend wirkende Vielfalt von Sprachkonstrukten und Bibliotheken. Besondere Pluspunkte für erfahrene Programmierer sind die Ausdrucksstärke und Vielfalt der Sprache und ihrer Konstrukte und die wirklich umfassende Sammlung von Bibliotheken.

Next/Apple Webobjects sind ein System, mit dem ein kleines, im Source vorliegendes Rumpf-C-CGI-Programm über einen remote procedure call mit einem als Coprozess laufenden Anwendungsprogramm kommuniziert. Das Anwendungsprogramm ist mit dem Webobjects Toolkit in Objective-C geschrieben und wird dann compiliert, es ist ein Maschinenprogramm. Kommunikation erfolgt durch das Datenbank-Kit von Next über die Native API der unterstützten Datenbanken mit beliebigen Datenbanken. Next bietet außerdem eine exzellente Entwicklungsumgebung mit GUI-Designer (ja, erzeugt auch HTML :-) und einem sehr schönen ER-Modeller und Debugger (gdb mit einem sexy Outfit). Webobjects skaliert sich ausgezeichnet durch das verwendete RPC-Schema (Next Portable Distributed Objects auf einem beliebigen etablierten RPC-Mechanismus aufsetzend) und die Möglichkeit, den Application Server zu replizeren. Interessant ist die ungeschlagene Geschwindigkeit und die extrem gute Entwicklungsumgebung.

3.2 Wie vergleicht sich die Performance von PHP zu Perl?

Antwort von Kristian Köhntopp

Eine Zählschleife in PHP3 ist um etwa den Faktor 10 langsamer als dieselbe Zählschleife in Perl, weil PHP3 die Zeilen der Schleife bei jedem Durchlauf erneut parsed und dann ausführt, während Perl beim Start des Interpreters Bytecode erzeugt und dann ausführt.

Typische Programme bestehen nicht aus Zählschleifen, sondern aus komplexen Funktionen, mit denen man das eigentliche Ziel des Programmes bewirken will. Solche Funktionen sind in Perlprogrammen in der Regel in perl realisiert, während diese Funktionen in PHP3 eher in C geschrieben sind. C ist in solchen Fällen etwa um den Faktor 5-10 schneller als Perl. Bytecode-Compiler haben außerdem eine Startup-Zeit, die ebenfalls in die Laufzeit des Programmes mit eingeht.

Wer vorne liegt, hängt extrem von der Aufgabe und dem verwendeten Code ab.

Im Zweifel kann man in beiden Sprachen die entsprechende Funktion als Spracherweiterung in C implementieren und ist so in jedem Fall schneller als mit einer PHP- oder Perlfunktion. PHP3 und PHP4 liegen hier wegen des im Vergleich zu Perl5 sehr viel besser strukturierten Sourcecodes vom Entwicklungsaufwand deutlich vorne. Ein Perl-Projekt, das den Perl-Interpreter neu schreiben will, ist derzeit in Arbeit.

PHP4 erzeugt wie Perl beim Start des Programmes Bytecode und führt diesen dann aus. Dabei liegt PHP4 mit Perl geschwindigkeitsmäßig in etwa gleichauf. Ebenso wie Perl braucht auch PHP4 dafür eine größere Startup-Zeit, in der das Programm analysiert und übersetzt wird.

Der Mitte März zum Betatest freigegebene Zend-Optimizer optimiert den PHP4-Bytecode noch einmal und holt je nach Anwendung noch einmal Geschwindigkeitsgewinne heraus. Diese Gewinne können in Benchmarks sehr signifikant sein - ihre praktische Bedeutung wird ebenfalls merkbar sein, aber sicherlich nicht so extrem wie in den Benchmarks. Die Anwendung des Optimizers erhöht die Startup-Zeiten des PHP4-Interpreters noch weiter. Der Optimizer ist ein closed source Produkt und wird nach Abschluss der Beta-Phase käuflich zu erwerben sein.

Der noch freizugebende Zend-Compiler kann den PHP4-Bytecode oder den optimierten PHP4-Bytecode speichern. Der Interpreter kann diesen Bytecode dann laden und sofort (ohne weitere Startup-Kosten fuer die Übersetzung) ausführen. Außerdem können in Bytecode übersetzte Programme an Kunden herausgegeben werden, ohne daß der Kunde einen Blick in den Source werfen kann. Der Compiler wird ein closed source Produkt sein und nach Abschluss der Beta-Phase käuflich zu erwerben sein.

Der noch freizugebende Zend-Cache kann häufig verwendeten Bytecode erkennen und im Interpreter im Speicher halten. Der Interpreter braucht diesen Bytecode dann nicht mehr zu laden, zu analysieren und zu übersetzen, sondern kann ihn direkt aufrufen. Durch den Zend-Cache erhöht sich der Speicherverbrauch des Interpreters stark und bei unzureichendem Speicherausbau der Maschine kann sich das negativ auf die Performance des Gesamtsystems auswirken. Der Zend-Cache wird ein closed source Produkt sein und nach Abschluss der Beta-Phase käuflich zu erwerben sein.

Man sagt, daß PHP3 und PHP4 leichter zu erlernen sind als Perl und daß PHP-Code leichter zu lesen und damit billiger zu warten sei als Perl-Code. Das ist sicherlich eine Frage der Übung - man kann in beiden Sprache nicht mehr wartbare Programme entwickeln bzw. in beiden Sprachen selbstdokumentierenden Code abliefern.

In PHP ist die Versuchung, kryptischen Code abzuliefern möglicherweise etwas kleiner als in Perl, aber speziell PHP3 ist hier in einigen Punkten behindert, weil es in der Sprache Referenzen (Zeiger auf Variablen) keine Objekte erster Ordnung sind und nur sehr eingeschränkt verwendet werden können (nur bei der Parameteruebergabe an Funktionen) und daher manchmal einige ekelige Workarounds notwendig machen. Dieses Problem ist erst in PHP4 behoben.

3.3 Wie kann ich mein ASP-Programm in PHP übersetzen?

Antwort von Kristian Köhntopp

Mit Hilfe des Konverters asp2php kann man große Teile seiner ASP-Anwendung zunächst einmal in PHP übersetzen lassen. In vielen Fällen ist kleinere Nacharbeit oder wenigstens Nachkontrolle notwendig.

Viele Eigenschaften des ASP-Framework lassen sich mehr oder weniger direkt in PHP abbilden:

ASP Application Object

PHP hat kein Application Object, aber die Funktionalität läßt sich etwa mit Hilfe der Shared Memory und Semaphore-Funktionen leicht nachbilden.

ASP Request Object

Das ASP Request-Objekt gibt Zugriff auf Client-Zertifikate, Cookies, Formularvariablen, den Query-String und verschiedene Servervariablen.

In PHP greift man auf Cookies als globale Variablen zu, oder über das Array $HTTP_COOKIE_VARS. Formularvariablen werden ebenfalls in den globalen Namensraum importiert oder sie sind in den Arrays $HTTP_GET_VARS und $HTTP_POST_VARS abgreifbar. Der Query-String und die Servervariablen sind ebenfalls globale Variablen oder können über getallheaders() erreicht werden. Die Ausgabe von phpinfo() gibt einen sehr schönen Überblick.

ASP Response Object

Das ASP Response Object hat einen Slot für Cookies, die Eigenschaften Buffer, ContentType, Expires, ExpiresAbsolute und Status sowie die Methoden AddHeader, AppendToLog, BinaryWrite, Clear, End, Flush, Redirect und Write.

PHP Version 3 kennt keine Pufferung und daher auch keine Clear-Methode zu Löschen des Puffers. Stattdessen arbeitet man einfach mit einem beliebigen String, an den man mit Code wie


  $buf .= sprintf("My Output<br>");
  $buf .= sprintf("Goes here<br>");

anhängt und den man dann entweder löschen oder ausgeben kann:


  print $buf; # Flush
  $buf = "";  # Clear

In PHP Version 4 gibt es den Output-Buffer, in den alle Ausgaben des Interpreters laufen, wenn dies durch die Konfigurationsvariable output_buffering eingeschaltet wird. Mit Hilfe der Funktionen ob_start(), ob_end_flush() (entspricht Flush) und ob_end_clean() (entspricht Clear) kann man den Puffer kontrollieren, mit Hilfe von ob_get_contents() bekommt man eine Kopie des Puffers zurückgegeben und kann ihn im Programm weiter manipulieren.

In PHP kann man Header und Cookies nur solange erzeugen wie noch kein Text ausgegeben wurde. Cookies werden mit der setcookie()-Funktion erzeugt. HTTP-Headerzeilen erzeugt man stattdessen mit header()-Funktion. Diese kann eingesetzt werden, um den Content-Type, den Status, die Expires-Zeit oder beliebige andere Eigenschaften der Seite zu manipulieren und kann auch Redirects erzeugen.


  # Content Type setzen
  header("Content-Type: image/gif");

  # Expires bestimmen
  $exp_gmt = gmdate("D, d M Y H:i:s", time()+$exp) . " GMT");
  header("Expires: " . $exp_gmt);

  # Status setzen
  header("Status: 401 Unauthorized");

  # Redirect erzeugen
  header("Location: http://..../.../inded.html");

Lognachrichten erzeugt in PHP die Funktion syslog(). BinaryWrite entfällt, weil PHP-Funktionen binärsicher sind - man kann eine gewöhnliche Ausgabefunktion verwenden (siehe auch die Parameter rb und wb bei fopen(). Das Äquivalent zu End ist exit().

ASP Server object

Das ASP Server Objekt hat die Eigenschaft ScriptTimeout und die Methoden CreateObject, HTMLEncode, MapPath und URLEncode.

In PHP kann man die Laufzeit eines Scriptes und den Speicherverbraucht mit Hilfe der Konfigurationsvariablen max_execution_time und memory_limit begrenzen.

Zum Codieren und Decodieren bietet PHP eine reichhaltige Familie von Funktionen und anders als ASP auch die passenden Decode-Funktionen. Die wichtigsten sind: rawurlencode(), rawurldecode(), urlencode(), urldecode(), base64_encode(), base64_decode(), quoted_printable_decode(), htmlspecialchars(), htmlentities(), addslashes(), stripslashes() und quotemeta().

ASP verwendet CreateObject, um COM-Objekte zu erzeugen und zu steuern. In Unix gibt es kein COM (aber eine Reihe von PHP-Erweiterungen, die genau die Funktionalität der COM-Objekte hhaben). In Windows NT erlaubt PHP Version 4 mit den Funktionen com_load(), com_invoke(), com_propget() und com_propput() den Zugriff auf COM-Objekte. Außerdem ist PHP Version 4 in der Lage, Objekteimplementationen zu überladen, sodaß man COM-Objekte wie native PHP-Objekte verwenden kann.

Verwendet man PHP als Apache-Modul, kann man die Funktion MapPath durch apache_lookup_uri() nachbilden, die jedoch noch sehr viel mehr Information zurückliefert als nur das Path-Mapping. In jedem Fall kann man sich für die eigene URL mit den Variablen $PATH_INFO und $PATH_TRANSLATED behelfen.

ASP Session object

Das ASP Session Objekt hat die Eigenschaften SessionID und Timeout und die Methode abandon.

In PHP Version 3 mit PHPLIB verwendet man $sess->id für die Session-ID und $sess->lifetime für den Timeout. $sess->delete() entspricht abandon.

In ASP speichert man Werte in seiner Session mit Hilfe von


  Session("memo") = "remember me"

In PHPLIB schreibt man


  $memo = "remember me";
  $sess->register("memo");

Die Funktionalität von Session_OnStart wird in PHPLIB mit Hilfe der Sessioneigenschaft $auto_init nachgebildet. PHPLIB hat außerdem Uservariablen. Diese gibt es nicht in ASP.

3.4 CGI PHP oder Modul?

Antwort von Kristian Köhntopp

Siehe auch Webserver verstehen und tunen von Kristian Köhntopp.

Jedesmal, wenn eine Seite mit PHP-Code darauf ausgeführt werden muß, muß der PHP-Interpreter gestartet werden. Wird PHP als CGI-Programm installiert, bedeutet dies, daß am Anfang der Seite ein neuer Prozeß erzeugt werden muß und der PHP-Interpreter in diesen Prozeß geladen werden muß. Dies verbraucht eine ganze Menge Systemressourcen. Am Ende der Seite endet auch der Interpreterprozeß und aller Speicher wird freigegeben. Ebenso werden alle Filehandles geschlossen und damit alle Datenbankverbindungen aufgegeben.

Installiert man PHP dagegen als Modul, etwa in einem Apache-Webserver, dann ist das PHP-Modul Bestandteil des Webservers und ständig geladen. Es kann außerdem Datenbanklinks über die Lebensdauer einer PHP-Seite hinaus offen halten, was speziell bei Oracle-Datenbanken große Performancevorteile bringt.

Nur mit einem CGI-PHP ist es möglich, PHP als Scriptsprache zur Erstellung von "Shellscripten" einzusetzen.

Viele Massen-Webhoster setzen bevorzugt CGI-PHP sein, weil es sich leicher auf eine Weise installieren läßt, die die Systemsicherheit nicht gefährdet (Einsatz der Apache-Erweiterung suexec, Erstellung einer chroot()-Umgebung) und in der sich die durch den Anwender verbrauchten Systemressourcen besser kontrollieren lassen.

Praktisch alle großen PHP-Sites setzen PHP als Modul ein, weil die Performance hier deutlich besser ist.

3.5 PHP-Scripte von Windows nach Unix portieren?

Antwort von Kristian Köhntopp

PHP ist weitgehend unabhängig von der verwendeten Systemplattform. Von den Funktionen microtime() und crypt() ist bekannt, daß sie sich beiden Betriebssystemen unterschiedlich verhalten.

Einige PHP-Module stehen nicht unter allen Systemplattformen zur Verfügung. So ist das Posix-Modul systembedingt nur unter Unix sinnvoll einsetzbar und das DCOM-Modul nur unter Windows verfügbar.

3.6 Welche Editoren sind für PHP geeignet?

Antwort von Kristian Köhntopp

Prinzipiell ist selbstverständlich jeder ASCII-Editor für PHP geeignet. Eine generische Liste in englischer Sprache wird von Keith Edmunds auf PHP Editors gepflegt. Folgende Editoren (Auswahl) bieten spezielle Unterstützung für PHP (z.B. Syntax-Hervorhebung):

In Unix (alphabetisch):

In Windows (alphabetisch):

Für den Apple Mac (alphabetisch):

Manche Editoren können sehr gut mit Microsoft Active Server Pages arbeiten, verändern aber PHP Code. In diesem Fall ist es sinnvoll, in der php.ini die Konfigurationsvariable asp_tags zu aktivieren und seinen Code mit Hilfe von <% ... %> zu schreiben. Speziell für Frontpage und ältere Versionen von Dreamweaver ist dies hilfreich.

3.7 Zeitgesteuerte PHP-Scripte und Shellscripte

Antwort von Kristian Köhntopp

"Hello, World!" als PHP-Script "hello.php":


#! /usr/local/bin/php -q --
<?php
  echo "argc=$argc\n";
  reset($argv);
  while(list($k, $v) = each($argv)):
    echo "argv[$k]=$v\n";
  endwhile;
 ?>

Dies setzt eine installierte CGI-Version von PHP in /usr/local/bin/php voraus. Ein solches Script läßt sich über die Unix-Zeitsteuerung cron regelmäßig aufrufen.

Dem Script steht das Array $argv[] zur Verfügung, welches die Kommandozeilenparameter des Aufrufs enthält. Dieses Array kann auf die übliche Weise aufgezählt werden. Die Anzahl der Elemente des Arrays kann man mit der Funktion count() bestimmen oder in der Variablen $argc nachschlagen.

Hat man nur mod_php zur Verfügung, kann man eine bestimmte URL des Webservers durch PHP regelmäßig zeitgesteuert abrufen lassen. Dazu sind Tools wie wget (Suse Linux Paket wget in Serie n1) oder lynx (Suse Linux Paket lynx in Serie n1) hilfreich.

3.8 Wie bette ich PHP in HTML ein? (Beispielprogramm)

Antwort von Kristian Köhntopp

PHP-Code wird mit Hilfe von SGML Processing Instructions (PIs) in HTML eingebettet. Der Code steht zwischen <?php und ?>:


<?php
  phpinfo();
 ?>

Alternativ kann auch ein Script-Tag für Server-Side Scripting verwendet werden:


<script language="php">
  phpinfo();
</script>

Falls die Konfigurationsvariable short_open_tag gesetzt ist, kann man den Namen der Scriptsprache in den PIs weglassen:


<?
  phpinfo();
 ?>

Falls die Konfigurationsvariable asp_tags gesetzt ist, kann man auch


<%
  phpinfo();
 %>

a la Microsoft schreiben. Dies wird von vielen Editoren (Frontpage, Dreamweaver) besser verstanden. Außerdem steht einem dann das Kürzel <%= $somevar %> zur Verfügung:


<!-- Dies -->
<%= $PHP_SELF %>
<!-- kann anstelle von -->
<% echo $PHP_SELF %>
<!-- geschrieben werden. -->

3.9 Wie finde ich heraus, wie mein PHP-Interpreter konfiguriert ist?

Antwort von Kristian Köhntopp

Die PHP-Funktion phpinfo() liefert eine Fülle von Information über die aktuelle Konfiguration des Interpreters. Das typische Testprogramm sieht so aus:


<?php
  phpinfo();
 ?>

3.10 Wo finde ich die php3.ini bzw. die php.ini?

Antwort von Kristian Köhntopp

Die Ausgabe von phpinfo() sagt unter anderem, wo der laufende PHP-Interpreter seine php3.ini- (bei PHP3) bzw. seine php.ini- (bei PHP4) Datei sucht. Die Datei wird immer zuerst im aktuellen Verzeichnis und dann an der Stelle gesucht, die in der Ausgabe von phpinfo() bezeichnet wird.

3.11 Wie kann ich auf Umgebungsvariablen zugreifen?

Antwort von Kristian Köhntopp

Man kann Umgebungsvariablen über die PHP-Einbaufunktion getenv() eine solche Variable lesen und sie mit Hilfe der PHP-Einbaufunktion putenv() setzen. Dies ist die empfohlene Methode.

Alternativ sind Umgebungsvariablen sind innerhalb von PHP als Variablen im Array $HTTP_ENV_VARS verfügbar. Die Umgebungsvariable HOME steht also als Element im Array $HTTP_ENV_VARS["HOME"] zur Verfügung. Wenn innerhalb einer Funktion auf den Wert einer solchen globalen Variablen zugegriffen werden soll, darf man nicht vergessen, dieses Array als global zu deklarieren.

Hinweis: In PHP3 stehen diese Variablen noch als globale Variablen zur Verfügung. Dies ist ein Sicherheitsrisiko. In PHP4 stehen diese Variablen im Array $HTTP_ENV_VARS zur Verfügung, und als globale Variablen, wenn die Einstellung register_globals in der php.ini aktiviert ist. Es ist empfohlen, register_globals auf Off zu stellen.


<%
  function somefunc() {
    # Empfohlen
    echo getenv("HOME"). "<BR>\n";

    # So geht es auch.
    global $HTTP_ENV_VARS;
    echo $HTTP_ENV_VARS["HOME"]<BR>\n";
  }

  somefunc();
 %>

Mit Hilfe der Funktion phpinfo() bekommt man unter anderem auch eine Übersicht über alle diese Variablen.

3.12 Wie kann ich auf den HTTP-Request-Header zugreifen?

Antwort von Kristian Köhntopp

Allen CGI-Programmen stehen zusätzliche Zeilen des HTTP-Request-Headers als Umgebungsvariablen zur Verfügung. Die Headerzeile bla-fasel: laber wird dabei zur Umgebungsvariablen HTTP_BLA_FASEL mit dem Wert laber.

Zusätzliche Angaben hinter der URL der Seite stehen in der Umgebungsvariablen PATH_INFO zur Verfügung. So wird im Request http://meinserver.de/test.php3/additional/info der Anteil /additional/info in dieser Variablen zu finden sein.

Zusätzliche Angaben in der Form von Query-Strings wie sie von Formularen mit der METHOD=get erzeugt werden, stehen in der Variablen QUERY_STRING zur Verfügung. Ist der Query-String korrekt formuliert, werden diese Parameter auch als globale Variablen vorbelegt. So wird der Request http://meinserver.de/test.php3?a=b&c=d die Umgebungsvariable QUERY_STRING mit dem Wert a=b&c=d belegen und außerdem die globale Variable $a mit dem Wert b und die globale Variable $c mit dem Wert d vorbelegen.

Die Interpretation von Query-Strings wird außerdem noch durch die Konfigurationsparameter gpc_order und track_vars in der globalen Konfigurationsdatei php3.ini gesteuert.

Verwendet man PHP als Apache-Modul, so kann man außerdem noch über die spezielle Funktion getallheaders() auf diese Request-Header zugreifen. Ein solches Script ist jedoch nicht mit einem CGI-Interpreter ausführbar, daher sollte man diese Funktion im Namen der Portabilität vermeiden.

3.13 Gibt es noch mehr interessante Variablen im Environment?

Antwort von Kristian Köhntopp

Der CGI-Standard definiert eine Reihe von Variablen, die vorhanden sein müssen. Eine Übersicht bekommt man wie üblich mit phpinfo() Von besonderem Interesse sind

HTTP_REFERER

Die URL der Seite, die auf diese Seite verwiesen hat.

HTTP_USER_AGENT

Die Browserkennung des abrufenden Browsers.

REMOTE_ADDR

Die IP-Nummer des abrufenden Rechners. Dies kann die momentane IP-Nummer des tatsächlichen Abrufers (oft dynamisch vergeben und variabel) oder die IP-Nummer des Proxy-Servers sein, den der Abrufer verwendet.

3.14 Ich verwende PHP (Version 3) als Apache-Modul. Wie kann ich dies konfigurieren?

Antwort von Kristian Köhntopp

Immer wenn von einer Konfigurationsanweisung der Form name=wert für die Konfigurationsdatei php3.ini die Rede ist, dann kann für mod_php in Apache stattdessen auch die Apache-Konfigurationsdirektive php3_name wert verwendet werden. Man beachte, daß dem Namen der Anweisung php3_ vorangestellt wird und daß in einer Apache-Konfigurationsdatei keine Gleichheitszeichen verwendet werden dürfen.

Die Konfigurationsdirektiven können in der httpd.conf in einem <directory>-Block oder in einer .htaccess-Datei stehen. Sie gelten für das bezeichnete Verzeichnis und alle seine Unterverzeichnisse. Dies erlaubt es, die PHP-Konfiguration pro Verzeichnis anzupassen.

Mit Hilfe der PHP-Funktion get_cfg_var.php() lassen sich Konfigurationsvariablen aus der php3.ini zur Laufzeit abfragen.

3.15 Was bedeuten master value und local value in phpinfo()?

Antwort von Kristian Köhntopp

Die Unterscheidung hat nur in der Modulversion von PHP einen Sinn:

Der Master Value ist der Wert, der in der php3.ini (PHP4: php.ini) eingetragen ist. Diese Datei wird nur beim Neustart des PHP-Interpreters (beim Modul also beim Neustart des Webservers) eingelegen und beachtet. Diese Werte gelten überall auf dem Server, wenn kein besonderer local value definiert ist.

Der Local Value ist der Wert, der in diesem Verzeichnis gilt. Er kann in einem Apache <Directory>-Block oder in einer .htaccess-Datei eingetragen sein. In ersterem Fall wird er beim Neustart des Servers neu gelesen, in letzterem Fall wird er bei jedem Zugriff neu gelesen und beachtet.

3.16 Welche Konfigurationsvariablen kann ich nicht in .htaccess-Dateien verwenden?

Antwort von Kristian Köhntopp

Sicherheitsrelevante Konfigurationseinträge sind nicht über .htaccess-Dateien steuerbar, sondern nur über Einträge in <Directory> oder <Location>-Blocks in der zentralen Konfigurationsdatei. Sonst wären sie auch sinnlos, da die .htaccess-Dateien ja durch den Anwender kontrolliert werden.

Aus mod_php3.c::php3_commands[]:


        {"php3_safe_mode", php3flaghandler, (void *)4,
                ACCESS_CONF|RSRC_CONF, FLAG, "on|off"},

Aus /usr/local/httpd/include/http_config.h:


/* The allowed locations for a configuration directive are the union
 * of those indicated by each set bit in the req_override mask.
 *
 * (req_override & RSRC_CONF)   => *.conf outside <Directory>
 *                                 or <Location>
 * (req_override & ACCESS_CONF) => *.conf inside <Directory>
 *                                 or <Location>
 * (req_override & OR_AUTHCFG)  => *.conf inside <Directory>
 *                                 or <Location> and .htaccess
 *                                 when AllowOverride AuthConfig
 * (req_override & OR_LIMIT)    => *.conf inside <Directory>
 *                                 or <Location> and .htaccess
 *                                 when AllowOverride Limit
 * (req_override & OR_OPTIONS)  => *.conf anywhere and .htaccess
 *                                 when AllowOverride Options
 * (req_override & OR_FILEINFO) => *.conf anywhere and .htaccess
 *                                 when AllowOverride FileInfo
 * (req_override & OR_INDEXES)  => *.conf anywhere and .htaccess
 *                                 when AllowOverride Indexes
 */

Wie man sieht, ist die Anweisung php3_safe_mode nur in der httpd.conf erlaubt, nicht jedoch in .htaccess-Dateien. Die folgenden Anweisungen sind sicherheitsrelevant und können nicht in einer .htaccess-Datei verwenden werden, sondern müssen direkt in den Server konfiguriert werden:

Sicherheitshinweis:

php3_sendmail_path ist erst seit PHP 3.0.15 als sicherheitsrelevant gekennzeichnet. In kleineren Versionen von PHP kann die Konfigurationsanweisung in .htaccess-Dateien verwendet werden und ist geeignet, aus dem safe_mode auszubrechen und um beliebige Kommandos unter der UID des Webservers auszuführen: PHP versendet in Unix eine Mail dadurch, daß die mail()-Funktion die Unix-Funktion popen() mit dem in php3_sendmail_path konfigurierten Kommando aufruft.

Konfiguriert man jetzt ein beliebiges Kommandos mit php3_sendmail_path in seiner .htaccess-Datei und ruft dann mail() auf, dann wird PHP dieses Kommando starten und die Mail auf der Standardeingabe reinpipen. Das Kommando läuft unter der UID des Webservers (bei einigen nicht richtig schlauen Webhostern also als root) und stellt dann die Mail zu oder auch nicht und macht noch beliebige andere Dinge.

Webhoster, die PHP3 mit safe_mode einsetzen, sollten dringend auf die aktuelle PHP3-Version updaten.

3.17 Was genau bewirkt safe_mode und ist das sicher?

Antwort von Kristian Köhntopp

Es gibt eine Konfigurationsvariable safe_mode, die in der php3.ini gesetzt werden kann. Weiterhin gibt es die Konfigurationsvariablen safe_mode_exec_dir und sql_safe_mode.

Wenn safe_mode aktiv ist, sind verschiedene PHP3-Funktionen privilegiert oder eingeschränkt. Zumeist gilt die Einschränkung safe_mode Einschränkung, daß auf eine Datei oder ein Verzeichnis nur eingewirkt werden darf, wenn die Datei oder das Verzeichnis denselben Eigentümer hat wie das Script. Im einzelnen:

Wenn sql_safe_mode aktiv ist, können bei einem MySQL-Connect host, user und password nicht angegeben werden ("SQL safe mode in effect - ignoring host/user/password information").

safe_mode ist nicht sicher: Ein Fehler in der popen()-Funktion ist erst mit 3.0.14 korrigiert worden, ein weiterer Fehler in der mail()-Funktion erst in 3.0.15. Man sollte stattdessen die CGI-Version in einem chroot-Environment verwenden und mit setrlimit noch weitergehende Einschränkungen definieren.

3.18 "Fatal error: Maximum execution time exceeded"

Antwort von Kristian Köhntopp

Mit Hilfe des Parameters max_execution_time in der php3.ini läßt sich die maximale Laufzeit eines PHP-Scriptes in Sekunden festlegen. Der Interpreter beendet sich selbst, wenn ein Script mehr als die dadurch zugewiesene Zeit läuft.

Wenn kein safe_mode aktiviert ist, kann ein Script sich selbst dieses Laufzeit-Limit mit Hilfe der Funktion set_time_limit() neu setzen.

3.19 Was ist --enable-force-cgi-redirect? Warum enthält $PHP_SELF den Pfad zum CGI-Interpreter?

Antwort von Kristian Köhntopp

Wenn CGI-PHP eingesetzt wird, bekommt der Interpreter den Pfad zum PHP3-Script in der CGI-Variablen $PATH_INFO übergeben. Der Aufruf eines PHP-Scriptes erfolgt also mit Hilfe einer URL der Form


http://www.meinserver.de/cgi-bin/php/somedir/somescript.php3

Der PHP-Interpreter wird dabei unter der URL http://www.meinserver.de/cgi-bin/php erreicht und die Variable $PATH_INFO erhält den Wert /somedir/somescript.php3. Der PHP-Interpreter greift sich den Wert der Konfigurationsvariablen document_root aus der php3.ini und setzt den Dateinamen des Scriptes aus dieser Variablen und $PATH_INFO zusammen.

Dies ist natürlich gefährlich, denn ein böswillger Anwender könnte jetzt anfangen, Scripte wie folgt aufzurufen:


http://www.meinserver.de/cgi-bin/php/../../../../../etc/passwd

Der PHP-Interpreter würde nun anfangen, die passwd-Datei als Script zu laden und auszuführen, d.h. die Paßwort-Datei auszugeben. Auf diese Weise kann man prinzipiell auf jede Datei im System zugreifen, sofern diese durch den Benutzer lesbar ist, unter dessen UID der Interpreter abläuft.

Übersetzt man CGI-PHP mit der Konfigurationsoption enable-force-cgi-redirect, funktioniert dies nicht mehr. PHP startet in diesem Fall nur noch, wenn die CGI-Umgebungsvariable $REDIRECT_STATUS gesetzt ist (der Wert der Variablen ist egal). Der Apache Server setzt diese Variable, wenn der Zugriff auf das CGI-Verzeichnis die Folge eines eines serverinternen Redirect ist. Üblicherweise konfiguriert man seinen Webserver dann so:


  Action     php3-script /cgi-bin/php
  AddHandler php3-script .php3

Die Direktive AddHandler bildet die Endung .php3 auf den Handler php3-script (der Name ist frei wählbar, solange er eindeutig ist) ab. Die Direktive Action deklariert eine externe Aktion, die durch ein CGI-Programm realisiert wird. In Kombination bewirken beide Direktiven, daß alle Zugriffe auf Dateien mit der Endung .php3 auf das CGI-Programm /cgi-bin/php redirected werden. Dies erzeugt als Nebenwirkung genau die benötigte $REDIRECT_STATUS-Variable.

Die beiden Apache-Konfigurationsdirektiven bewirken, daß Dateien mit der Endung .php3 im PHP-Interpreter landen und die Abfrage auf die $REDIRECT_STATUS-Variable bewirkt, daß ein direkter Aufruf des PHP-Interpreters mit einem manipulierten $PATH_INFO nicht mehr möglich ist. In Kombination stellen beide Mechanismen ein vernünftiges Maß an Sicherheit her.

PHP berechnet nun den Wert der Variablen $PHP_SELF auf andere Weise, falls enable-force-cgi-redirect in Kraft ist: In diesem Fall enthält die Variable wirklich nur den Namen des Scriptes ohne den Pfad zum PHP-Interpreter. Ist der Interpreter jedoch ohne diese Einstellung übersetzt worden, ist der Name des PHP-Interpreters Bestandteil von $PHP_SELF. Dies ist ein Hinweis auf ein schweres Sicherheitsproblem. Der Interpreter sollte ausgetauscht werden durch eine Version, die mit enable-force-cgi-redirect übersetzt wurde.

Manche Webserver sind nicht in der Lage, Endungen auf CGI-Programme zu mappen (beim Netscape Server kann man dies mit Hilfe eines Plugin-Modules nachrüsten) oder sie erzeugen keine $REDIRECT_STATUS-Variable, wenn sie ein solches Mapping vorgenommen haben. In diesem Fall muß man ohne enable-force-cgi-redirect arbeiten und mit dem Sicherheitsloch leben oder - besser - den Webserver wechseln.

3.20 Warum funktioniert set_time_limit() nicht wie angepriesen?

Antwort von Kristian Köhntopp

Die Funktion set_time_limit() bzw. die Konfigurationsanweisung max_execution_time in der php3.ini wirkt nicht auf die absolute Laufzeit des Scriptes, sondern sie begrenzt die verbrauchte CPU-Zeit eines Scriptes. In


  set_time_limit(1);
  sleep(10);
  print("hallo");

verbraucht die sleep()-Funktion zwar reale Zeit, aber keine CPU-Zeit. Daher wird das Zeitlimit von einer Sekunde hier auch nicht wirksam und der Text wird noch gedruckt.

3.21 Was ist das für ein @-Zeichen vor einigen Funktionsaufrufen?

Antwort von Kristian Köhntopp

Manche PHP-Anweisungen können Fehlermeldungen oder Warnungen generieren. Diese Meldungen werden bei der Ausführung von Scripten Bestandteil der Ausgabe, wo sie unter Umständen sehr stören können. Stellt man einem Funktionsaufruf ein @-Zeichen voran, wird der Interpreter die Ausgabe der Meldung unterdrücken.

Wenn die Konfigurationsanweisung track_errors aktiv ist, werden die Meldungen stattdessen in der Variablen $php_errormsg hinterlegt.

Diese Eigenschaften von PHP sind im Kapitel Error Handling dokumentiert.

3.22 Wie kann ich auf Kommandozeilen-Argumente zugreifen?

Antwort von Kristian Köhntopp

Wenn PHP über die Shell als Skriptsprache benutzt wird, ist es oft nützlich, auf der Kommandozeile Parameter zu übergeben. In PHP stehen die Variablen $argc und $argv zur Verfügung.

$argc

Anzahl der auf der Kommandozeile übergebenen Argumente.

$argv

Array mit den übergebenen Argumenten und dem Dateinamen im ersten Element.

Wenn ein PHP-Skript über das Web aufgerufen wird, enthalten diese Variablen die über GET übergeben Argumente. In PHP 4.0 kann man dieses Verhalten in der php.ini-Datei abschalten (register_argv_argc).

Beispiel:


tobias@dev:~ > cat arg.php3
#!/usr/bin/php -q
<?
for($i=0; $i<$argc; $i++)
    print($argv[$i]."\n");
?>

tobias@dev:~ > ./arg.php3 foo bar baz
./arg.php3
foo
bar
baz

3.23 Wie kann ich einen Parameter von einer PHP-Seite an eine andere weitergeben?

Antwort von Kristian Köhntopp

Man kann Parameter an ein PHP-Script als HTTP-GET- oder HTTP-POST-Parameter übergeben. Die Übergabe von Parametern als HTTP-POST ist in Wie kann ich einen HTTP POST-Request absenden? erläutert.

Einen HTTP-GET-Request erzeugt man, indem man einfach ein Link auf das gewünschte Script erzeugt und die Parameter mit der Funktion urlencode() codiert anhängt.


<?php

 $para1 = "dies ist ein string";
 $para2 = 42;

 $pstring = sprintf("para1=%s&amp;para2=%s",
                urlencode($para1),
                urlencode($para2));
?>
<a href="meinscript.php3?<?php print $pstring ?>">go</a>

Das empfangende Script wird diese Parameter ganz normal entgegennehmen, automatisch decodieren und als globale Variablen mit den Namen $para1 und $para2 bereitstellen.

Die Länge der durch einen GET-Request übergebaren Parameter ist begrenzt. Im einem GET- oder POST-Request übergebene Parameter sind durch den Anwender leicht manipulierbar. Wie in Webserver verstehen und tunen diskutiert, ist es wesentlich besser, Sessionvariablen zu verwenden, wie sie etwa durch PHPLIB oder mit Hilfe der Sessionfunktionen von PHP4 realisiert sind - im Gegensatz zu dem hier gezeigten manipulierbaren Verfahren sind Sessions nämlich sicher.

3.24 Wie kann ich eine PHP-Präsentation auf CD brennen?

Antwort von Kristian Köhntopp

PHP ist zur Ausführung auf einen Webserver angewiesen. Es ist zwar möglich, eine PHP-Präsentation mittels eines Spiders durchzugehen und das generierte HTML abzuspeichern, aber die Interaktivität und die Datenbankverbindungen gehen so verloren.

Alternativ kann man dem Kunden einen Webserver mitliefern, den dieser dann auf seinem Rechner installieren muß und der dann die PHP-Scripte ausführt. Der Kunde wird dann unter der URL http://localhost/ auf die Anwendung zugreifen können.

3.25 Werden meine PHP-Seiten von einer Suchmaschine indiziert?

Antwort von Kristian Köhntopp

Ein Webspider bekommt bei der üblichen Konfiguration von Webservern in keinster Weise mit, ob die angeforderte Default-Datei (Directory-Index) eines Verzeichnisses nun home.htm, index.php3 oder irgendwie anders heißt.

Er wird diese Datei lesen, und dann werden die HTML-Tags extrahiert, die für das Ranking aber noch eine Rolle spielen können (<h1>...</h1>). Aus dem reinen Text werden meistens noch die Stopwörter entfernt und restliche Textbrei fließt dann aufbereitet in den Index. Die MetaTags spielen bei intelligenteren Suchmaschinen für das Ranking meist kaum noch eine Rolle.

Jede Suchmaschine könnte grundsätzlich dynamisch generierte Seiten genauso erfassen, wie statische Seiten, weil der Spider/Robot der Suchmaschine genauso ein Client ist, wie Dein Browser und nicht mehr und nicht weniger sieht als Dein Browser: nämlich den HTML-Code und den Content-Type. Endungen der Dateinamen spielen bei korrekt programmierten Suchmaschinen keine Rolle - entscheidend sind im Web stattdessen die übermittelten Content-Types.

Viele Suchmaschinenbetreiber werden jedoch keine dynamisch generierten Seiten erfassen, weil sie davon ausgehen, daß sich deren Inhalt sehr oft ändert und eine Indizierung der Seiten somit sinnlos ist. Wird nun bei einem HTML-Dokument aufgrund der Extension, des Pfadnamens (enthält das Schlüsselwort cgi) oder offensichtlich per GET übergebener Parameter eine dynamische Generierung vermutet, werden diese Dateien von einigen Spidern nicht indiziert, bzw. entsprechende Links nicht verfolgt. Dies ist zwar ebenfalls falsch - stattdessen sollte sich der Spider nur nach dem Inhalt der Datei robots.txt richten - aber viele Sites haben nur ungenügende oder ganz fehlende robots.txt-Dateien.

3.26 Wie kann ein Besucher meiner Seite den PHP-Code im Browser sehen?

Antwort von Martin Jansen

Wenn der Webserver ordnungsgemäß konfiguriert ist und er PHP unterstützt, bekommt der Besucher der Seite den PHP-Code nicht zu sehen, da er vom Webserver geparst wird und nur der generierte HTML-Code an den Browser geschickt wird.

Folgende Szenarien sind jedoch denkbar, bei denen der Besucher den PHP-Code trotzdem zu sehen bekommt:

In einer der folgenden Versionen von PHP4 wird es einen Compiler geben, der es dem Programmierer ermöglicht, seinen Code zu schützen.

In jedem Fall ist es sinnvoll, Code mit Datenbankpaßworten in einem Verzeichnis abzulegen, das nicht durch eine URL erreichbar ist oder das mit einer .htaccess-Datei gegen Zugriff gesichert ist. Dieser Code kann dann mit Hilfe von include() oder require() eingebunden werden. Siehe dazu auch den Abschnitt Wie kann ich mein Datenbankpaßwort gegen Spionage sichern? dieser FAQ.

3.27 Gibt es für PHP einen "Dokumentationsgenerator" ähnlich Javadoc für Java?

Antwort von Guido Haeger

Im Moment gibt es noch kein Standard-Tool für PHP, mit dem sich aus dem Quellcode Dokumentationen generieren lassen. Es sind jedoch momentan mehrere diesbezügliche Projekte im Alpha- oder Beta-Stadium. U.a.:

3.28 Wie kann ich die Ausgabe meines Scriptes in einen anderen Frame umlenken?

Antwort von Daniel T. Gorski

Wenn das Script bereits läuft, gar nicht.

Es gibt einen inoffiziellen Lösungsansatz seitens Netscape, mit dem es möglich ist, die Ausgabe eines laufenden Scriptes in einen anderen Frame umzulenken. Dies ist jedoch nicht portabel: PHP läuft auf dem Server und weiß zunächst einmal nichts von den Frames eines Clients.

Es jedoch möglich, sich mit JavaScript eine Brückenseite zu schreiben (vom laufendem Script schreiben zu lassen), die mit Hilfe von onLoad() erneut eine PHP-Seite vom Server für einen anderen Frame anfordert (top.anderesFrame.location.href="seite1.php"; self.location.href="seite2.php").

Von diesem Verfahren wird hier deutlich abgeraten, da viele User (bei manchen Firmen gehört das auch zu der Firmenpolicy) JavaScript aus Sicherheitsgründen abschalten. Dann bleibt der Bildschirm u.U. leer und der Besucher verwirrt. Wiederkommen wird er bestimmt nicht mehr.

Vor der Ausführung des Scriptes, ist es selbstverständlich möglich mittels <A HREF="seite1.php" TARGET="andererFrame"> das Script in einem anderem Frame (bzw. mit javascript:window.open() geöffneten Fenster) ausführen zu lassen.

3.29 Warum ist es schlecht, mit dem Referer zu arbeiten?

Antwort von Martin Jansen

Der Referer ist eine Variable, in der stehen soll, von welcher Seite der Benutzer kommt, der sich gerade auf der Seite befindet. Hat der User zum Beispiel bei einer Suchmaschine auf einen Link geklickt, so würde der Referer "http://suchmaschine.tld/query?suchwort=german-faq" lauten.

Dies muss allerdings nicht immer so sein:

Diese Punkte sind Argumente dafür, den Referer nicht zu sicherheitsrelevanten Zwecken auf einer Website einzusetzen.


4. Typen und Funktionen

4.1 Welche Variablenarten gibt es in PHP und wie greife ich auf sie zu?

Antwort von Kristian Köhntopp

Konstanten


define(NAME, Wert);

definiert eine Konstante. Diese Konstante ist überall gültig, also nicht nur im globalen Namensraum, sondern auch in Klassen und in Funktionen.

Globale Variablen

Eine globale Variable definiert man implizit durch ihre Benutzung innerhalb des globalen Namensraumes, also einfach durch


$a = 10;

außerhalb jeder Klasse oder Funktion.

Variablen in Funktionen

In PHP hat jede Funktion ihren separaten Namensraum. In diesem Namensraum existieren nur diejenigen Variablen, die als Formalparameter der Funktion in die Funktion importiert worden sind oder die innerhalb der Funktion definiert worden sind. Diese Variablen haben die Lebensdauer des Funktionsaufrufes, d.h. sie verlieren beim Verlassen der Funktion ihren Wert.

Es ist möglich, in den Namensraum einer Funktion weitere Variablen hineinzuimportieren. Dies geschieht mit Hilfe der Anweisung global.

Beispiel:


function testfunc($para1) {
  global $glob1;

  $loc1 = 10;
  $glob1 = $glob1 + 1;

  return;
}

Diese Funktion hat zunächst einmal die Variablen $para1 und $loc1 als Parameter bzw. als lokale Variable definiert. Diese Variablen verlieren mit der schließenden Klammer "}" ihren Wert. Außerdem wird die Variable $glob1 in den Namensraum der Funktion importiert. Der Wert von $glob1 ist auch außerhalb von testfunc() sichtbar und $glob1 bleibt für die gesamte Lebensdauer des Programmes bestehen.

Weiterhin kann man innerhalb einer Funktion noch lokale Variablen definieren, die nur innerhalb der Funktion sichtbar sind, deren Wert jedoch nach dem Ende der Funktion erhalten bleibt und bei einem erneuten Funktionsaufruf erneut sichtbar wird. Dies erfolgt mit Hilfe der Anweisung static.


function testfunc2($para1) {
  global $glob1;
  static $stat1 = 0;

  $loc1 = 10;
  $glob1 = $glob1 + 1;
  $stat1 = $stat1 + 1;

  printf("Die Funktion testfunc2() wurde %s mal aufgerufen.\n",
    $stat1);
}

In diesem Beispiel ist die Anweisung static $stat1 dazu gekommen, die die Variable $stat1 als lokale, aber langlebige Variable in testfunc2() definiert und zur Vermeidung einer Warnung mit 0 initialisiert. Der Wert von $stat1 bleibt über einen einzelnen Funktionsaufruf erhalten. $stat1 zählt also im Beispiel oben die Anzahl der Aufrufe von testfunc2().

PHP definiert die Pseudovariable $GLOBALS[] vom Typ Array/Hash. Es handelt sich um einen Namen fuer die globale Symboltabelle des Interpreters. Der Name ist in allen Namensräumen sichtbar. Entsprechend ist das Konstrukt


function testfunc3() {
  global $glob1;

  $glob1 = $glob1 + 1;

  return;
}

gleichbedeutend mit


function testfunc3b() {
  $GLOBALS["glob1"] = $GLOBALS["glob1"] + 1;

  return;
}

Variablen in Klassen

Die dritte Sorte Namensräume, die in PHP existiert, sind Klassen und Objekte. Eine Klasse wird vereinbart mit der Anweisung


class MyClass {
  var $a;
  var $b;

  function c() {
    print("Der Wert von a ist %s\n", $this->a)
    $this->b = $this->b + 1;
    print("Der Wert von b ist %s\n", $this->b);
  }

  function MyClass() {
    $this->a = 10;
    $this->b = 0;
  }
}

Nach dieser Definition ist KEINE Variable belegt, aber es existiert ein Bauplan fuer MyClass-Variablen. MyClass ist ein Typ, so wie integer, float oder string Typen in PHP sind. Mit Hilfe der Anweisung "new" kann man sich Variablen nach diesem Bauplan erstellen lassen. "new" ist also eine Fabrik, der man einen Bauplan mitgibt und die nach diesem Plan Variablen herstellt.


$o = new MyClass;

Den Bauplan einer Variablen bezeichnet man als Klasse, hier der Klasse MyClass. Die nach diesem Plan gebaute Variable als ein Objekt der Klasse, hier also als das Objekt $o der Klasse MyClass. Manche Leute sagen zu dem Objekt $o der Klasse MyClass auch "$o ist eine Instanz der Klasse MyClass".

Nach einem Bauplan können mehrere Objekte gebaut werden, d.h. eine Klasse kann mehrere Instanzen haben.


$p = new MyClass;

Die Variablen mit den Namen "a" und "b" in einem Objekt der Klasse MyClass werden in PHP als Slots oder Instanzvariablen bezeichnet. Man spricht sie immer über den Namen der Klasse an:


$o->a = 17;
$p->a = 31;

Dadurch werden sie unterscheidbar, d.h. es ist erkennbar, ob man den Slot mit dem Namen "a" des Objektes $o oder des Objektes $p meint. Das kann man sich vorstellen wie Pfadnamen: In unterschiedlichen Verzeichnissen eines Dateisystems kann man ja auch Dateien mit demselben Namen liegen haben, aber diese Variablen unterscheiden sich im Pfadnamen.

Eine Klasse kann nicht nur Instanzvariablen definieren, sondern auch Instanzfunktionen, hier die Funktionen mit den Namen "c" und "MyClass". Sie werden genauso angesprochen wie die Instanzvariablen:


$o->c();
$p->c();
$o->MyClass();
$p->MyClass();

Manche Leute nennen die Funktionen eines Objektes auch "Methoden" und reden dann von "Instanzmethoden" statt "Instanzfunktionen". Das hat keinen praktischen Zweck und dient nur der Förderung der allgemeinen Verwirrung.

Beim Funktionsaufrufen gilt die folgende Sonderregel: Eine Funktion, die exakt denselben Namen hat wie die Klasse, in der sie enthalten ist, wird auch dann aufgerufen, wenn das Objekt von new gebaut wird. Diese Funktion kann also verwendet werden, um die Slots des Objektes mit Defaultwerten zu initialisieren:


$c = new MyClass;

Hier wird also die Funktion $c->MyClass() automatisch aufgerufen. Weil diese Funktion aufgerufen wird, während das Objekt nach dem Bauplan gebaut wird, nennt man eine solche Funktion einen Konstruktor. Ein Konstruktor könnte auch optionale Parameter erwarten und man könnte der Funktion dann beim Zusammenbau diese Parameter mitgeben:


class MyClass2 {

  ...

  function MyClass2($p1 = "") {
    if ($p1 != "") {
      $this->...
    }
  }
}

$mc1 = new MyClass2("beispielwert");
$mc2 = new MyClass2();

Wenn der Bauplan einer Variablen erstellt wird, wenn wir also die Klasse MyClass definieren, dann wissen wir noch nicht, unter welchen Namen die Slots und Instanzfunktionen des Objektes einmal angesprochen werden. Daher können wir in der Funktion nicht $o->a oder $p->c() schreiben, sondern müssen irgendetwas allgemeineres als Platzhalter notieren.

Die Pseudovariable $this steht dabei in einer Instanzfunktion als Platzhalter für den tatsächlichen Namen dieses Objektes. Innerhalb des Objektes $o steht $this->a also als Platzhalter fuer $o->a, innerhalb des Objektes $p steht $this->a jedoch als Platzhalter fuer $p->a. Man kann $this auch als "meine" lesen, also ist $this->a "mein Slot a" und $this->c() "meine Instanzfunktion c".

Referenzen in PHP3 und PHP4

Schon in PHP3 ist es so, dass jede Zuweisung eines Kopie der angesprochenen Variablen erzeugt:


$a = 10;
$b = 20;
$c = 30;

$arr = array( $a, $b, $c );

$arr[1] = 22;

Dies verändert den Wert von $arr[1] auf 22, aber der Wert von $b ändert sich nicht. Dies gilt auch bei Funktionsaufrufen:


function m($p1) {
  $p1 = 22;
}

m($b);

Dies setzt den Wert von $p1 auf 22, aber $p1 ist eine Kopie von $b und $b verändert den Wert nicht. Da beim Aufrufen ("Call") einer Funktion der Wert von $b in $p1 kopiert, also der Wert von $b an $p1 übergeben wird, nennt man dies "Call by value".

Man könnte auch eine Funktion schreiben, die den Namen der Variablen übergeben bekommt und dann mit Hilfe dieses Namens auf die globale Variable dieses Namens zugreift:


function m2($p1) {
  $GLOBALS[$p1] = 22;
}

m2("b");

Da in diesem Fall der Name der Variablen übergeben wird und mit Hilfe dieses Namens dann die originale Variable dieses Namens verändert wird, nennt man diese Art des Funktionsaufrufes "Call by name" (obwohl bei richtigem Call by Name noch viel obskurere Dinge möglich sind)..

Schon in PHP3 ist es so, dass man eine Funktion als "Call by Reference" definieren kann. In diesem Fall wird keine Kopie des Variablenwertes übergeben, sondern der Formalparameter p1 wird zu einem alternativen Namen für den übergebenen Wert. Wieder wird der Wert der originalen globalen Variablen verändert:


function m3(&$p1) {
  $p1 = 22;
}

m3($b);

Hier ist $p1 in m3() ein alternativer Name fuer die Variable, die beim Aufruf genennt wird. In m3($b) wird die Variable $b benannt, also ist $p1 in diesem Aufruf ein alternativer Name fuer $b. Immer wenn innerhalb dieses Aufrufes von m3() $p1 verwendet wird, wird $b angesprochen.

In PHP4 ist es nun neuerdings so, dass man Referenzen auch an anderer Stelle verwenden kann, zum Beispiel in der o.a. Arraydefinition:


$a = 10;
$b = 20;
$c = 30;

$arr = array( &$a, &$b, &$c );

$arr[1] = 22;

print $b;

Dieses Beispiel druckt den Wert 22. In diesem Beispiel sind $arr[0], $arr[1] und $arr[2] keine Kopien der Werte von $a, $b und $c, sondern die $arr-Variablen werden zu alternativen Namen fuer $a, $b und $c. Jeder Zugriff auf $arr[1] spricht also in Wirklichkeit $b an.

Man liest "$arr[1] = &$b" als "$arr von Eins ist eine Referenz auf $b".

Klassenfunktionen in PHP4

Wenn man eine Klasse definiert, kann man in PHP3 mit dieser Klasse nichts anderes machen als mit Hilfe von "new" Objekte dieser Klasse zu erzeugen.

In PHP4 kann man jedoch nicht nur Funktionen eines Objektes aufrufen, sondern auch schon Funktionen einer Klasse. Das sind also nichts anderes als gewöhnliche Funktionsaufrufe, nur dass die Funktionsnamen sehr seltsam aussehen:


class MyClass3 {
  function c() {
    print "Ich bin Funktion c() in der Klasse MyClass\n";
  }
}

MyClass3::c();

Dieses Stück Code haette man so auch schreiben koennen und es wäre leichter zu verstehen gewesen:


function c() {
  print "Ich bin die stinknormale Funktion c()\n";
}

c();

Man kann dies verwenden, um in abgeleiteten Klassen Funktionen gleichen Namens in Oberklassen aufzurufen.

4.2 Welche Datentypen gibt es in PHP?

Antwort von Kristian Köhntopp

Dadurch, daß PHP allen Variablenbenutzungen das Markierungszeichen $ (Dollar) voranstellt, ist es möglich, Variablen in Stringkonstanten zu interpolieren, wie man es von der Unix-Shell her kennt. Will man das Markierungszeichen selbst ausgeben, kann man ihm es durch einen vorangestellten \ (Backslash) entwerten. Einen Backslash gibt man aus, indem man ihn entwertet: \\ (Backslash Backslash).

In PHP gibt es die folgenden Datentypen:

Skalare Werte: integer, float, string


# Zuweisung
$myint   = 1;
$myfloat = 3.14;
$mystring= "hallo";

# Verwendung
$yourint   = $myint * 2;
$yourfloat = $myfloat * 2.71828;
$yourstring= $mystring . " du da!";

# Ausgabe
print "$yourint\n";
print "$yourfloat\n";
print "$yourstring\n";

Felder (Arrays)

Der Gebrauch von Feldern ist im Kapitel Arrays und Arrayvariablen ausführlicher erläutert.

PHP unterscheidet nicht zwischen Feldern (Arrays) mit Integer-Index und Hashes (Assoziativen Arrays) mit beliebigen Indices.


# Arrays

# Zuweisung
$a1    = array( 10, 20, 30);
$a2[0] = 10;
$a2[2] = 30;
$a2[1] = 20;

# Verwendung
$a3[0] = $a1[0] + $a2[2]; // $a3[0] ist 40
$a3[]  = $a1[1] + $a1[0]; // $a3[1] ist 30, Index autom. vergeben

# Ausgabe
printf("A1: %s %s %s\n", $a1[0], $a1[1], $a1[2]);

# Aufzählung
for ($i=0; $i<=2; $i++) {
  printf("%s: %s\n", $i, $a1[$i]);
}


# Hashes

# Zuweisung
$a1    = array( "peter" => 10, "paul" => 20, "mary" => 30);
$a2["peter"] = 10;
$a2["mary"]  = 30;
$a2["paul"]  = 20;

# Verwendung
$a3["peter"] = $a1["peter"] + $a2["mary"]; // $a3["peter"] ist 40

# Ausgabe
printf("A1: %s %s %s\n", $a1["peter"], $a1["paul"], $a1["mary"]);

# Aufzählung
reset($a1);
while(list($k, $v) = each($a1)) {
  print("%s: %s\n", $k, $v);
}

Objekte

Der Gebrauch von Objekten ist im Kapitel Klassen und Objekte ausführlicher erläutert.

4.3 Wie schreibe ich eine Funktion mit einer variablen Anzahl von Argumenten?

Antwort von Kristian Köhntopp

In PHP kann man Funktionsparameter mit Default-Werten versehen. Läßt man die Argumente der Funktion von hinten nach vorne weg, werden stattdessen die Defaults eingesetzt. Defaultwerte müssen skalare Konstanten sein. Variable Ausdrücke (Variablen, Funktionsaufrufe) oder nichtskalare Werte (Arrays, Objekte) sind nicht gestattet.


function beispiel($p = "default") {
  printf("Der Parameter p hat den Wert %s\n", $p);
}

beispiel("hallo");
beispiel();

Auf diese Weise kann man jedoch keine echten variadischen Funktionen schreiben. So ist es zum Beispiel nicht möglich eine Funktion wie printf() in PHP3 zu schreiben. Man kann variadische Funktionen jedoch durch die Übergabe eines Array- oder Hashparameters simulieren.


function beispiel2($p) {
  if (!isset($p) or !is_array($p))
    # Defaults setzen
    $p = array("para1" => "bla", "para2" => "fasel");

  if ($p["para1"])
    machdies();

  if ($p["para2"])
    machdas();
}

beispiel2(array("para1" => "laber", "para2" => "lall"));

Echte variadische Funktionen sind erst in PHP4 möglich. Dort gibt es die drei Funktionen

func_num_args()

Diese Funktion liefert die Anzahl der Funktionsargumente als Integer.

func_get_arg()

Diese Funktion bekommt eine Argumentnummer als Parameter und liefert des Wert des Funktionsargumentes mit diesem Index zurück.

func_get_args()

Diese Funktion liefert alle Argumente einer Funktion als Array zurück.

Eine echte variadische Funktion kann also in PHP4 folgendermaßen geschrieben werden:


function beispiel3() {
    $args = func_get_args();
    for($i=0; $i<count($args); $i++) {
         print($args[$i]."\n");
    }
}

beispiel3("das", "ist", "ein", "test");

4.4 Wie gebe ich mehrere Werte mit einer Funktion zurück?

Antwort von Kristian Köhntopp

PHP kann nur einen Wert mit return() zurückgeben.

Wie bei der Übergabe von Funktionsargumenten, kann man aber auch hier beliebige Werte in einem Array zusammenfassen und so eine Rückgabe mehrerer Werte simulieren:


function beispiel() {
    $ret = array(1, 2, 3);

    return($ret);
}

Im Funktionsaufruf kann man list() verwenden, um Variablen die Elemente des zurückgelieferten Arrays zuzuweisen:


list($var1, $var2, $var3) = beispiel();

Wenn man nicht tatsächlich mehrere Werte zurückgeben möchte, sondern lediglich mehrere Werte in einem Funktionsaufruf beeinflussen möchte, dann kann stattdessen auch mit Referenzparametern arbeiten.

4.5 Wie schreibe ich ein Script, das beliebige Parameter verarbeitet?

Antwort von Kristian Köhntopp

Manchmal möchte man ein Script zu schreiben, das beliebige Parameter verarbeitet - etwa ein Script, das alle gegebenen Parameter in eine Mail verpackt und diese dann versendet.

Wenn der Konfigurationsparameter track_vars gesetzt ist, kann man dies relativ leicht erreichen, indem man die Arrays $HTTP_GET_VARS oder $HTTP_POST_VARS durchläuft.

Vollständiges Beispiel:


<?php
  $msg = "";
  
  if (isset($HTTP_GET_VARS) and is_array($HTTP_GET_VARS)) :
    $msg .= "Alle GET-Parameter\n";
    $msg .= "==================\n";
    $msg .= "\n";
    reset($HTTP_GET_VARS);
    while(list($k, $v) = each($HTTP_GET_VARS)):
      $msg .= sprintf("%s\n  %s\n\n", $k, $v);
    endwhile;
  endif;

  if (isset($HTTP_POST_VARS) and is_array($HTTP_POST_VARS)) :
    $msg .= "Alle POST-Parameter\n";
    $msg .= "==================\n";
    $msg .= "\n";
    reset($HTTP_POST_VARS);
    while(list($k, $v) = each($HTTP_POST_VARS)):
      $msg .= sprintf("%s\n  %s\n\n", $k, $v);
    endwhile;
  endif;

  # Hier kann $msg per Mail versendet werden.
?>

4.6 Variable Variablen

Antwort von Kristian Köhntopp

Manchmal möchte man auf Variablen zugreifen, deren Namen variabel sind. Zum Beispiel könnte man die Variablen mit den Namen $myvar1, $myvar2, $myvar3, ..., $myvar9 haben.

Am günstigsten wäre es, in so einem Fall ein Array zu nehmen.


for ($i=0; $i<10; $i++)
        echo $myvar[$i];

Wenn es unbedingt skalare Variablen sein müssen, kann man stattdessen über das $GLOBALS[]-Array zugreifen:


for ($i=0; $i<10; $i++) {
  if (isset($GLOBALS["myvar$i"])
    printf("Variable var%d existiert und ihr Wert ist %s<br>\n",
      $i, $GLOBALS["myvar$i"]);
}

Statt echo $GLOBALS[$lall]; kann man auch die zwei Befehle global $$lall; echo $$lall; verwenden. Empfohlen ist jedoch das Konstrukt mit $GLOBALS[], weil es leichter zu lesen und zu verstehen ist. Das gilt besonders bei Dateinamen, die sich aus einem konstanten Stamm und einem variablen Anteil zusammensetzen.

Eine weitere alternative Schreibweise für variable Variablen ist ${$lall}; für zusammengesetzte Variablennamen entsprechend beispielsweise ${"datei_$lall"}.

4.7 Was ist der Unterschied zwischen isset() und einem Vergleich auf den Leerstring?

Antwort von

if($var) evaluiert nur dann zu true, wenn $var keinen der folgenden Werte darstellt: false, 0, 0.0, "" oder "0", NULL, array(). Alle diese Werte bedeuten false in ihrem jeweiligen Typ (Bool, Integer, Float, String, Null, Array).

if(isset($weiter)) evaluiert immer zu true, wenn $weiter nicht undefined ist.

Im ersten Fall wird der Inhalt, im zweiten Fall die Existenz der Variablen bewertet.

Ein ähnliches Problem tritt bei Vergleichen auf: if($var == false) evaluiert immer dann zu true, wenn $var einen der obigen Werte darstellt. PHP führt hier eine automatische Typenkonvertierung durch, wodurch die beiden Variablen als äquivalent angesehen werden.

4.8 Wie kann ich JavaScript-Funktionen aus PHP heraus aufrufen?

Antwort von Johannes Frömter

JavaScript läuft auf dem Client (im Browser), PHP läuft auf dem Server, also genau am anderen Ende der Welt; wenn die HTML-Seite beim Browser ankommt, ist PHP mit der Arbeit schon fertig. Der Aufruf einer JavaScript-Funktion aus PHP ist also prinzipiell unmöglich.

Allerdings kann man Werte von PHP an JavaScript übergeben; um eine in PHP vorhandene Variable in JavaScript verwenden zu können, muß man sie mittels echo innerhalb eines <script>-Bereiches ausgeben:


<script language="JavaScript">
<?php echo "js_text = '$php_text';\n"; ?>
</script>

Auf diese Weise wird die JavaScript-Variable js_text mit dem Wert der PHP-Variable $php_text vorbelegt.

Umgekehrt kann man aus JavaScript auch keine PHP-Funktionen direkt aufrufen. PHP wird immer als das Resultat eines HTTP-Requests ausgeführt, also beim Holen einer Seite mit GET oder beim Verarbeiten eines Formulares mit POST. Es ist also nicht möglich, aus JavaScript heraus eine PHP-Funktion aufzurufen, außer durch Erzeugen eines HTTP-Requests (durch den von PHP eine neue Seite generiert wird).

Einen GET-Request mit JavaScript erreicht man prinzipiell durch


<script language="JavaScript">
window.location.href = "script.php?parameter=" + parameter;
</script>

Einen POST- oder GET-Request mit einem Formular erreicht man durch


<script language="JavaScript">
document.formularname.submit();
</script>

Weitere Informationen zur Variablenübergabe: siehe Variablen und Formulare in dieser FAQ.


5. Stringfunktionen

5.1 Was ist besser, print() oder echo?

Antwort von Kristian Köhntopp

echo() ist ein internes Sprachkonstrukt, print() ist eine Expression. Man kann print() also in Situationen benutzen, wo Expressions gefragt sind, z.B. $res = print("...")?1:0.

echo hat eine variable Argumentliste, dabei muß man aber auf die Klammern verzichten: echo $var1, $var2;. print() kann nur ein Argument haben.

In PHP 3 ist echo() schneller, in PHP 4 ist die Geschwindigkeit gleich. Die Geschwindigkeitsdifferenz in PHP3 ist unter 3 Prozent.

5.2 Wie zerlege ich einen String?

Antwort von Kristian Köhntopp

Man kann einen String wie ein Array ansprechen:


 $str = "teststring";
 $len = strlen($str);
 for($i=0; $i<$len; $i++)
  printf("Zeichen %d ist %s<br>\n", $i, $str[$i]);

Mit Hilfe der Funktion substr() kann man Teilstrings aus einem String herausschneiden.

Mit Hilfe der Funktion explode() kann man einen String an einem Trennzeichen in ein Array zerlegen.


  $str = "dies ist ein teststring.";
  $avar = explode(" ", $str);
  $len = count($avar);
  for ($i=0; $i<$len; $i++)
    printf("%d: %s<br>\n", $i, $avar[$i]);

Dieses Beispiel zerlegt den gegebenen Teststring an den Leerzeichen und erzeugt ein Array $avar mit den Indices 0 bis 3 (4 Elementen).

Kompliziertere Zerlegungen lassen sich mit Hilfe der Funktion preg_split() vornehmen. Ältere Versionen von PHP3 haben diese Funktion nicht, dort muß man das weniger leistungsfähigere und langsamere split() verwenden.


  $str = "ich bin  ein    sehr komplizierter test, nicht wahr?";
  $avar = preg_split("/[ \t.!?]+/", $str);
  $len = count($avar);
  for ($i=0; $i<$len; $i++)
    printf("%d: %s<br>\n", $i, $avar[$i]);

Im Gegensatz zum vorhergehenden Beispiel werden hier mehrfache Leerzeichen nicht als mehrfache Trennungen gezählt und auch Satzzeichen werden zu den Trennzeichen gezählt.

5.3 Wie zerlege ich eine URL?

Antwort von Kristian Köhntopp

Mit Hilfe der Funktion parse_url() kann eine URL in ihre Bestandteile zerlegt werden.


  $str = "http://user:password@www.koehntopp.de:80/kris/artikel#php";
  $avar = parse_url($str);
  reset($avar);
  while(list($k, $v) = each($avar))
    printf("k=%s, v=%s<br>\n", $k, $v);

Ein QUERY_STRING kann mit Hilfe der Funktion parse_str() in seine Variablen zerlegt werden.

5.4 Wie gebe ich eine Zahl formatiert aus?

Antwort von Kristian Köhntopp

Mit Hilfe der Funktion number_format() oder mit Hilfe von printf().

5.5 Wie kann ich Zeilenumbrüche in <br> umwandeln?

Antwort von Kristian Köhntopp

PHP bietet die Funktion nl2br(). Damit wird vor jeden Zeilenumbruch ein <br> (ab PHP 4.0.5 ein XHTML-konformes <br />) eingefügt.


  # Einlesen der Datei "datei" in den String $str
  $str = implode("", @file("datei"));
  # Ausgeben der Datei mit Umbrüchen
  print nl2br($str);

Sollen Zeilenumbrüche komplett ersetzt werden, benutzt man str_replace():


$string = str_replace("\n", "<br>", $string);

5.6 Wie breche ich einen String nach x Zeichen um?

Antwort von Johannes Frömter

Ab PHP 4.0.2 gibt es die Funktion wordwrap(), um lange Strings auf eine definierte Zeilenlänge zu bringen. Als Default wird nach 75 Zeichen mit \n umgebrochen, man kann aber optional als dritten bzw. vierten Parameter auch eigene Werte angeben, was gerade bei der Ausgabe in einer HTML-Seite praktisch ist:


echo wordwrap($ganzLangerText, 25, "<br>", 1);

Der vierte Parameter cut ist in PHP 4.0.3 hinzugekommen, er bewirkt, daß der String auf jeden Fall (auch mitten in einem Wort) umgebrochen wird. Achtung: Setzen Sie cut auf jeden Fall, sonst landet wordwrap() bei Wörtern mit einer Länge > width in einer Endlosschleife!

Kann man wordwrap() nicht benutzen, helfen frei verfügbare Scripte wie z.B. textwrap von Brian Moon.


6. Reguläre Ausdrücke

6.1 Wie kann ich mehr über reguläre Ausdrücke lernen?

Antwort von Kristian Köhntopp

Bei O'Reilly gibt es das ausgezeichnete Buch Mastering Regular Expressions von Jeffrey E. F. Friedl. Es enthält eine ausgezeichnete Übersicht über die verschiedenen Formen von regulären Ausdrücken in Unix mit Beispielen.

Online gibt es z.B. das Tutorial Rx (englisch).

6.2 Soll ich ereg() oder preg() verwenden?

Antwort von Kristian Köhntopp

Wenn die verwendete Version von PHP3 ausreichend neu ist und das Modul PCRE aktiviert ist (dies kann man mit einem Aufruf von phpinfo() leicht feststellen), dann sollte man wo immer es geht die preg-Funktionen verwenden. Sie sind nicht nur schneller, sondern auch flexibler und leistungsfähiger als die alten ereg-Funktionen.

Es gibt keinen Grund mehr, die ereg-Funktionen noch zu verwenden außer Rücksicht auf veraltete Installationen.

6.3 Wie verwende ich die preg()-Funktionen?

Antwort von Kristian Köhntopp

Die preg-Funktionen sind im Abschnitt Perl-compatible regular expressions des Online-Handbuches beschrieben. Es handelt sich um die Funktionen

Für alle Funktionen gilt das in den Abschnitten Pattern Options und Pattern Syntax gesagte.

6.4 Was sind reguläre Ausdrücke?

Antwort von Kristian Köhntopp

Reguläre Ausdrücke sind Suchmuster, die sich auf Strings anwenden lassen und für die entscheidbar ist, ob sie auf den String passen (match) oder nicht passen. So paßt das Suchmuster ei auf den String Weichei, weil darin die Zeichenfolge ei enthalten ist, aber nicht auf den String Warmduscher. Wendet man ein Suchmuster auf eine Menge von Strings an, dann bekommt man zwei Teilmengen, nämlich die Menge aller Strings, auf die das Muster paßt und die Menge aller Strings, auf die das Muster nicht paßt. Meistens interessiert man sich für eine der beiden Teilmengen ("Finde alle Namen, die mit einem einem A beginnen.", "Finde alle Zeichen, die nicht rechts von einem Kommentarzeichen stehen.")

Reguläre Ausdrücke werden meistens durch einen endlichen Automaten realisiert. Die Informatik kennt Verfahren, mit denen man automatisch einen Automaten generieren kann, der für ein bestimmtes Suchmuster entscheidet, ob es auf einen String paßt oder nicht. Auch die regulären Ausdrücke in PHP funktionieren so: Bei der ersten Benutzung eines regulären Ausdruckes wird ein solcher Automat intern generiert (das Muster wird "compiliert") und dann angewendet. Bei späteren Benutzungen desselben Suchmusters kann dieser Automat dann unter Umständen wieder verwendet werden, was deutlich schneller ist.

Einige Suchmuster und Bedingungen sind zu komplex, als daß man sie mit Hilfe von regulären Ausdrücken und Automaten formulieren können. Typische Beispiele dafür sind Dinge, die Abzählungen erforderlich machen ("Finde alle Worte, die aus genausovielen b's bestehen, wie sie a's enthalten") und Dinge, die Vorbedingungen notwendig machen ("Finde alle Worte print, aber nur, wenn sie nicht in Anführungszeichen stehen oder Bestandteil eines Kommentares sind."). In diesem Fällen braucht man leistungsfähigere Konzepte und Werkzeuge, kontextfreie oder kontextsensitive Grammatiken und dazu passende Parser.

In der Praxis verwendet man reguläre Ausdrücke, um zu entscheiden, ob ein String bestimmten formalen Kritierien genügt ("Akzeptiere den Formularwert nur dann, wenn er ausschließlichlich Ziffern enthält.") oder um bestimmte Teilstücke aus Strings herauszuschneiden ("Liefere mit den Text zwischen dem begin und end aus dem gegebenen String.").

6.5 Welche Bauelemente kommen in regulären Ausdrücken vor?

Antwort von Kristian Köhntopp

Suchmuster in regulären Ausdrücken bestehen aus gewöhnlichen Zeichen und Zeichen mit einer Sonderbedeutung. Gewöhnliche Zeichen in Suchausdrücken stehen für die entsprechenden Zeichen in einem String. Der Suchausdruck hallo paßt also auf alle Strings, die irgendwo genau diese Zeichenfolge enthalten. Zeichen mit Sonderbedeutung stehen als Platzhalter für ein oder mehrere andere Zeichen, für Zeilenanfänge oder -enden oder für andere Sonderfunktionen. Sie sind es, die reguläre Ausdrücke eigentlich mächtig und sinnvoll machen.

In regulären Ausdrücken gibt es Zeichenmengen (wird weiter unten erklärt), die durch eckige Klammern [ und ] eingeschlossen werden. Bei Zeichen mit Sonderbedeutung gelten leicht unterschiedliche Regeln, je nachdem ob man gerade eine außerhalb einer Zeichenmenge arbeitet oder innerhalb.

Außerhalb von Zeichenmengen gibt es die folgenden besonderen Regeln:

Eckige Klammern leiten eine Zeichenmenge ein. Eine Zeichenmenge steht immer für genau ein Zeichen und zwar für ein Zeichen, das in der Menge enthalten ist. Also paßt der reguläre Ausdruck [0123456789] auf genau eine beliebige Ziffer. Innerhalb der eckigen Klammern einer Zeichenmenge haben die folgenden Zeichen eine besondere Bedeutung:

Verwendet man einen Suchausdruck mit den preg*-Funktionen, dann ist der Suchausdruck ist sogenannte Begrenzerzeichen (Delimiter) einzuschließen, hinter denen noch Optionen mit angegeben werden können. Meistens verwendet man entweder die Schrägstriche / oder Gleichheitszeichen =.

Aus diesen Komponenten kann man sich mit einiger Übung Suchausdrücke zusammenbauen, die nicht nur weitgehend unlesbar sind, sondern die außerdem schnell und schmerzlos die gewünschten Suchfunktionen oder Such- und Ersetzefunktionen durchführen.

6.6 Wie teste ich auf die Existenz mehrerer Suchworte in einem String/Array?

Antwort von Kristian Köhntopp

Der einfachste Anwendungsfall von preg_match() ist zu testen, ob ein Suchmuster auf einen gegebenen String paßt.


if (!preg_match("/^\d+$/", $eingabe))
  return "Die Eingabe darf ausschließlich aus Ziffern bestehen.";

# $eingabe verarbeiten

6.7 Wie isoliere ich Suchstrings aus einem größeren Text?

Antwort von Kristian Köhntopp

Das folgende vollständige Beispiel zeigt, wie man den Inhalt des Body-Tags aus einer HTML-Datei isolieren kann.


  $str = "lalalala
<body bgcolor=#cccccc>lang und weilig
noch eine zeile
<h1>Bla</h1>
</body>
tralalal";
  preg_match_all("=<body[^>]*>(.*)</body>=siU", $str, $a);
  print $a[1][0];

Das Beispiel macht von den Optionen i, s und U der Perl Regular Expressions Gebrauch: Die Option i sorgt dafür, daß Groß- und Kleinschreibung keine Rolle spielen, die Option U sorgt dafür, daß Ungreedy gematched wird, d.h. der kürzest mögliche Match verwendet wird. Die Option s bewirkt, daß der Punktoperator auch Newlines mit matched. Dadurch ist es möglich, den regulären Ausdruck auf auf einen mehrzeiligen String anzuwenden.

6.8 Wie finde ich alle Links in einer HTML-Datei?

Antwort von Björn Schotte

$zeile sei der Inhalt einer zuvor eingelesenen HTML-Datei. Diese Variable muß innerhalb der While-Schleife neu zusammengebaut werden, sonst läuft man hier in eine Endlosschleife.


$pattern = '=^(.*)<a(.*)href\="?(\S+)"([^>]*)>(.*)</a>(.*)$=msi';
while (preg_match($pattern, $zeile, $txt))
{
  /* $txt[3] enthält die gewünschte URL. */
  echo $txt[3]."\n";

  /* $zeile neu bauen */
  $zeile = $txt[1]." hier war mal ein Link ".$txt[6];
}

/* $zeile zur Kontrolle ausgeben */
print "<br>".nl2br($zeile);

$txt enthält als Array alle Tokens, die in der Regexp in Klammern angegeben sind. $txt[0]als Sonderstellung enthält den ganzen Text.

6.9 Wie ersetze ich alle relativen Links in einer HTML-Datei?

Antwort von Björn Schotte

$zeile sei der Inhalt einer zuvor eingelesenen HTML-Datei. Im folgenden Beispiel werden alle relativen Links durch das Konstrukt <?php $sess->purl("relativerlink"); ?> ersetzt. relativerlink sei hierbei der relative Link, der gefunden wurde.

Bei längeren Texten ist darauf zu achten, dass dieses Konstrukt teilweise recht lange dauert, bis es den kompletten Text durchsucht hat.


print "Konvertiere";
flush();

$pattern  = '=^(.*)<a\\n*(.*)href\="?([^h][^t][^t][^p][^:]\S+)"';
$pattern .= '([^>]*)\\n*>(.*)</a\\n*>(.*)$=msi';
$repl = '\\1<a\\2 href="<?php $sess->purl("\\3"); ?>"\\4>\\5</a>\\6';

while (!$fertig)
{

$zeilenew = preg_replace($pattern, $repl, $zeile);

  if ($zeilenew == $zeile)
  {
     $fertig = true;
     $zeile = $zeilenew;
     flush();
     break;
  } else {
     $zeile = $zeilenew;
     print ".<br>\n";
     flush();
  }
}

print "Ersetzt:<br><br>".nl2br(htmlspecialchars($zeile));

6.10 Wie überprüfe ich einen String auf seinen Inhalt?

Antwort von Martin Jansen

Häufig ist es nötig, festzustellen, ob ein String nur Ziffern bzw. nur Buchstaben enthält.

$string sei die Zeichenkette, die überprüft werden soll. Die Regular Expression im ersten Beispiel überprüft, ob nur Ziffern in $string enthalten sind. Ist dies der Fall, gibt sie "Zeichenkette OK" aus, ansonsten lautet die Ausgabe "Ungültiges Zeichen in der Zeichenkette".


/* Regex zur Ueberpruefung des Strings */
if (!preg_match("/^\d+$/",$string)) {
  echo "Ungültiges Zeichen in der Zeichenkette";
} else {
  echo "Zeichenkette OK";
}

Um zu überprüfen, ob in der Zeichenkette nur Buchstaben stehen, kann man folgende Regex verwenden, die auf dem gleichen Prinzip beruht:


if (!preg_match("=^[a-zäöüß]+$=i",$string))
  echo "Ungültiges Zeichen in der Zeichenkette";
} else {
  echo "Zeichenkette OK";
}


7. Arrays und Arrayvariablen

7.1 Wie kann ich ein Element an ein Array anfügen?

Antwort von Kristian Köhntopp

Durch Verwendung des leeren Arrayoperators wird an ein Array ein Element angehängt. In Code:


$avar[] = "neues element";

In PHP 4 kann man mit der Funktion array_push() auch mehrere Elemente gleichzeitig an ein Array anfügen.

Durch Verwendung eines Index kann man ein Element an einer bestimmten Stelle in einem Array ansprechen. Der Index kann numerisch oder ein String sein:


$avar[1]     = "Element mit dem Index 1";
$avar["bla"] = "Element mit dem Index 'bla'";

7.2 Wie kann ich ein Array aufzählen?

Antwort von Kristian Köhntopp

Ein Array enthält $anz = count($avar) viele Elemente. Man kann diese Elemente mit einer for-Schleife aufzählen, falls die Indices numerisch-zusammenhängend sind:


$anz = count($avar);
for ($i=0; $i<$anz; $i++):
  printf("i: %d avar[%d] = %s<br>\n", $i, $i, $avar[$i]);
endfor;

Für assoziative Arrays ist dieses Konstrukt besser geeignet:


if (isset($avar) && is_array($avar)):
  reset($avar);
  while(list($k, $v) = each($avar)):
    printf("k=%s v=%s<br>\n", $k, $v);
  endwhile;
endif;

Es macht Gebrauch von den Funktionen reset() um den internen Positionszeiger eines Arrays zurückzusetzen, list() um einen Zuweisungskontext für ein Wertepaar $k und $v zu erzeugen und each() um den Schlüssel (key, k) und den Wert (value, v) an der aktuellen Position des Arrays auszulesen.

Von der Anwendung der Funktionen next(), prev() und current() ist in diesem Zusammenhang abzuraten, da sie bei Arrays mit Nullwerten falsche Ergebnisse liefern:


$avar = array(-2, -1, 0, 1, 2);
for (reset($avar); $v = current($avar); next($avar)):
  printf("v = %d<br>\n", $v);
endfor;

Diese Schleife wird nur die Werte -2 und -1 drucken, da hier der Wert 0 nicht vom Feldende unterschieden werden kann.

7.3 Wie greife ich auf ein mehrdimensionales Array zu?

Antwort von Kristian Köhntopp

Das läßt sich am einfachsten an einem kleinen Beispiel zeigen:


$array = array(
   "foo" => array ("a", "b"),
   "bar" => array ("c", "d")
   );

print($array["foo"][0]); // gibt "a" aus.
print($array["bar"][1]); // gibt "d" aus.

7.4 Wie kann ich ein Array nach einem beliebigen Kriterium sortieren lassen?

Antwort von Kristian Köhntopp

PHP stellt eine Reihe von vordefinierten Sortierfunktionen zur Verfügung. Wenn diese nicht ausreichen, kann man mit Hilfe der Funktion usort() nach beliebigen Kriterien sortieren lassen. Der Funktion muß eine Vergleichsfunktion und das zu sortierende Array als Parameter mit übergeben werden.

Das nachfolgende Beispiel sortiert ein Array von Paaren alphabetisch nach dem 2. Element.


kris@valiant:~ > ./php
<?php
  $a = array(
         array(0, "Schmidt"),
         array(2, "Albert")
       );

  function cmp($a, $b) {
    printf("type a = %s type b = %s\n", gettype($a), gettype($b));
    if ($a[1] == $b[1]) return 0;
    return ($a[1] > $b[1])?1:-1;
  }

  usort ($a, "cmp");

  reset($a);
  while(list($k, $v) = each($a))
    printf("k = %s  v[0] = %s  v[1] = %s\n", $k, $v[0], $v[1]);
?>
X-Powered-By: PHP/4.0b5-dev
Content-type: text/html; charset=iso-8859-1

type a = array type b = array
k = 0  v[0] = 2  v[1] = Albert
k = 1  v[0] = 0  v[1] = Schmidt
kris@valiant:~ >

7.5 Wie kann ich Duplikate aus einem Array entfernen?

Antwort von Johannes Frömter

Ab PHP Version 4.0.1 gibt es die Funktion array_unique(), um doppelte Einträge in Arrays zu eliminieren.

Anmerkung: array_unique() behält die Indizes des Original- Arrays bei; um auf die Elemente des Arrays mit fortlaufenden numerischen Indizes von 0 bis count()-1 zugreifen zu können, verwende man


$a = array_values(array_unique($a));

In PHP3 kann man sich z.B. mit folgender Konstruktion behelfen:


function array_unique($a)
{
 while (list($k, $v) = each($a)) $b[$v] = "";
 while (list($k, $v) = each($b)) $c[] = $k;
 return $c;
}

7.6 Wie kann ich ein Array von einer Seite auf eine andere transportieren?

Antwort von Martin Jansen

Die einzige sichere Methode, um ein Array von einer Seite auf eine andere Seite zu transportieren, ist das Ablegen des Arrays in einer Session auf dem Server. Alle anderen Methoden stellen ein zu grosses Sicherheitsrisiko dar und sollten somit auf keinen Fall angewendet werden.

Weitere Informationen zu Sessions finden sich in PHP4: Sessions und in Was sind Sessions und warum sind sie nützlich?


8. Klassen und Objekte

8.1 Warum Klassen und Objekte benutzen?

Antwort von Kristian Köhntopp

Der Artikel Data Driven Websites mit PHP, Teil 2 zeigt, daß es recht schwierig ist, Code zu bauen, der sowohl komfortabel als auch wiederverwendbar ist.

Mit Hilfe von Klassen läßt sich solcher Code so kapseln, daß er vergleichsweise störungsfrei in existierende Projekte eingesetzt werden kann, ohne Gefahr zu laufen, mit bereits benutzten Funktions- und Variablennamen zu kollidieren.

8.2 Wie definiere ich eine Klasse? Wie erzeuge ich ein Objekt?

Antwort von Kristian Köhntopp

Angenommen, es ist eine Reihe von Funktionen vorhanden, die mit einer Datenbank kommunizieren und diese Funktionen sollen in eine Klasse umgewandelt werden:


 $Link_ID  = 0;  // ID der aktuellen DB-Verbindung
 $Query_ID = 0; // ID des aktuellen Abfrageresultates
 $Error    = 0;    // Letzte Datenbank-Fehlermeldung

 function connect() { ... }

 function query()   { ... }

 function next_record() { ... }

 function num_rows() { ... }

Aus diesen Variablen und Funktionen wird eine Klasse, indem man vor alle verwendeten Variablen das Schlüsselwort var schreibt und indem man alle Variablen und Funktionen mit einem class-Konstrukt umschließt.


class DB_MiniSQL {
 var $Link_ID  = 0; // ID der aktuellen DB-Verbindung
 var $Query_ID = 0; // ID des aktuellen Abfrageresultates
 var $Error    = 0; // Letzte Datenbank-Fehlermeldung

 function connect() { ... }

 function query()   { ... }

 function next_record() { ... }

 function num_rows() { ... }
}

Klassen selbst sind nur Baupläne, sie erzeugen keine Variablen und die Funktionen, die in ihnen enthalten sind, lassen sich so nicht verwenden. Mit Hilfe der Anweisung new läßt man den PHP-Interpreter eine Variable, ein Objekt, nach diesem Bauplan bauen.


 $db1 = new DB_MiniSQL; // $db1 ist ein Objekt der Klasse DB_MiniSQL
 $db2 = new DB_MiniSQL; // $db2 ist noch ein Objekt derselben Klasse

Das Objekt $db1 kann man sich wie ein Array mit einer besonderen Syntax vorstellen. Anstatt auf $db1["Link_ID"] und $db1["Error"] zuzugreifen, muß man $db1->Link_ID und $db1->Error verwenden. Auch die Funktionen in einem Objekt lassen sich so aufrufen: $db1->connect(), $db1->query() und so weiter.

Ein beliebter Fehler ist, $db1->Error zu meinen, aber $db1->$Error zu schreiben. Das ist falsch: Der vollständige Name der Variablen ist db1->Error, mit einem $ davor, um ihn als Variablennamen zu kennzeichnen.

8.3 Was ist $this?

Antwort von Kristian Köhntopp

Innerhalb einer Funktion wie connect() muß auf die Variable Link_ID zugegriffen werden, um das Resultat eines Connect abzuspeichern. In connect() können wir nicht wissen, wie die Funktion nun gerade heißt, also ob ihr Name nun gerade $db1->connect() oder $db2->connect() ist und ob die Link-ID nun in $db1->Link_ID oder in $db2->Link_ID abgespeichert werden muß.

Eigentlich ist das auch egal: Wir wollen ja nur auf unsere eigene Link-ID zugreifen. $this bezeichnet nun genau unser eigenes Objekt, also $db1 innerhalb von $db1 und $db2 innerhalb von $db2. Man schreibt daher code wie


class DB_MiniSQL {
  var $Link_ID = 0;

  function connect() {
    $this->Link_ID = mysql_connect(...);
    ...
  }

  ...
}

oder


class DB_MiniSQL {
  var $Link_ID = 0;

  function query($query) {
    // Wenn kein Datenbank-Link vorhanden ist, eines herstellen.
    if (!$this->Link_ID)
      $this->connect();

    ...
   }
  ...
}

8.4 Was ist extends? Was ist Vererbung?

Antwort von Kristian Köhntopp

Häufig braucht man eine Klasse, die sich genauso verhält wie eine Klasse, die man schon hat, aber mit ganz kleinen Änderungen. Mit Hilfe des Schlüsselworts extends kann man sich eine Klasse definieren, die genauso ist wie eine bereits existierende Klasse und braucht dann nur noch das zu notieren, was anders ist.

Die Änderungen können dabei eine bestehende Klasse erweitern, also neue Variablen und Funktionen zu einer Klasse hinzufügen oder bestehende Variablen und Funktionen einer Klasse ersetzen.

Der folgende Beispiel-Code definiert eine Klasse Example_SQL, die sich ganz genauso verhält wie die Klasse DB_Sql in PHPLIB. Die Variablen $Host, $User, $Password und $Database sind jedoch anders belegt als in der originalen Klasse: Wir setzen dort einfach die Informationen ein, die notwendig sind, um unsere Datenbank zu kontaktieren. Außerdem ist die Funktion haltmsg() ersetzt. Die Klasse ruft diese Funktion auf, wenn ein Fehler aufgetreten ist. Wir ersetzen diese Funktion durch eine eigene Version, sodaß wir Fehlermeldungen mit den Informationen drucken können, die der Anwender benötigt.


class Example_Sql extends DB_Sql {
  var $Host     = "database.netuse.de";
  var $User     = "kris";
  var $Password = "xyzzy";
  var $Database = "example_database";

  function haltmsg($msg) {
?>
    Es ist ein Datenbankfehler aufgetreten. Die Bearbeitung
    Ihrer Eingaben wurde abgebrochen. Bitte informieren Sie
    <a href="mailto:webmaster@example.kunde.de">den Webmaster</a>
    von diesem Problem.<p>
 
<?php
    printf("Die Fehlermeldung der Datenbank war: %s\n", $msg);
  }
}

Man kann die Klasse Example_SQL nicht verstehen, ohne die originale Klasse DB_SQL zu kennen. Die Klasse Example_SQL hat alle Eigenschaften und Funktionen, die DB_SQL auch hat. Erzeugt man also ein $db = new Example_SQL, dann kann man $db->query(...), $db->next_record() und so weiter aufrufen, als ob man es mit einer Klasse DB_SQL zu tun hätte.

Die Klasse zeigt nur in folgenden Punkten abweichendes Verhalten: Sie druckt ihre Fehlermeldungen in deutsch und enthält anwendungsspezifische Kontaktinformationen und sie kontaktiert anders als die Originalklasse defaultmäßig die Datenbank example_database auf dem Host database.netuse.de mit dem angegebenen Usernamen und Paßwort.

8.5 Was ist ein Konstruktor?

Antwort von Kristian Köhntopp

Ein Konstruktur ist eine gewöhnliche Funktion einer Klasse. Sie unterscheidet sich von anderen Funktionen derselben Klasse dadurch, daß sie beim Erzeugen der Klasse mit new automatisch aufgerufen wird. In PHP muß ein Konstruktor unglücklicherweise genauso heißen wie die Klasse selbst.

Ein Konstruktor kann optionale Parameter mitgegeben bekommen. Er kann niemals ein Funktionsergebnis liefern.

Man verwendet Konstruktoren oft, um die Variablen eines Objektes zu initialisieren. Die Klasse Menu in PHPLIB verwendet beispielweise einen Konstrutor, um eine Menüstruktur zu initialisieren.


class Menu {
  function Menu {
    $this->setup();
  }

  function setup() {
    reset($this->urlmap);
    while(...) {
      ...;
    }
  }
}

Die Entscheidung, wie in C++ Konstruktoren genauso zu benennen wie die Klasse ist überaus unglücklich, denn auf diese Weise muß man wissen, ob eine Klasse einen Konstruktor hat, wenn man eine Klasse erweitert.


class My_Menu extends Menu {
  var $urlmap = array(
    // meine eigenen Menüpunkte hier
  );
}

Diese abgeleitete Klasse wird nicht funktionieren, denn ihr fehlt der Konstruktor My_Menu(). Die Funktion setup() wird niemals aufgerufen und daher kann das Menü nicht funktionieren. Man muß stattdessen Code wie diesen schreiben:


class My_Menu extends Menu {
  var $urlmap = array(
    // meine eigenen Menüpunkte hier
  );

  function My_Menu() {
    $this->setup();
  }
}

8.6 Was sind polymorphe Funktionen? Kann ich sie simulieren?

Antwort von Kristian Köhntopp

Unter Polymorphie versteht man das Verhalten von objektorientierten Sprachen, die Signatur einer Funktion als Bestandteil des Funktionsnamens bei einem Aufruf zu betrachten. Die Signatur einer Funktion sind der Returntyp und die Parametertypen einer Funktion. In einer Sprache mit Polymorphie würde Code wie der folgende funktionieren:


# Funktion f mit Integer-Resultat und Integer-Parametern
function int f(int $a, int $b) {
  return $a*$b;
}

# Funktion f mit Array-Resultat und Array-Parametern
function array f(array $a, array $b) {
  $r = array(),
  $l = count($a);
  for ($i=0; $i<$l; $i++)
    $r[] = $a[$i] * $b[$i];
  return $r;
}

# Definition von Integer-Parametern
$xi = 3;
$yi = 4;

# Aufruf der Integer-Funktion f.
$zi = f($xi, $yi);

# Definition von Array-Parametern
$xa = array(2, 3, 4);
$ya = array(4, 3, 2);

# Aufruf der Array-Funktion f.
$za = f($xa, $ya);

Dieser Code definiert zwei verschiedene Funktionen, die intern als int_f_int_int() und array_f_array_array() bezeichnet werden können, die im Code aber beide ununterscheidbar f() heißen. Er ruft dann die Funktion f() einmal mit Integer-Parametern und Array-Paramerern auf. Die Sprache ist aufgrund der Polymorphie in der Lage, diese beiden f() zu unterscheiden und korrekt die Funktion int_f_int_int() oder array_f_array_array() aufzurufen.

PHP unterstützt keine Polymorphie und kann dies schon deswegen nicht tun, weil die Return- und Parametertypen einer Funktion nicht deklariert werden müssen. Stattdessen muß man manuell mit den Typfunktionen wie folgt codieren:


function f($a, $b) {
  # Wandle $a in ein Array um, wenn es das nicht ist.
  if (!is_array($a))
    $a = array($a);

  # Ebenso $b.
  if (!is_array($b))
    $b = array($b);

  # Normaler Code.
  $r = array(),
  $l = count($a);
  for ($i=0; $i<$l; $i++)
    $r[] = $a[$i] * $b[$i];

  if (count($r) == 1)
    # Skalar zurückgeben
    return $r[0];
  else
    # Array zurückgeben
    return $r;
}

8.7 Wie kann ich Metainformationen über eine Klasse bekommen?

Antwort von Kristian Köhntopp

Metainformationen über eine Klasse oder ein Objekt sind alle Informationen, die man über diese Klasse oder eine Instanz dieser Klasse (ein Objekt) bekommen kann. Sie umfassen den Namen der Klasse eines Objektes und die Namen aller Oberklassen dieser Klasse, die Namen und Typen aller Instanzvariablen des Objektes und die Namen, Returntypen sowie Parametertypen aller Funktionen eines Objektes.

In PHP kann man die Klasse eines unbekannten Objektes nicht bestimmen, d.h. man kann keine Funktion


function show_classname($o) {
  if (is_object($o))
    printf("Die Klasse des Objektes ist %s\n", wunderfunktion($o));
  }
}

schreiben, weil es die benötigte Wunderfunktion nicht gibt. Man kann jedoch die Namen aller Slots (Instanzvariablen und Funktionen) einer Klasse aufzählen und ihre Typen eingeschränkt bestimmen, weil Objekte in PHP3 nur Hashes mit Zuckerguß sind.


kris@valiant:~ > ~/bin/php -q
<?php
  class Beispiel {
    var $i = 10;
    var $s = "hallo";
    var $a = array(1, 2);

    function f() {
      print "Hallo";
    }
  }

  $b = new Beispiel;

  reset($b);
  while(list($k, $v) = each($b)):
    printf("%s ist ein %s\n", $k, gettype($v));
  endwhile;
i ist ein integer
s ist ein string
a ist ein array
f ist ein user function

Weil PHP3 die Classname-Funktion nicht hat, kann die eingebaute Funktion serialize() PHP3-Objekte auch nicht korrekt abspeichern und wiederherstellen: Das resultierende Objekt hat keinerlei Beziehung zu seiner Klasse mehr und keine der Methoden des Objektes kann noch aufgerufen werden.

PHPLIB hat dieses Problem nicht: Dort ist es zwingend notwendig, in jeder Klasse einen Slot mit dem Namen classname zu definieren, der den genauen Namen der Klasse enthält. In PHPLIB kann man also mit print $o->classname den Name der Klasse eines Objektes abfragen und entsprechend kann PHPLIB Objekte korrekt serialisieren.

In Zend kann man folgende Metadaten über eine Klasse bestimmen:

Da PHP4 vom Zend-Interpreter Gebrauch macht, existieren diese Funktionen auch in PHP4.


9. Variablen und Formulare

9.1 Wie übergebe ich Variablen aus einem Formular an ein PHP-Script?

Antwort von Kristian Köhntopp

Gar nicht. Wenn das action=-Attribut eines Formulares ein PHP-Script ist, dann stehen die Variablen aus dem Formular und aus den Cookies automatisch als Elemente in einem von drei Arrays in PHP zur Verfügung: Je nach der Art der Übergabe stehen sie in $HTTP_GET_VARS, $HTTP_POST_VARS oder $HTTP_COOKIE_VARS bereit. Wenn der Schalter register_globals in der Konfiguration von PHP4 gesetzt ist (und in PHP3 immer), dann stehen sie außerdem als globale Variablen bereit. Hinweis: Dies ist ein Sicherheitsrisiko und nicht empfohlen.

Weil es sich bei diesen drei Arrays um globale Variablen handelt, sind sie in Funktionen nicht sichtbar, es sei denn, man übergibt sie als Parameter oder importiert sie mit Hilfe der Anweisung global.

Die Stellung des Schalters register_globals kann man in der Ausgabe von phpinfo() leicht prüfen.

9.2 Wie kann ich ohne Formular Variablen an ein Script übergeben?

Antwort von Kristian Köhntopp

Indem man die Variablen mit urlencode() codiert und sie dann an die URL anhängt:


<?php
function req_url($url, $para) {
  $sep = "?";

  $u = $url;
  if (! is_array($para))
    return $url;

  reset($para);
  while(list($k, $v) = each($para)) {
    $url .= sprintf("%s%s=%s",
      $sep,
      $k,
      urlencode($v)
    );
    $sep = "&"
  }

  return $url;
}

$p = array(
  "a" =< "b",
  "c" =< "d"
);

$url = req_url("beispiel.php3", $p);
?>
Klicke auf das <a href="<?php print $url ?>">Beispiel</a>.

9.3 Wie viele Formularelemente kann ich auf einer Seite haben?

Antwort von Kristian Köhntopp

Wird das Formular mit POST übergeben, ist die Anzahl und Größe der Elemente möglicherweise begrenzt durch serverseitige Einstellungen (Apache: siehe LimitRequestBody und verwandte Direktiven).

Wird das Formular mit GET übergeben, ist die Anzahl der Variablen begrenzt durch die maximale Länge der URL, die der Browser und der Webserver verarbeiten können. Beim Browser ist dies vom Browser und der Browserversion abhängig. Beim Webserver ist das Limit unter Umständen konfigurierbar (Apache: siehe LimitRequestLine (8190) und verwandte Direktiven).

9.4 Sollte ich besser GET oder POST verwenden?

Antwort von Kristian Köhntopp

Im allgemeinen ist es besser, die Methode GET zu verwenden: Formulare sind leichter zu debuggen und der Anwender kann sich ein fertig ausgefülltes Formular mit Parametern in die Bookmarks oder einen Link legen - das ist bequem und ergonomisch.

Enthält das Formular Werte, die nicht in der URL angezeigt werden sollen und die ggf. nicht Bestandteil des Referer sein sollen und nicht in Proxy-Logs auftauchen sollen, dann ist die Verwendung von POST angezeigt. Dies ist zum Beispiel immer der Fall, wenn ein Eingabeelement Password verwendet wird.

Ebenfalls soll POST verwendet werden, wenn die Länge von Eingabeelementen nicht nach oben begrenzt ist, also immer dann, wenn ein TEXTAREA verwendet wird.

Schließlich ist die Verwendung von POST zwingend notwendig, wenn ein File-Upload durchgeführt werden soll, einmal wegen der prinzipiell unbegrenzten Länge, aber auch weil der notwendige ENCTYPE="multipart/form-data" nur mit POST zusammen funktioniert.

9.5 Wie kann man ein <select multiple> verarbeiten?

Antwort von Kristian Köhntopp

Das Formular muß so aussehen:


<form action="script.php3">
<select multiple size=3 name="avar[]">
<option value="a">Eins
<option value="b">Zwei
<option value="c">Drei
<option value="d">Vier
<option value="e">Fuenf
<option value="f">Sechs
</select>
<br>
<input type="submit" name="doit" value="Los!">
</form>

Entscheidend ist, daß der Name der Variablen im <select>-Tag mit eckigen Klammern endet, damit ein Array erzeugt wird. Das Script script.php3 erhält nun diese Variable $avar als Array und kann die Werte dieses Arrays aufzählen.

In PHP3 können auf diese Weise nur eindimensionale Arrays erzeugt werden, in PHP4 sind auch mehrdimensionale Felder möglich. In jedem Fall kann nur die letzte Dimension unbestimmt sein.

9.6 Wie kann man Checkboxen verarbeiten?

Antwort von Kristian Köhntopp

Wenn die Checkboxen nicht markiert sind, werden sie überhaupt nicht übermittelt. Andernfalls haben sie den im Attribut VALUE= angegebenen Wert. Man kann die Elemente auf die folgenden beiden Arten erzeugen:


# Fall 1: Verschiedene Namen, gleicher Wert
<input type="checkbox" name="cbutton[1]" value="yes">
<input type="checkbox" name="cbutton[2]" value="yes">

# Fall 2: "Gleiche" Namen, verschiedene Werte
<input type="checkbox" name="cbutton[]" value="1">
<input type="checkbox" name="cbutton[]" value="2">

Die Abfrage erfolgt in beiden Fällen mit


if (isset($cbutton)) {
  reset($cbutton);
  while(list($k, $v) = each($cbutton)) {
    print "$k $v\n";
  }
} else {
  print "alle cbutton schlafen schon.\n";
}

Im Fall 1 wertet man die $k aus, im Fall 2 die $v.

9.7 Wie funktioniert Datei-Upload über HTML-Formulare?

Antwort von Kristian Köhntopp

Ein Upload-Formular muß ein Input-Element enthalten, das den Typ file hat. Da Dateien in einem Upload prinzipiell beliebig groß werden können, muß die Übermittlung des Formulares mit der Methode POST erfolgen. Außerdem muß ein bestimmter ENCTYPE für das Formular angegeben werden. Ein solches Formular kann von Netscape Navigator ab Version 3 und von Microsoft Internet Explorer ab Version 4 verarbeitet werden.


<h1>Hallo</h1>
<form action="/submit.php3" method="post"
      enctype="multipart/form-data">
<input type="file" name="probe">
<input type="submit" value="los">
</form>

Das empfangende PHP-Script bekommt das Resultat des Datei-Uploads in einer Reihe von globalen Variablen mit dem Namensprefix $probe übermittelt, weil das Input-Element im Formular diesen Namen hat.

$probe

Diese Variable enthält den Namen der Datei in einem temporären Verzeichnis auf dem Server. Sie kann von dort mit einem copy()-Aufruf abgeholt werden. Das ist auch notwendig, da die Originaldatei am Ende des Scriptes automatisch gelöscht wird.

$probe_name

Diese Variable enthält den Namen der Datei auf dem System des Anwenders. Der genaue Dateiname mit evtl. vorhandenen Laufwerksbuchstaben, Pfadseparatoren und anderen Sonderzeichen ist betriebssystemabhängig und das empfangende Script sollte keine Annahmen hierüber machen.

$probe_size

Diese Variable enthält die Länge der Datei auf dem Server in Bytes.

$probe_type

Diese Variable enthält den MIME-Type der Datei, so wie er dem Server vom Browser übermittelt worden ist.

Der Upload von Dateien wird durch die beiden Konfigurationsparameter upload_tmp_dir und upload_max_filesize in der php3.ini gesteuert. Und in Windows muß man sich in derphp3.ini dringend ein gültiges upload_tmp_dirdefinieren, bevor das gezeigte Beispiel funktionieren kann. Des weiteren ist es unter Umständen notwendig, die Variable $probe mit der PHP-Funktion stripslashes() zu bearbeiten, bevor man auf sie mit copy() oder einem anderen Befehl zugreift. Dies liegt daran, dass Windows Pfadangaben scheinbar quotet, was der Befehl stripslashes() wieder rückgängig macht. Der Pfad zu upload_tmp_dir muß absolut angegeben werden.

PHP legt die temporäre Datei in dem angegebenen Verzeichnis an und löscht sie am Ende des Scriptes wieder. Die Datei darf maximal die angegebene Größe haben. Ein Einstellen der Größenbegrenzung begrenzt jedoch nicht wirklich den Plattenplatz, der auf dem Server von PHP durch Fileupload verbraucht wird: Aus technischen Gründen muß PHP die Datei zunächst empfangen und kann sie erst dann verwerfen, wenn sie zu groß ist. Seit PHP 3.0.10 kann mehr als eine Datei pro Formular hochgeladen werden.

Achtung:

Aufgrund eines Bugs in PHP muß das Script nach dem Upload prüfen, ob sich der/die Name(n) des Datei-Formularfeldes (im folgenden Beispiel probe) in den Hashes $HTTP_GET_VARS, $HTTP_POST_VARS oder $HTTP_COOKIE_VARS befindet. Ist dem so, muß das Script die Weiterverarbeitung (Kopieren) der - angeblich - hochgeladenen Datei(en) verweigern.

Mehr über neue Funktionen zum Upload-Handling der PHP Versionen > 3.0.16 und > 4.0.2 gibt es im Kapitel POST method uploads auf www.php.net.

Vollständiges Beispiel:


<h1>Upload</h1>

<form 
  action="<?php print $PHP_SELF ?>" 
  method="post" 
  enctype="multipart/form-data"> 
<input type="file" name="probe"> 
<input type="submit" value="Los!">
</form>
<hr> 
<?php
  if (isset($probe)) :

    // Bugfix für: http://www.securityfocus.com/archive/1/80106
    if ( isset($HTTP_COOKIE_VARS["probe"]) ||
         isset($HTTP_POST_VARS  ["probe"]) ||
         isset($HTTP_GET_VARS   ["probe"])         
       ) die("Aus Sicherheitsgründen stirbt das Script jetzt.");
         
    copy($probe, "./newfile.txt");
    printf("Die Datei %s steht jetzt als"
          ." newfile.txt zur Verfügung.<br>\n",
      $probe_name); 
    printf("Sie ist %s Bytes groß und vom Typ %s.<br>\n",
      $probe_size, $probe_type); 
  endif; 
 ?>

9.8 Wie kann ich aus einer Datenbanktabelle einen <Select> erzeugen?

Antwort von Kristian Köhntopp

Aus einer Datenbanktabelle mit einer List Of Values (LOV) soll eine Selectbox erzeugt werden:


<?php
/*
 * lovselection - turn a list of values table from a database
 *                into a html option list.
 *
 * (requires DB_Sql subclass from PHPLIB)
 *
 * by Kristian Köhntopp
 *
 */

function lovselection($classname, $table, $field, $oldvalue) {
  $ret = "";
  $db  = new $classname;

  $query = sprintf("select %s from %s",
             $field,
             $table
           );
  $db->query($query);
  while($db->next_record()) {
    if ($db->f("field") == $oldvalue)
      $selected = " selected";
    else
      $selected = "";
      
    $ret .= sprintf("<option%s>%s</option>\n", 
              $selected,
              $db->f($field)
            );
  }
  
  $db->free();
  return $ret;
}

/*
 * is_validlov - check if a given value is valid according
 *               to a given list of values
 *
 * by Kristian Köhntopp
 *
 */
function is_validlov($classname, $table, $field, $value) {
  $db = new $classname;
  
  $query = sprintf("select %s from %s where %s = '%s',
             $field,
             $table,
             $field,
             addslashes($value) // value from outside
                                // is potentially dangerous!
           );
  $db->query($query);
  if ($db->num_rows() == 1) {
    $db->free();
    return true;
  } else {
    $db->free();
    return false; // 0 or 2+ answers are a failure!
  }
}

?>

<!-- Formularfragment -->
<select name="f_ortsnetz" size="1">
<?php lovselection("DB_Example","ortsnetze","vorwahl",$ortsnetz) ?>
</select>


<!-- Auswertefragment -->
<?php
if (is_validlov("DB_Example"; "ortsnetze", "vorwahl", $f_ortsnetz))
  $ortsnetz = $f_ortsnetz; // value is valid
else {
  $error["ortsnetz"] = "Das angegebene Ortsnetz ist ungültig.";
  $ortsnetz = ""; // clear session variable
}
?>

Die Funktion lovselection() generiert aus einer Tabelle von Werten in der Datenbank (eine sogenannte List Of Values, LOV) eine Reihe von Option-Tags. Man kann dann leicht den passenden Select-Container drumwickeln. lovselection() verwendet die Datenbank-Klasse von PHPLIB, läuft also mit prinzipiell jeder SQL-Datenbank.

Wenn man einen solchen Select-Tag generiert, dann heißt das natürlich nicht, daß das so erzeugte Formular on Submit ausschließlich Werte zurückliefert, die man dort zur Auswahl gestellt hat. Stattdessen kann jeder beliebige Wert zurück kommen.

Es ist also notwendig, dass man ein Prädikat erzeugt, das überprüft, ob der eingegangene Wert gültig ist. Dieses Prädikat ist is_validlov(): Die Funktion liefert true, wenn der gegebene Wert genau einmal in der angegebenen Tabelle vorkommt.

Im Beispiel kann mit der Variablen $f_ortsnetz, die aus dem Formular kommt, nicht gearbeitet werden - sie kann potentiell ungültige Werte ("003432") enthalten oder sogar potentiell gefährliche Werte ("0431' or sp_clearall() or '0431", wobei sp_clearall() irgendeine Einbaufunktion oder Stored Procedure in einer Datenbank ist, die gefährliche Dinge mit der Datenbank macht).

Der Wert von $f_ortsnetz kann nach $ortsnetz kopiert und gefahrlos verwendet werden genau dann und nur dann, wenn is_validlov() wahr ist, denn dann kommt der Inhalt von $f_ortsnetz genau einmal in der angegebenen Tabelle vor und ist damit eine gültige und garantiert harmlose Auswahl.

Die Funktion is_validlov() selbst kann sich den Luxus nicht erlauben, mit harmlosen Werten zu arbeiten und muß daher so geschrieben sein, daß sie auch mit gefährlichen Inhalten in $value zurecht kommt. Ein - noch recht schwacher, aber einigermassen allgemeingültiger - Versuch, dies zu gewährleisten ist die Anwendung von addslashes() bevor der Wert von $value in das SELECT-Statement eingesetzt wird. Besser wäre es, könnte man Annahmen ueber das Aussehen von $value machen (keine Spaces, keine Single Quotes, keine Backslashes, ...) und würde man dann mit preg_match() abtesten, ob $value so aussieht, wie man erwartet, bevor man den Wert in das SELECT-Statement einsetzt.

9.9 Wie verarbeite ich <input type=image>?

Antwort von Kristian Köhntopp

In Formularen kann man statt eines SUBMIT auch ein IMAGE als Absendeknopf installieren. Dies sieht dann so aus:


<INPUT TYPE="image" SRC="meinbild.gif" NAME="sub">

Wenn der User das Bild anklickt, werden zwei Variablen mit den Namen sub.x und sub.y erzeugt, die die Koordinaten des Klicks relativ zur linken, oberen Ecke des Bildes beschreiben. Da Variablennamen in PHP keine Punkte enthalten dürfen, wandelt PHP die Punkte in Unterstriche um. Im Beispiel bekommt man die Variablen mit den Namen $sub_x und sub_y übergeben.


10. Dateifunktionen und Programmausführung

10.1 Wie kann ich eine Datei auslesen?

Antwort von Kristian Köhntopp

Man kann eine Datei manuell Öffnen und Zeile für Zeile lesen:


$fp = @fopen("datei", "r") or die ("Kann Datei nicht lesen.");
while ($line = fgets($fp, 1024)):
  machwas($line);
endwhile;
fclose($fp);

Dies verwendet die Funktionen fopen() und fgets(). Wenn die gelesenen Zeilen sofort ausgegeben werden sollen, dann kann man dies kürzer mit fpassthru() oder gar readfile() schreiben:


$fp = @fopen("datei", "r") or die ("Kann Datei nicht lesen.");
fpassthru($fp);
# fclose($fp); entfällt.

# Noch einfacher ist es mit readfile():
readfile("datei");

Will man stattdessen die Daten in der Datei in einem Array zur Verfügung haben, kann man file() verwenden. Will man die Daten in der Datei in einem einzigen String zur Verfügung haben, muß man dies mit implode() kombinieren:


# Einlesen in Array
$avar = file("datei");

# Einlesen in String
$str = implode("", file("datei"));

# Mit unterdrückten Meldungen
$str = implode("", @file("datei"));

In jedem Fall kann man den Funktionen wie üblich einen Klammeraffen voranstellen, um die Fehlermeldungen zu unterdrücken.

Im safe_mode unterliegt das Lesen und Schreiben von Dateien weiteren Einschränkungen.

10.2 "Warning: Supplied argument is not a valid File-Handle resource"

Antwort von Kristian Köhntopp

Ein Script versucht mit einem Filehandle ($fp) zu arbeiten, das das Resultat eines fopen() ist ($fp = fopen("..", "r") oder ähnlich). Dieses Filehandle ist ungültig, z.B. weil die Datei nicht existiert oder die Zugriffsrechte das Öffnen nicht gestatten.

Das Script ist fehlerhaft, weil es nach dem fopen() nicht prüft, ob das fopen() erfolgreich war:


        $fp = fopen(..., "r");

        /* Das fehlt zum korrekten Code:  */
        if (!$fp)
                die("Kann Datei ... nicht oeffnen.\n");

        /* Diese Anweisung macht dann Ärger */
        while($line = fgets($fp, 1000)) {
        ...
        }

10.3 Wie kann ich ein externes Programm von PHP aus starten?

Antwort von Kristian Köhntopp

PHP kennt nicht weniger als fünf Mechanismen, um externe Kommandos (z.B. Unix-Shellbefehle) von PHP aus zu starten. Alle diese Mechanismen können zu einem Sicherheitsrisiko werden, wenn man Benutzereingaben Bestandteil der ausgeführten Kommandos oder Dateinamen werden läßt.

Durch Anwendung der Funktion EscapeShellCmd() kann man das Risiko etwas vermindern (etwa: system(escapeshellcmd($cmd))). Dennoch empfiehlt es sich, die Parameter, die in die Gestaltung von $cmd eingehen, sorgfältig zu prüfen.

Externe Kommandos werden bei Verwendung von CGI PHP unter der Identität des CGI-Wrappers ausgeführt, bei Verwendung einer Modulversion von PHP mit der Identität des Webservers (siehe auch Webserver verstehen und tunen von Kristian Köhntopp).

Will man ein externes Kommando einfach nur ausführen, kann man das betreffende Kommando einfach in Backticks setzen:


  `touch yyy`;

Dies wird im selben Verzeichnis wie das PHP-Script die Datei yyy erzeugen, falls der Webserver dort Schreibrecht hat.

Alternativ kann man ein Kommando durch die Funktion exec() starten lassen. Auch hier ist die Ausgabe nicht sichtbar, kann aber in einem Array abgelegt werden:


  exec("cat /etc/group", $lines, $result);
  echo "result = $result<br>";

  echo "Lines<br>\n";
  reset($lines);
  while(list($k, $v) = each($lines)):
    echo "k=$k v=$v<br>\n";
  endwhile;

Die Ausgabe des Befehls wird im Feld $lines zur Verfügung gestellt, der Exitcode des Befehls in $result.

Die Funktion system() gibt die Ausgabe des Unix-Kommandos dagegen an den Webserver weiter. Ebenso passthru():


  system("ls -l", $result);
  echo "Result: $result<br>\n";

  passthru("ls -l", $result);
  echo "Result: $result<br>\n";

Schließlich kann man externe Kommandos noch mit Hilfe der Funktion popen() starten:


  $fp = popen("ls -l", "r");
  while($line = fgets($fp, 1024)):
    printf("%s<br>\n", $line);
  endwhile;

Im safe_mode unterliegt die Programmausführung weiteren Einschränkungen.

Antwort von Johannes Frömter

PHP wartet bei der Ausführung externer Programme auf deren Beendigung, d.h. das PHP-Script ist solange blockiert, bis das aufgerufene Programm fertig ist. Um dies zu vermeiden, muß man den Output des Programms umleiten, z.B. nach /dev/null:


exec("programm >/dev/null 2>&1");

10.4 Wie realisiere ich einen Dateidownload mit PHP?

Antwort von Kristian Köhntopp

Grundsätzlich kann man einen Dateidownload auf zwei verschiedene Arten realisieren: Entweder man schreibt ein PHP-Script, das einen Redirect (siehe Wie erzeuge ich mit PHP einen Redirect auf eine andere Seite?) auf die zu ladende Datei generiert, oder man startet den Download durch das PHP-Script. Die Methode mit dem Redirect hat den Nachteil, daß Anwender die Ziel-URL des Redirect mitbekommen und später dann direkt und ungeschützt auf diese Datei zugreifen können.

Will man das verhindern, muß man den Download innerhalb von PHP abhandeln. Die zu ladenden Dateien liegen dann außerhalb der Document Root des Webservers (haben also keine URL) und sind nur durch PHP zugreifbar. In PHP sendet man den passenden MIME-Typ als Header und schickt dann die gewünschte Datei hinterher. Natürlich kann man vorher noch einen Downloadzähler aktualisieren oder überprüfen, ob der Anwender überhaupt für den Download autorisiert ist.


# $download sei der Bezeichner für die zu ladende Datei

# Dieses Verzeichnis liegt außerhalb der Document_Root und
# ist nicht per URL zuzugreifen.
$basedir = "/home/www/download";

# Übersetzung von Download-Bezeichner in Dateinamen.
$filelist = array(
  "file1" => "area1/datei1.zip",
  "file2" => "area1/datei2.zip",
  "file3" => "area2/datei1.zip"
);

# Einbruchsversuch abfangen.
if ($filelist[$download] == "")
  die("Datei $download nicht vorhanden.");

# Vertrauenswürdigen Dateinamen basteln.
$filename = sprintf("%s/%s", $basedir, $filelist[$download]);

# Passenden Datentyp erzeugen.
header("Content-Type: application/octet-stream");

# Passenden Dateinamen im Download-Requester vorgeben,
# z.B. den Original-Dateinamen
$save_as_name = basename($filelist[$download]);
header("Content-Disposition: attachment; filename=\"".$save_as_name."\"");

# Datei ausgeben.
readfile($filename);

Dieses Script kann mit dem Parameter $download aufgerufen werden. Dieser Parameter wird dann in den Namen einer zu ladenden Datei übersetzt. Aus Sicherheitsgründen ist es nicht möglich, dem Script direkt Dateinamen zu übergeben - wir möchten vermeiden, daß jemand als Parameter download=../../../../../../../etc/passwd oder ähnliche Namen erfolgreich übergeben kann.

Antwort von Guido Haeger

Die Reaktion des UserAgent auf die oben genannten Header ist in den RFCs für HTTP und MIME nicht eindeutig definiert. Eventuell versucht der jeweilige User-Agent deshalb, die Datei mit der Standardanwendung für die jeweilige Extension zu öffen. Über Probleme wurde insbesondere bei einigen Versionen des Microsoft Internet Explorers in Verbindung mit PDF-Dateien berichtet. Laut einigen Berichten in der Newsgroup de.comp.lang.php kann die Verwendung von Content-Type-Header wie "x-type/subtype" die Probleme lösen. Das einleitende "x-" kennzeichnet diese Content-Type-Angabe als nicht standardisierten Content-Type. Das Verhalten des jeweiligen User-Agents bei derartigen Headern ist somit ebenfalls nicht standardisiert.

10.5 Wie kann ich in einer Datei eine Zeile einfügen oder löschen?

Antwort von Kristian Köhntopp

Für dieses Problem gibt es keine elegante oder effiziente Lösung. Die Ursache liegt darin, wie Unix und Windows die unterliegenden Dateien handhaben, nämlich als unstrukturierte Byteströme. Für diese Byteströme gibt es keine Indices und auch keine Methoden, mit denen man effizient beliebige Teile der Datei löschen oder in die Datei einfügen könnte.

Tatsächlich ist der Wunsch nach einfachen Einfüge- und Löschoperationen der Auslöser für die Schaffung von Datenbankfunktionen wie die DBM-Funktionen oder von ganzen Datenbanken wie MySQL gewesen. Wenn man auf diese Sorte Problem trifft, sollte man also intensiv über den Einsatz von DBM-Dateien oder Datenbanken nachdenken.

Um in einer Datei eine Zeile einzufügen oder zu löschen, muß man die Datei öffnen und zeilenweise durchlesen und in eine zweite Datei schreiben. Erreicht man die gewünschte Position, muß man dort eine Zeile einfügen oder löschen. Nach Abschluß der Operation ist die Originaldatei zu löschen und die neue Datei umzubenennen. Dabei ist zu beachten, daß in einer Webumgebung ohne weiteres mehrere Benutzer zugleich eine solche Operation für dieselbe Datei anfordern können. Man muß also auch durch Locking dafür Sorge tragen, daß sich diese Benutzer nicht in die Quere kommen.


# Shared lock auf die Quelldatei
$old = fopen($oldfile, "r");
flock($old, 1) or die("Kann die Quelldatei $oldfile nicht locken.");

# Exclusive lock auf die Zieldatei
$new = fopen($oldfile.".new", "w");
flock($new, 2) or die("Kann die Zieldatei $newfile nicht locken.");

$lineno = 0;

while($line = fgets($old, 1024)):
  if ($lineno++ == $zielzeile)
    continue;  # Zeile auslassen

  fputs($new, $line);
endwhile;

fclose($old); # Gibt das Lock automatisch auf

# Alte Datei wegwerfen.
unlink($oldfile);

# Neue Datei umbenennen.
# (In Windows müssen das rename() und das fclose($new)
#  vertauscht werden, da es nicht möglich ist, in Windows
#  eine offene Datei umzubenennen.
rename($oldfile.".new", $oldfile);

# Neue Datei schließen und dabei Lock aufgeben.
fclose($new);

Möchte man nur Daten an eine Datei anhängen (z.B. beim Schreiben eines Logfiles), reicht es, die betreffende Datei im Append-(Anhängen)-Modus zu öffnen. Diese Ergänzung wurde von Ralf D. Kloth vorgeschlagen.


<?php
  $fp = fopen($filename, 'a+');
  flock($fp, 2) or die("Kann die Datei nicht locken.");
  fwrite($fp, "$zeile \n");
  fclose($fp);
?>

10.6 Wie kann ich einen Datei-Upload per FTP durchführen?

Antwort von Kristian Köhntopp

Am einfachsten geht dies, wenn das verwendete PHP neu genug ist und mit der Option -enable-ftp (bei PHP 3: -with-ftp) übersetzt worden ist. In der Ausgabe von phpinfo() erscheint dann in der Modulliste der FTP-Support.

Die Funktionen sind im PHP-Manual im Kapitel FTP-Funktionen beschrieben.

Vollständiges Beispiel:


<h1>FTP Test</h1>
<?php
  $link = ftp_connect("localhost");
  if (!ftp_login($link, "ftp", "user@host.de"))
    die("Kann mich nicht einloggen.");

  if (!ftp_chdir($link, "/pub"))
    die("Kann nicht in das Zielverzeichnis /pub wechseln.");

  $name = ftp_nlist($link, ".");
  if (isset($name) and is_array($name)) {
    reset($name);
    while(list($k, $v) = each($name)) {
      printf("%s - %s<br>\n", $k, $v);
    }
  }

  $size = ftp_size($link, "beispiel");
  if ($size < 0)
    die("Kann die Größe der Datei beispiel nicht bestimmen.");

  $mtime = ftp_mdtm($link, "beispiel");
  if ($mtime < 0)
    die("Kann die mtime der Datei beispiel nicht bestimmen.");

  printf("beispiel - %d Byte, %s<br>\n",
    $size,
    strftime("%c", $mtime));

  $result = ftp_get($link, "/tmp/bbb", "beispiel", FTP_BINARY);
  if (!$result)
    die("Download von Datei beispiel fehlgeschlagen.");

  if (!ftp_chdir($link, "/incoming"))
    die("Kann nicht in das Zielverzeiczhnis /incoming wechseln.");

  $result = ftp_put($link, "upload.txt", "/etc/termcap", FTP_BINARY);
  if (!$result)
    die("Upload von Datei termcap fehlgeschlagen.");

  ftp_quit($link);

  printf("Ende.<br>\n");
?>

10.7 Unix: Welche Zugriffsrechte brauche ich, um eine Datei anzulegen?

Antwort von Kristian Köhntopp

Um in Unix eine Datei anlegen zu können, benötigt ein Programm x-Rechte an jedem Verzeichnis entlang des Pfadnamens sowie w-Recht an dem Verzeichnis, in dem die Datei angelegt werden soll.

w-Recht an einem Verzeichnis berechtigt nicht nur dazu, eine Datei neu anzulegen, sondern es berechtigt außerdem dazu, die Namen existierender Dateien aus dem Verzeichnis zu entfernen. Dies wird in Unix oft vereinfachend als "das Löschen einer Datei" bezeichnet. Beim Entfernen von Namen aus einem Verzeichnis werden die Rechte an der Datei und ihr Eigentümer nicht geprüft.

Dies ändert sich, setzt man an dem Verzeichnis, in dem sich die Datei befindet, zusätzlich das t-Recht. In diesem Fall können nur der Superuser (genauer: jeder Prozeß der CAP_FOWNER Capability hat), der Eigentümer des Verzeichnisses oder der Eigentümer der Datei noch den Namen aus dem Verzeichnis entfernen.

Um eine existierende Datei zu überschreiben, ist dagegen x-Recht an jedem Verzeichnis entlang des Pfadnamens notwendig, sowie w-Recht an der zu verändernden Datei (oder man hat CAP_FOWNER Capability).

In Apache mit suexec ist es so, daß CGI-Programme mit derjenigen User-ID und Group-ID ausgeführt werden, die für den virtuellen Webserver mit Hilfe der Direktiven User und Group definiert worden sind. CGI-PHP legt also Dateien mit diesen User- und Group-Rechten an und das Zielverzeichnis muß entsprechende Rechte besitzen, damit ein fopen() mit dem Anlegen einer Datei erfolgreich sein kann.

In Apache mit mod_php ist es so, daß der PHP-Interpreter als Bestandteil des Webservers läuft und mit den Rechten des Webservers Dateien anlegt oder löscht. In diesem Fall ist ausschlaggebend, was die globale (serverweite) User- und Group-Direktive für den Webserver festlegt. Das Zielverzeichnis muß für diesen Unix-Benutzer passende Rechte anbieten.

In PHP kann es sein, daß zusätzliche Beschränkungen gelten, die in Kraft treten, falls safe_mode aktiviert ist. Dies kann in CGI PHP in der php.ini geschehen, in mod_php zusaetzlich in <Directory>-Blöcken in der zentralen httpd.conf, jedoch nicht in .htaccess-Dateien.

Es ist empfehlenswert, für jeden virtuellen Webserver eine gesonderte CGI-Identität anzulegen und jedem virtuellen Webserver ein Verzeichnis einzuräumen, das keine URL hat (nicht unterhalb der Document Root liegt) und das nur durch diese CGI-Identität beschreibbar ist. Auf diese Weise kann jeder virtuelle Webserver anwendungsspezifische Daten auf eine Weise speichern, die nicht durch das http-Protokoll erreichbar ist und die nicht durch andere CGI-Programme anderer virtueller Webserver gelesen oder geschrieben werden koennen.

Wird mod_php verwendet, ist ein solcher Schutz nur mit safe_mode erreichbar. Dies birgt jedoch andere Nachteile.

10.8 Wie kann ich mit PHP auf die serielle Schnittstelle zugreifen?

Antwort von Matthias P. Wuerfl

Da PHP serverseitig interpretiert und ausgeführt wird, ist es nicht möglich, mit PHP auf die serielle Schnittstelle eines Clients zuzugreifen. Allerdings besteht die Möglichkeit, auf die seriellen Schnittstellen des Computers zuzugreifen, auf dem das Script ausgeführt wird. Hierzu müssen jedoch die entsprechenden Rechte gesetzt sein, so daß der PHP-Interpreter oder der Webserver auch darauf zugreifen dürfen.

Windows und Unixe stellen die serielle Schnittstelle im Filesystem zur Verfügung und man kann mit den normalen Dateisystemfunktionen darauf zugreifen. Um Daten an die serielle Schnittstelle zu senden, genügt schon ein sehr kleines Script:


$string = "Hallo Schnittstelle!\n";
$pointer = fopen("/dev/ttyS0","w");
fwrite ($pointer, $string);
fclose($pointer);

In diesem Beispiel wird die serielle Schnittstelle /dev/ttyS0 zum Schreiben geöffnet, ein String wird hineingeschrieben und die Schnittstelle wird wieder geschlossen. Analog dazu könnte man die Schnittstelle auch zum Lesen öffnen und mit fgets() auf Input warten. Hierbei ist allerdings zu beachten, daß das Script nicht weiterarbeitet, bis nicht eine Zeile eingelesen ist. Können von der seriellen Schnittstelle keine Daten gelesen werden, so stoppt das Script beim fgets() und wartet bis zum Timeout.

Die serielle Schnittstelle kann nicht zum gleichzeitigen Lesen und Schreiben geöffnet werden; jedoch kann ein Script lesend darauf zugreifen, während ein anderes schreibt. Da die seriellen Schnittstellen einen bunten Strauß an Konfigurationsmöglichkeiten kennen, ist die Konfiguration der Schnittstelle auf Betriebssystemebene mitunter etwas kniffelig. Bevor man mit PHP darauf zugreift, sollte man seine Konfiguration mit den Mitteln des Betriebssystems testen, um bei der Erstellung eines PHP-Scriptes eine falsche Schnittstellenkonfiguration als Grund für das Nichtfunktionieren auszuschließen.

10.9 Warum funktioniert unlink() unter Windows nicht?

Antwort von Martin Jansen

In einigen älteren Versionen von PHP 4 ist die Funktion unlink() unter Windows nicht implementiert, was sich in der Fehlermeldung


Warning: unlink() is not supported in this PHP build in ...
on line ...

ausdrückt. Abhilfe schafft hier ein Update auf eine aktuelle Version von PHP 4.


11. Datums- und Kalenderprobleme

11.1 Wie kann ich das aktuelle Datum bekommen?

Antwort von Kristian Köhntopp

Mit Hilfe der Funktion time() bekommt man den Unix-Timestamp, der sich mit anderen Funktionen (strftime())weiterverarbeiten läßt. Mit der Funktion date() kann man das Datum direkt als String formatiert abrufen.

11.2 Wie kann ich ein deutsches Datum in MySQL-Format umwandeln (und umgekehrt)?

Antwort von Kristian Köhntopp

MySQL verarbeitet Datumsangaben im ISO-8601-Format (siehe die Abhandlung von Markus Kuhn zu diesem Thema). Dies ist das offizielle deutsche Datumsformat, eine Umwandlung ist nicht notwendig, weil nicht normgerecht.

Dennoch kann man das Datum auch in anderen Formaten bekommen. Wahlweise kann die Umwandlung bereits in MySQL oder erst in PHP geschehen. In MySQL kann man mit Hilfe der Funktion date_format das Datum in beliebigen Formaten bekommen:


mysql> select date_format(changed, '%d.%m.%Y %H:%i:%s') as datum
    ->   from teiln_liste;
+---------------------+
| datum               |
+---------------------+
| 27.08.1998 12:49:29 |
| 14.12.1999 15:05:52 |
| 13.12.1999 08:30:43 |
| 13.05.1998 15:51:45 |
| 06.10.1998 14:30:25 |
| 07.08.1998 11:28:59 |
| 23.06.1998 17:15:16 |
| 14.01.1999 08:34:22 |
| 07.01.2000 11:36:42 |
| 01.02.1999 08:47:25 |
+---------------------+
10 rows in set (0.00 sec)

In PHP3 kann man mit Hilfe der Funktion date() aus einem time_t ein beliebiges Datum generieren und mit Hilfe der Funktion mktime() aus den Fragmenten einer Datumsangabe einen time_t erzeugen. Mit den time_t (Sekunden seit Mitternacht GMT, 1. Januar 1970) lassen sich sehr natürlich Zeitdifferenzen bestimmen und andere Zeitrechnungen ausführen.

Will man dagegen nur einen MySQL TIMESTAMP oder DATE in andere Reihenfolge umsortieren, kann man stattdessen mit den Funktionen explode() oder substr() arbeiten.


###
#
# date_mysql2german - wandelt ein MySQL-DATE (ISO-Date)
#                     in ein traditionelles deutsches Datum um.
#
function date_mysql2german($datum) {
  list($jahr, $monat, $tag) = explode("-", $datum);

  return sprintf("%02d.%02d.%04d", $tag, $monat, $jahr);
}

###
#
# date_german2mysql - wandelt ein traditionelles deutsches Datum
#                     nach MySQL (ISO-Date).
#
function date_german2mysql($datum) {
  list($tag, $monat, $jahr) = explode(".", $datum);

  return sprintf("%04d-%02d-%02d", $jahr, $monat, $tag);
}

###
#
# timestamp_mysql2german - wandelt ein MySQL-Timestamp
#                          in ein traditionelles deutsches Datum um.
#
function timestamp_mysql2german($t) {
  return sprintf("%02d.%02d.%04d",
    substr($t, 6, 2),
    substr($t, 4, 2),
    substr($t, 0, 4));
}

11.3 Wie kann ich die Anzahl der Tage zwischen zwei Daten bestimmen?

Antwort von Kristian Köhntopp

Dazu gibt es verschiedene Lösungsansätze. Beispielsweise kann man beide Daten in Julianische Tage verwandeln und sie dann voneinander subtrahieren. Das geschieht mit der Funktion GregorianToJD() aus der optionalen Kalenderbibliothek von PHP (muss erst kompiliert werden).

Eine andere mögliche Vorgehensweise ist es, die Daten in Timestamps umzuwandeln und dann voneinander abzuziehen. Marcus Schwarz zeigt in einem kurzen Artikel, wie das gemacht werden kann.

11.4 Wie kann ich das Datum des Vortages bestimmen?

Antwort von Kristian Köhntopp

Wenn man date() und mktime() kombiniert, kann man Datumsberechnungen durchführen. Die Funktion mktime() berechnet automatisch den korrekten Wert für Überläufe - der 32.12.1997 wird richtig in 01.01.1998 umgewandelt. So erhält man also auch das Datum des Vortages:


$tstamp    = mktime(0, 0, 0, date("m"), date("d")-1, date("Y"));
$gestern = date("Y-m-d", $tstamp); // ISO-8601 Format
print $gestern;

Beachte: Die Reihenfolge der Parameter in der PHP-Funktion mktime() entspricht nicht der Reihenfolge der Parameter in der C-Funktion gleichen Namens.

11.5 Wieviel Tage hat der aktuelle Monat?

Antwort von Kristian Köhntopp

date() versteht seit PHP 3.0.9 die Option "t", die die Anzahl der Tage im Monat zurückliefert:


$tage = date("t");

Benutzer älterer Versionen behelfen sich mit


$tstamp = mktime(0, 0, 0, date("m")+1, 0, date("Y"));
$tage = date("d", $tstamp);
print $tage;


12. Mail lesen und schreiben

12.1 Was ist SMTP?

Antwort von Kristian Köhntopp

SMTP ist das Simple Mail Transport Protocol, das Protokoll, das im Internet verwendet wird, um Mail bei einem Mailserver einzuliefern und Mail zwischen zwei Mailservern auszutauschen. SMTP ist ein textorientiertes Protokoll, das auf dem TCP-Port 25 abgewickelt wird. Daher kann man es mit dem Kommando telnet leicht simulieren und debuggen.


kris@valiant:~ > telnet mail 25
Trying 193.102.57.5...
Connected to white.koehntopp.de.
Escape character is '^]'.
220 white.koehntopp.de ESMTP Sendmail 8.9.3/8.9.3/SuSE Linux 8.9.3-0.1
HELO valiant.koehntopp.de
250 white.koehntopp.de Hello valiant.koehntopp.de [193.102.57.3], [..]
MAIL FROM: kris@koehntopp.de
250 kris@koehntopp.de... Sender ok
RCPT TO: kris@koehntopp.de
250 kris@koehntopp.de... Recipient ok
DATA
354 Enter mail, end with "." on a line by itself
From: ab@sen.der
To: Mailingliste der Empfaenger <in@vali.de>
Subject: Testmail

Text der Nachricht
.
250 LAA08243 Message accepted for delivery
QUIT
221 white.koehntopp.de closing connection
Connection closed by foreign host.

Nachdem die Verbindung mit dem empfangenden Mailer hergestellt worden ist, identifiziert sich der absendende Rechner mit dem Kommando HELO. Falls dieser Rechner berechtigt ist, überhaupt Mail einzuliefern, antwortet der empfangende Rechner mit einem Statuscode (hier: 250) und einer Textmeldung.

Der Absender nennt jetzt den tatsächlichen Absender (MAIL FROM) und den tatsächlichen Empfänger (RCPT TO, "Receipt to") der Mail. Nach dem Kommando DATA beginnt die Übertragung der eigentlichen Mailnachricht, die mit einem einfachen Punkt abgeschlossen wird (das Protokoll verlangt, daß Punkte am Anfang einer Zeile, die Teil der Nachricht sind, verdoppelt werden).

Die Absender und Empfänger im Text der Mail sind bedeutungslos und werden vom Mailer nicht weiter ausgewertet: Entscheidend sind sie nur ganz am Anfang, wenn die Mail auf den Weg gebracht wird, ähnlich wie bei einem Brief, der in einen Umschlag getütet wird. Von Bedeutung sind nur die Absender und Empfänger im MAIL FROM und RCPT TO-Dialog. Diese beiden Adressen nennt man Envelope-From und Envelope-To, nach den Anschriften, die außen auf einem Briefumschlag (Envelope) stehen und für die Zustellung wichtig sind.

Dies bewirkt zum Beispiel, daß die Empfänger einer Mailingliste nicht alle in der To:-Zeile des Headers aufgelistet sind: Im To: steht nur der Name der Liste, der Listenroboter (Mailexploder) expandiert diesen Namen jedoch zu einer Liste der Empfänger im Envelope. Der Envelope-From einer Nachricht ist nach der Auslieferung einer Mail in eine Mailbox als From-Header (From-Space im Gegensatz zu From-Doppelpunkt, dem Body-From) ganz am Anfang der Nachricht enthalten. Der Envelope-To ist verschwunden, weil er nicht mehr gebraucht wird - daher ist es nicht möglich, eine einzelne POP3-Mailbox nach dem Download mit Fetchmail oder anderen Werkzeugen korrekt im Hause weiter aufzuteilen: Der tatsächliche Empfänger steht nicht mehr zur Verfügung und die Body-To-Zeilen liefern nur Anhaltspunkte.

12.2 Was ist das Domain Name System?

Antwort von Kristian Köhntopp

Das Domain Name System ist eine verteilte Datenbank, die Namen in Domainform (ein.wort.mit.punkten.darin) beliebige getypte textuelle Information zuordnen kann. Man kann dem Domain Name System Fragen stellen, die die Form (name, typ) haben und bekommt dann einen oder mehrere Ressource Records des passenden Typs als Antwort.

Wichtige Ressource-Records sind A- und AAAA-Records, die die IP-Nummern von Rechnern darstellen, A-Records für IPv4-Adressen und AAAA-Records für IPv6-Adressen. Außerdem sind für die Mailzustellung MX-Records besonders wichtig, weil sie den für den Mailempfang zuständigen Rechner bezeichnen. Ebenfalls häufig findet man PTR-Records (Umwandlung von IP-Nummern zurück in Namen) und NS-Records (Finden von zuständigen Nameservern).

Mit Hilfe des Werkzeuges nslookup kann man in Unix und Windows Anfragen an das DNS stellen, außerdem kann man bequemer das Unix-Programm dig verwenden.

Ein Mailer wie das Unix-Programm sendmail richtet sich bei der Mailzustellung nach den MX-Records (MX steht für mail exchanger) einer Domain.


kris@valiant:~ > nslookup
Default Server:  nuki.netuse.de
Address:  193.98.110.1

> set type=mx
> php.net.
Server:  nuki.netuse.de
Address:  193.98.110.1

Non-authoritative answer:
php.net preference = 10, mail exchanger = php.chek.com

Authoritative answers can be found from:
php.net nameserver = NS1.EASYDNS.com
php.net nameserver = NS2.EASYDNS.com
php.net nameserver = REMOTE1.EASYDNS.com
php.net nameserver = REMOTE2.EASYDNS.com
php.chek.com    internet address = 208.247.106.167
NS1.EASYDNS.com internet address = 205.210.42.21
NS2.EASYDNS.com internet address = 205.210.42.22
REMOTE1.EASYDNS.com     internet address = 208.247.106.167
REMOTE2.EASYDNS.com     internet address = 198.96.119.44
> ^D
kris@valiant:~ >

kris@valiant:~ > dig php.net mx

; <<>> DiG 8.2 <<>> php.net mx
;; res options: init recurs defnam dnsrch
;; got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 5
;; QUERY SECTION:
;;      php.net, type = MX, class = IN

;; ANSWER SECTION:
php.net.                5h11m23s IN MX  10 php.chek.com.

;; AUTHORITY SECTION:
php.net.                1d19h26s IN NS  NS1.EASYDNS.com.
php.net.                1d19h26s IN NS  NS2.EASYDNS.com.
php.net.                1d19h26s IN NS  REMOTE1.EASYDNS.com.
php.net.                1d19h26s IN NS  REMOTE2.EASYDNS.com.

;; ADDITIONAL SECTION:
php.chek.com.           19h41s IN A     208.247.106.167
NS1.EASYDNS.com.        1d19h23m28s IN A  205.210.42.21
NS2.EASYDNS.com.        1d19h23m28s IN A  205.210.42.22
REMOTE1.EASYDNS.com.    1d19h23m28s IN A  208.247.106.167
REMOTE2.EASYDNS.com.    1d19h23m28s IN A  198.96.119.44

;; Total query time: 86 msec
;; FROM: valiant to SERVER: default -- 193.102.57.4
;; WHEN: Mon Feb 14 11:57:54 2000
;; MSG SIZE  sent: 25  rcvd: 221

Im Beispiel mit nslookup, das so unter Unix und unter Windows NT funktioniert, kann man sehen, daß der lokale Domain Name Server als Datenquelle verwendet wird. Durch das Kommando set type=mx legen wir fest, daß wir Informationen über MX-Records haben möchten. Durch einfaches Eingeben eines Namens (hier: php.net) wird die Anfrage abgesendet und wir erhalten zwei Sorten von Informationen:

Die Ausgabe von dig liefert diese Information noch einmal, nur in besser lesbarer und übersichtlicherer Form.

12.3 Unix: Wie funktioniert der Mailversand?

Antwort von Kristian Köhntopp

Ein Unix-System hat in der Regel ein Mailprogramm installiert, das Anfragen an das DNS-System stellen kann oder auf andere Weise die zur Auslieferung der Mail benötigten Informationen beschaffen kann. In den meisten Fällen ist dieses Mailprogramm /usr/lib/sendmail (oder /usr/sbin/sendmail) und versteht eine Option -t, die bewirkt, daß man das Programm starten und eine Mail mit allen benötigten Headern auf der Standardeingabe hinterlassen kann. Das Programm wird dann auf der Basis der erhaltenen Headerinformationen den Envelope generieren, den Header vervollständigen und ggf. korrigieren, bevor die Mail dann abgesendet wird.

Der einfachste Weg, um ein Programm zu starten und dann seine Standardeingabe mit Daten zu füllen, ist die C-Funktion popen(). Die PHP-Funktion mail() verwendet diese C-Funktion, um das durch die Konfigurationsvariable sendmail_path definierte Mailprogramm zu starten und mit den benötigten Daten zu füttern.


  mail("em@pfaeng.er",
     "Testmail", 
     "Dies ist nur eine Testnachricht.", 
     "From: ab@send.er\nReply-To: devnull@send.er");

Hinweis: In vielen Versionen von PHP unter Windows funktioniert die Variante von mail() mit drei Parametern wegen eines Fehlers im Interpreter nicht. In diesem Fall ist die Funktion mail() zwingend mit vier Parametern aufzurufen, etwa indem als vierter Parameter der Wert Content-Type: text/plain; charset=iso-8859-1\n angegeben wird.

Alternativ kann man diese Funktionalität auch manuell in PHP nachprogrammieren, also die PHP-Funktion popen() aufrufen und die Mail dann ausgeben. Beide Ansätze sind funktional gleich (aber mail() ist weniger Arbeit).


  $fd = popen("/usr/sbin/sendmail -t ","w");
  fputs($fd, "To: em@pfaeng.er\n");
  fputs($fd, "From: ab@send.er\n");
  fputs($fd, "Reply-To: devnull@send.er\n");
  fputs($fd, "Subject: Testmail\n\n");
  fputs($fd, "Das ist nur eine Testnachricht.\n");
  pclose($fd);

12.4 Windows: Wie funktioniert der Mailversand?

Antwort von Kristian Köhntopp

In Windows kann man nicht davon ausgehen, daß wie in Unix ein Mailer installiert ist, der Mail selber zustellen kann. Daher muß man dem Windows-System einen Rechner mitteilen, der einen Mailer installiert hat, den das Windows-System über TCP/IP mitbenutzen darf. Das kann der lokale Rechner localhost sein, falls auf dem eigenen System ein Mailserver installiert ist, aber auch jedes andere System, das für uns seinen Relay- und Spamschutz abgeschaltet hat.

Die Funktion mail() baut unter Windows eine TCP/IP-Verbindung zum Port 25 dieses in der Konfigurationsvariable SMTP festgelegten Systems auf und erzeugt mit dem oben gezeigten SMTP-Dialog eine Mail. Dabei wird die in der Konfigurationsvariablen sendmail_from festgelegte Absenderadresse verwendet.

Anders als in Unix hat man hier also nicht die Freiheit, den Absender oder Blindkopienempfänger in der Mailfunktion frei definieren zu können, weil der entfernte Mailer diese Daten von uns nicht annimmt oder bei der späteren Headerkorrektur wieder überschreibt.

Alternativ kann man die Arbeit der Mailfunktion auch manuell nachprogrammieren - dabei sollte man jedoch bedenken, daß ein SMTP-Server nicht jede Headerzeile so annimmt oder unverfälscht durchläßt, wie man sie ihm vorwirft! Der hier gezeigte Code ist sehr unzuverlässig, denn er hat keine richtige Fehlerprüfung.


$hdr  = "From: ab@send.er\r\n";
$hdr .= "To: em@pfaeng.er\r\n";
$hdr .= "Reply-To: devnull@send.er\r\n";
$hdr .= "Subject: Testmail\r\n";
$hdr .= "\r\n";

# Socket oeffnen.
$fp = fsockopen("mail.server.de", 25);
$banner = fgets($fp, 1024);

# HELO
fputs($fp, "HELO mein.eigener.servername.de\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 220)
        die("HELO Statuscode falsch: $result");

# MAIL FROM
fputs($fp, "MAIL FROM: ab@send.er\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 250)
        die("MAIL FROM Statuscode falsch: $result");

# RCPT TO
fputs($fp, "RCPT TO: em@pfaeng.er\r\n");
$result = fgets($fp, 1024); 
if ($result+0 != 250)
        die("RCPT TO: Statuscode falsch: $result");

# DATA
fputs($fp, "DATA\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 354)
        die("DATA: Statuscode falsch: $result");

# Header senden
fputs($fp, $hdr);

# Text senden
fputs($fp, "Das ist nur eine Testnachricht.\r\n");

# .
fputs($fp, ".\r\n");
$result = fgets($fp, 1024);
if ($result+0 != 250)
        die("DATA(end): Statuscode falsch: $result");

# QUIT
fputs($fp, "QUIT\r\n");
$result = fgets($fp, 1024);

Wer sich diesen Code nicht zutraut, kann auch die SMTP Klasse von Manuel Lemos verwenden. Sie verbirgt das manuelle Senden der Daten über den Socket gegenüber dem Programmierer und erzeugt auch Fehlermeldungen, wenn mal etwas nicht klappte. Beispiel:


<?php

  /* Funktionsklasse inkludieren. */
  include "smtp.php";

  $smtp = new smtp_class;
  $smtp->host_name=getenv("HOSTNAME");
  $smtp->localhost="localhost";
  $from = "name@".$smtp->host_name;
  $to = "da@irgendwo.de";

  $inhalt = "Inhalt der Mail";

  if ($smtp->SendMessage(
      $from,
      array($to),
      array("From: $from","To: $to","Subject: test"),
      $inhalt))
  {
     print "Mail wurde erfolgreich versandt.";
  }

?>

12.5 Windows: Wo finde ich Mailserver, die ich bei mir installieren kann?

Antwort von Kristian Köhntopp

Wenn man in Windows keinen eigenen Mailserver hat, den man zum Mailversand nutzen kann, muß man sich einen installieren. Die folgenden Produkte wurden in der Newsgroups zu diesem Zweck genannt:

12.6 Wie kann ich eine HTML-Mail versenden? Wie kann ich den Absender meiner Mail festlegen?

Antwort von Johannes Frömter

Die Mailfunktion in PHP hat einen optionalen vierten Parameter, mit dem man zusätzliche Headerzeilen definieren kann. Diese Headerzeilen können den MIME-Type einer Mail bestimmen, den Absender der Mail festlegen oder auch beliebige Nicht-Standard-Header (X-Mailer: etc.) enthalten.


$message = "<h1>Hello world!<h1>";
$to      = "empfaenger@system.de";
$subject = "Betrefftext";
$xtra    = "From: ab@sender.de (Ab Sender)\n";
$xtra   .= "Content-Type: text/html\nContent-Transfer-Encoding: 8bit\n";
$xtra   .= "X-Mailer: PHP ". phpversion();

mail($to,
     $subject,
     $message,
     $xtra);

Durch die Absender-Angabe From: im vierten Parameter von mail() wird der Envelope-From: (die Absenderangabe im "Briefumschlag" der Mail) nicht gesetzt. Nicht zustellbare Mails gehen daher an den Serveradministrator, und nicht an den tatsächlichen Absender zurück.

Seit PHP Version 4.0.5 kennt mail() einen fünften Parameter, dessen Inhalt direkt an das Mailprogramm weitergereicht wird. Im Falle von sendmail übergibt man den Absender für das Envelope-From: folgendermaßen:


mail("empfaenger@system.de",
     "Betrefftext",
     "Hello world!",
     "From: ab@sender.de (Ab Sender)",
     "-f ab@sender.de");

Normalerweise fügt sendmail dann auch ein X-Authentication-Warning in den Header ein; um diese Warnung zu unterdrücken, sollte man den User, unter dessen Account der Webserver läuft, zu den "trusted users" in der sendmail-Konfiguration hinzufügen.

12.7 Wie kann ich ein Attachment mit einer Mail versenden?

Antwort von Kristian Köhntopp

Bei der mail()-Funktion von PHP kann man im vierten Argument jeden beliebigen zusätzlichen Header angeben. Attachments werden MIME-kodiert. Eine frei verfügbare Mail-Klasse kapselt diese Funktionalität und macht die ganze Geschichte recht einfach.

Wer die Datei, die attached werden soll, per Eingabeformular auswählen will, sollte folgenden Weg nehmen:

Wo vorher


$attachment = fread(fopen("test.jpg", "r"), filesize("test.jpg")); 
[...] 
$mail->add_attachment("$attachment", "test.jpg", "image/jpeg");

verwendet wurde, muß nach einem Dateiupload


$attachment = fread(fopen($userfile, "r"), $userfile_size); 
[...] 
$mail->add_attachment($attachment, $userfile_name, $userfile_type);

verwendet werden. Dabei ist $userfile gegen den Namen von <input type="file"> auszutauschen.

12.8 Wie kann ich eine Mail effizient an sehr viele Empfänger versenden?

Antwort von Kristian Köhntopp

Am günstigsten und sichersten versendet man Mail an viele Empfänger, indem man eine spezialisierte Software dafür verwendet. Empfehlenswert sind Mailinglisten-Server wie majordomo oder ezmlm.

Alternativ kann man sich mit einer deutlich primitiveren Lösung in PHP behelfen, indem man gemäß den Beispielen oben zusätzliche Headerzeilen mit Bcc-Empfängern erzeugt. Auf diese Weise generiert man eine einzelne Mail an viele Empfänger, die vom Mailer sehr effizient verteilt werden kann. Gleichzeitig vermeidet man durch die Verwendung von blind carbon copy (BCC)-Empfängern, daß die Empfänger im Kopf der Mail mit aufgeführt werden und auf diese Weise ein Monsterheader entsteht.


  # Empfaengerliste
  $empfaenger = array("a@system.de", "b@system.de");

  # Bcc generieren
  reset($empfaenger);
  while(list($k, $v) = each($empfaenger)) {
    $bcc .= "Bcc: $v\n";
  }

  mail("em@pfaeng.er",
     "Testmail",
     "Dies ist nur eine Testnachricht.",
     $bcc);

12.9 Wie kann ich die Gültigkeit einer Mailadresse testen?

Antwort von Kristian Köhntopp

Der Mailer eines Systems kann die Mail dann zustellen, wenn das Domain Name System (DNS) für die Zieladresse einen Mail Exchanger (MX) Ressource Record (RR) oder einen Address (A) Ressource Record enthält. Wenn man testen möchte, ob die Empfängeradresse für eine Mail gültig ist, braucht man Zugriff auf das Internet und einen DNS-Server, den man befragen kann. Dann kann man die Anfrage, die der Mailer später einmal stellen wird, um die Mail zuzustellen, manuell mit Hilfe der Funktion checkdnsrr() nachvollziehen. Die Funktion liefert true, wenn ein passender RR vorhanden ist.

Eine DNS-Anfrage kann je nach Verfügbarkeit des DNS-Systems bis zu mehreren Minuten dauern. Der betreffende Webserverprozeß ist in diesem Zeitraum blockiert. Das Vorhandensein der benötigten RRs garantiert natürlich nicht, daß das Zielsystem auch mit uns redet oder daß der gewünschte Benutzer existiert und Mail empfangen kann. Die einzige Methode, zuverlässig zu testen, ob eine Mail zustellbar ist, ist sie zuzustellen.


  $addr = "user@host.doma.in";
  list($user, $host) = explode("@", $addr);
  if (checkdnsrr($host, "MX") or checkdnsrr($host, "A")) {
    print "Mail ist vielleicht zustellbar.<BR>\n";
  } else {
    print "Mail ist sicher nicht zustellbar.<BR>\n";
  }

Antwort von Guido Haeger

Im SIMPLE MAIL TRANSFER PROTOCOL (SMTP - RFC 0821 ) ist das Kommando VERIFY (VRFY) definiert. Hat man die Mailadresse "demouser@domain.tld" dann kann man theoretisch mittels "VERIFY demouser" beim für "domain.tld" zuständigen SMTP-Server anfragen, ob ein Postfach für "demouser" existiert. In der Praxis ist dieses Kommando aber bei fast allen SMTP-Servern deaktiviert, bzw. überhaupt nicht implementiert (Spam-/Datenschutz), so daß VERIFY keine praktikable Lösung ist.

12.10 Wie kann ich überprüfen, ob eine versendete Mail tatsächlich angekommen ist?

Antwort von Kristian Köhntopp

Manche Mailer unterstützen Delivery Status Notification (DSN) nach RFC 1894. Dies ist ein RFC auf der Internet Standards Track im Status proposed standard, er wird also in veränderter Form einmal Draft Standard und dann Internet Standard werden. Relevant im selben Zusammenhang ist die ganze Reihe der RFCs in diesem Bereich:

Die Anforderung von DSNs erfolgt mit Hilfe der in RFC 1891 beschriebenen SMTP Service Extension, ist also Bestandteil des SMTP Dialoges.

Auf Unix-Systemen wird der SMTP-Dialog durch das lokal installierte sendmail-Programm abgewickelt. Dieses versteht bestimmte Optionen (-N und -R, die in der Manualpage von sendmail beschrieben sind), mit deren Hilfe DSNs angefordert werden können. Die Option -N <reportklasse> legt fest, für welche Fälle DSNs erzeugt werden sollen. Es können mehrere Reportklassen durch Komma getrennt spezifiziert werden: failure, wenn eine Benachrichtigung bei Zustellproblemen erzeugt werden soll, delay für eine Benachrichtigung bei Zustellverzögerungen und success, für Nachricht, wenn die Zustellung erfolgreich war. Mit der Zustellbenachrichtigung wird zugleich ein Teil der Originalnachricht zurück übermittelt, damit der Absender feststellen kann, auf welche Nachricht sich die Zustellbenachrichtigung bezieht. Die Option -R <return> legt fest, wieviel von der Originalnachricht zurück übermittelt werden soll: hdrs übermittelt nur die Headerzeilen der Originalnachricht zurück, während full die komplette Originalnachricht in der Zustellbenachrichtigung zurückgibt.

Auf Unix-Systemen kann man also DSNs anfordern, wenn man einen ausreichend neuen Sendmail (8.8.x oder besser) installiert hat und man die Konfigurationsvariable sendmail_path passend setzt:


sendmail_path = /usr/lib/sendmail -N failure,success -R hdrs -t

Auf Windows-Systemen wickelt PHP den SMTP-Dialog mit einem entfernten SMTP-Mailer ab. Es sind keine Eingriffsmöglichkeiten vorgesehen, mit deren Hilfe man den SMTP-Dialog erweitern kann. Entscheidet man sich, den SMTP-Dialog manuell abzuwickeln, damit man die SMTP-Erweiterungen gemäß RFC 1891 implementieren kann, ist unbedingt darauf zu achten, daß man das Vorhandensein dieser Erweiterungen im EHLO-Kommando des SMTP-Dialoges richtig abfragt, sonst schlägt die Mailzustellung fehl, wenn man auf einem Mailer einliefert, der kein DSN kann.

Das Resultat von aktivierten DSNs sind Nachrichten mit dem Mime-Typ multipart/report wie in RFC 1892 und RFC 1894 spezifiziert. Diese Nachrichten enthalten mindestens zwei, meist jedoch drei Teile, von denen einer message/delivery-status ist und eine maschinenlesbare DSN nach RFC 1894 enthält. Mit Hilfe eines POP3 oder IMAP-Clients kann man diese Nachrichten einsammeln und analysieren.

12.11 Wie kann ich feststellen, ob eine Mailadresse äußerlich gültig ist?

Antwort von Kristian Köhntopp

Durch einen geeignet gewählten regulären Ausdruck. Ein einfacher solcher Ausdruck hätte zum Beispiel die Form


if (!eregi("^[_\.0-9a-z-]+@([0-9a-z][0-9a-z-]+\.)+[a-z]{2,3}$",$email)):
  # Mailadresse sieht seltsam aus
endif;

Dieser Ausdruck ist jedoch gleichzeitig zu locker und zu restriktiv gewählt: Einige Mailsysteme mögen zum Beispiel weitere Zeichen im Empfängernamen zulassen oder ablehnen und möglicherweise gibt es Topleveldomains mit einer anderen Buchstabenanzahl als 2 oder 3. Einige Absender haben vielleicht auch eine exotischere Mailadresse der Form bla!user@host.dom oder bla%host1.dom@host2.dom. Alle diese legalen Adressen werden von dem o.a. Ausdruck abgelehnt.

12.12 Wie versende ich SMS mit PHP?

Antwort von Martin Jansen

Generell muss man bei der Verwendung von SMS zwei Arten unterscheiden:

Auf der einen Seite gibt es die Möglichkeit, eine SMS an eine vorher definierte Rufnummer (z.B. die des eigenen Handys) zu senden.

Die zweite Möglichkeit, die sehr häufig von Portalseiten (z.B. Lycos) genutzt wird, ist, dem Besucher der Seite die Möglichkeit zu geben, an eine Rufnummer seiner Wahl eine SMS zu senden. Diese SMS enthalten allerdings in der Regel einen kurzen Werbetext.

Variante 1 lässt sich mit der mail()-Funktion von PHP realisieren: Jeder Netzanbieter stellt jedem seiner Kunden auf Nachfrage eine eigene E-Mail-Adresse (z.B. <rufnummer>@smsmail.eplus.de) zur Verfügung. Nachrichten an diese E-Mail-Adresse werden als SMS an den Mobilfunkaccount des Kunden weitergeleitet. Dieser Service kostet je nach Anbieter zwischen 0,29 DM und 0,39 DM pro SMS. So würde zum Beispiel folgendes Skript eine SMS an die Nummer 0177-1234567 senden:


<?php
$subject = "Hello World";
$text = "Just another PHP hacker";
mail("491771234567@smsmail.eplus.de",$subject,$text,"From: sms-service@send.er");
?>

Mit dieser Variante lässt sich auf einfache Weise der Versand von SMS an eine vorher definierte Nummer (i.d.R. die eigene) realisieren. Ein Risiko, das man nicht unterschätzen sollte, ist die Gefahr des Missbrauchs: Ein böswilliger User sendet zum Beispiel 100 SMS in Folge und verursacht somit beim Empfänger auf einen Schlag Kosten von etwa 30-40 DM. Hier müssen entsprechende Sicherheitsmechanismen wie zum Beispiel ein Limit an SMS pro IP oder Cookies ansetzen, die aber alle keine 100%-ige Sicherheit garantieren.

Die zweite Möglichkeit verwendet eine vollwertige SMS-Gateway, um den Versand von SMS an beliebige Nummern zu realiseren.

Die SMS-Gateway ist entweder lokal auf dem eigenen Server installiert oder man verwendet einen Fremdanbieter, der eine SMS-Gateway zu entsprechenden Konditionen zur Verfügung stellt.

Nachrichten, die über die SMS-Gateway versendet werden sollen, werden nach dem gleichen Prinzip wie Variante 1 mit der PHP-Funktion mail() versendet. Der Unterschied besteht darin, dass neben dem Text der SMS auch entsprechende Steuersignale im E-Mail-Text enthalten sind, die für die SMS-Gateway die entsprechenden Informationen liefern, wohin die SMS gesendet werden soll. Bei der Verwendung eines Fremdanbieters sind des weiteren die nötigen Authentifizierungsdaten in der E-Mail enthalten, die den SMS-Versand ermöglichen.

Selbstverständlich birgt auch diese Variante die Möglichkeit des Missbrauchs, weshalb auch hier die gleichen Sicherheitsmechanismen wie bei Variante 1 angewendet werden müssen. Die meisten der grossen Anbieter von kostenlosem SMS-Versand reduzieren so zum Beispiel die Anzahl Nachrichten, die pro IP gesendet werden dürfen, auf eine gewisse Anzahl pro Tag.


13. Datenbanken

13.1 Wie kann ich mehr über SQL lernen?

Antwort von Kristian Köhntopp

Bei Markt und Technik gibt es den vollständigen Text von SQL in 21 Tagen online zu lesen. Man kann das Buch auch kaufen.

Guido Stepken hat sein MySQL Datenbankhandbuch auf seiner Website zum Online-Lesen oder zum Download bereitgestellt.

Das Buch "PostgreSQL", welches bei Addison-Wesley erschienen ist, kann auch online gelesen werden.

Ein englischsprachiges Einsteiger-Tutorial findet sich unter www.sqlcourse.com (Teil 1, behandelte Themen: von "Table basics" über "Inserting/Updating/Deleting" bis "Advanced Queries"). Teil 2 nimmt sich folgende Themen vor: "Aggregate Functions", "GROUP BY/HAVING/ORDER BY", "Table Joins" etc. Nettes Special: ein Online-SQL-Interpreter.

Für MySQL-spezifische Fragen gibt es eine eigene deutschsprachige Newsgroup: de.comp.datenbanken.mysql (FAQ dieser NG)

13.2 Wieso kann ich mehrere, durch Semikolon getrennte Statements nicht ausführen?

Antwort von Kristian Köhntopp

SQL kennt keine Mehrfachstatements. Einige SQL-Frontends (der MySQL-Kommandoprozessor, phpMyAdmin) kennen Mehrfachstatements, die sie manuell in einzelne Anweisungen zerlegen und nacheinander an den Datenbankserver senden. PHP selbst macht dies nicht. Man muß seine Statements manuell zerlegen und einzeln nacheinander absenden.

Um ein Statement zu zerlegen, ist es nicht ausreichend, auf dieses Statement einfach explode() anzuwenden. Beispiel:


INSERT INTO table VALUES('foo;bar');

Wie es richtig geht, kann man im Code von phpMyAdmin nachlesen. Die relevante Stelle ist die Funktion split_sql() in der Datei db_readdump.php.

13.3 Ist es sinnvoll, Bilder in einer Datenbank abzulegen?

Antwort von Kristian Köhntopp

Aus irgendeinem Grund scheinen viele Leute zu glauben, daß es Bilddaten adeln würde, wenn man sie in eine Datenbank stopft.

Wenn man die Bilddaten selbst in der Datenbank ablegt, hat dies den Vorteil, daß keine broken links auftreten können, weil ja die Bilder selbst genauso wie die Links auf die Bilder aus der Datenbank erzeugt werden. Liegen die Bilddaten dagegen im Dateisystem und die Datenbank enthält nur Pfadnamen, dann ist es problemlos möglich, daß jemand die Dateien umbenennt, ohne diese Änderung in der Datenbank nachzuführen und umgekehrt. Leider ist es speziell bei MySQL so, daß keinerlei Mechanismen vorhanden sind, die die referentielle Integrität der Datenbank sicherstellen, sodaß diese Sicherheit nicht wirklich gegeben ist.

Dazu kommen noch eine Reihe von weiteren Nachteilen:

Wie man Bilder in einer Datenbank speichert, wird im Kapitel Wie kann ich Bilder in einer MySQL-Datenbank speichern? beschrieben.

13.4 Windows: Jeder Zugriff auf meine Datenbank dauert eine halbe Minute!

Antwort von Kristian Köhntopp

Eine Komponente Deines Netzes versucht, aus IP-Nummern per gethostbyaddr() auf den Hostnamen zu schließen und findet keinen Domain Name Server. Die Verarbeitung des Requests wird erst nach dem DNS-Timeout fortgesetzt.

Sorge dafür, daß ein DNS-Server mit korrektem Reverse Lookup verfügbar ist oder sorge dafür, daß keine Reverse Lookups gemacht werden. Dazu mußt Du zunächst einmal die Komponente identifizieren, die die Lookups macht.

13.5 Wie kann ich meine Datenbankperformance steigern?

Antwort von Kristian Köhntopp

Bei jeder Art von Performancetuning ist das wichtigste zunächst einmal eine Messung. Es kommt ganz entscheidend darauf an, als erstes festzustellen, was denn genau langsam ist, bevor man sich daran macht, die Dinge zu verändern. Wenn ein Script mit Datenbankzugriff zu langsam ist, dann kann dies an einer von mehreren Ursachen liegen.

Die Datenbank steht offsite oder ist nur langsam erreichbar.

Wenn die Datenbank nicht auf derselben Maschine läuft wie der Webserver, dann findet die Kommunikation zwischen Datenbank-Client und Server nicht mehr über schnelle Kommunikationsmethoden wie shared memory oder UNIX Domain Sockets statt, sondern über eine TCP/IP-Verbindung, die eine wesentlich geringere Kapazität und wesentlich höhere Latenzzeiten hat. Dies hat besonders fatale Auswirkungen, wenn die Datenbank und der Webserver durch eine langsames Netzwerk getrennt sind (Umlaufzeiten für Pakete von 10ms und mehr) oder wenn die Netzwerkbandbreite eingeschränkt ist (8 KB/sec und weniger).

Hier kommt es ganz entscheidend darauf an, die Anzahl der Anfragen pro Seitenaufbau zu vermindern und die Menge der übertragenen Daten zu verringern. Die Anzahl der Abfragen läßt sich dadurch vermindern, daß man SQL JOIN-Operationen statt vieler Abfragen verwendet. Ein typisches, falsches Konstrukt ist


# Liste der Treffer bestimmen
$result=do_database_query("select id from tabelle where $bedingung");

# Treffer anzeigen
reset($result);
while(list($k, $v) = each($result)):
  $detail = do_detail_query("select * from tabelle2 where id =$v");
  show_detail($detail);
endwhile;

Dieser Code generiert eine Masse von Queries nacheinander. Für jede einzelne Query wird ein Umlauf zur Datenbank und zurück notwendig und so summieren sich diese Umlaufzeiten zu gigiantischen Wartezeiten beim Seitenaufbau. Viel geschickter ist stattdessen


# Treffer bestimmen
$detail = do_database_query("select * from tabelle, tabelle2
 where ( $bedingung ) and tabelle.id = tabelle2.id");
reset($detail);
while(list($k, $v) = each($detail)):
  show_detail($v);
endwhile;

Dies liefert die gewünschten Daten mit einer einzigen Query.

Die Datenbank hat hohe Verbindungsaufbaukosten und es wird CGI PHP verwendet.

Einigen Datenbanken wie MySQL macht es nicht aus, Datenbanklinks zu öffnen und wieder zu schließen. Andere Datenbanken wie Oracle starten bei jedem Connect einen eigenen Clientprozeß. Dies ist ein sehr aufwendiger Vorgang. Wenn CGI PHP verwendet wird, dann ende der CGI Interpreter am Ende einer jeden Seite und mit dem Interpreter werden auch alle geöffneten Dateihandles und damit auch alle Datenbankverbindungen geschlossen - der Clientprozeß der Datenbank endet und muß für eine neue Seite neu geladen und gestartet werden.

In solchen Fällen ist die Verwendung eines PHP-Interpreters als Modul angezeigter, weil in dieser Konfiguration die mit pconnect() geöffneten Links über die Lebensdauer einer PHP-Seite hinaus gehalten und auf neuen Seiten wiederverwendet werden können.

Die Queries in der Datenbank sind nicht effizient.

Alle Datenbanken haben Werkzeuge zur Analyse von Anfragen. In MySQL ist dies das EXPLAIN Kommando, in Oracle ist es EXPLAIN PLAN. Die Ausgabe dieser Kommandos sollte man in jedem Fall verstehen lernen und zu Rate ziehen. Nur so kann man erkennen, ob Indices zur Beschleunigung der Query verwendet werden, ob die Typen von Key und Foreign Key zueinander kompatibel sind und ob die Datenbank die richtige Tabelle als treibende Tabelle in einem JOIN verwendet.

13.6 Wie kann ich zwei Tabellen miteinander verknüpfen?

Antwort von Kristian Köhntopp

Man kann dies mit Hilfe einer JOIN-Operation tun. Diese ist im Kapitel 7.20 des MySQL-Handbuches beschrieben.

Wenn die Tabellen artikel und email die Primärschlüsselfelder artikel.KundenID und email.eid haben und artikel mit email über den Fremdschlüssel email.KundenID verknüpft ist, dann kann man einen Equi-JOIN mit dem folgenden Statement formulieren:


mysql> select * from artikel;
+----------+
| KundenID |
+----------+
|        1 |
|        2 |
|        3 |
+----------+
3 rows in set (0.00 sec)

mysql> select * from email;
+-----+----------+
| eid | KundenID |
+-----+----------+
|   1 |        1 |
|   2 |        2 |
|   3 |        3 |
+-----+----------+
3 rows in set (0.00 sec)
mysql> select a.KundenID as aid, 
     >        e.eid as eid, 
     >        e.KundenID as e_aid 
     >   from artikel as a, 
     >     email as e 
     > where a.KundenID = e.KundenID;
+-----+-----+-------+
| aid | eid | e_aid |
+-----+-----+-------+
|   1 |   1 |     1 |
|   2 |   2 |     2 |
|   3 |   3 |     3 |
+-----+-----+-------+
3 rows in set (0.01 sec)

In keinem Fall können in den herangejointen Tabellen Nullwerte enthalten sein.

Diese Operation ist dann effizient, wenn a.KundenID und t.KundenID denselben Typ haben, und auf auf a.KundenID und t.KundenID ein UNIQUE INDEX oder ein INDEX liegen. In MySQL ist ein PRIMARY KEY immer auch ein UNIQUE INDEX.

Wenn man optionale Werte hat, dann kann man keinen symmetrischen Join (Equijoin) mehr machen, sondern muß einen asymmetrischen Join (Left Join) durchführen. Dadurch können auf der rechten Seite Nullwerte entstehen:


mysql> select * from telefon;
+-----+----------+
| tid | KundenID |
+-----+----------+
|   1 |        1 |
|   2 |        3 |
+-----+----------+
2 rows in set (0.00 sec)


Equijoin (es fehlt KundenID 2, weil keine 
Telefonnummer definiert ist):

mysql> select a.KundenID as aid, 
     >        e.eid as eid, 
     >        e.KundenID as e_aid, 
     >        t.tid as tid, 
     >        t.KundenID as t_aid 
     >   from artikel as a, 
     >        email as e, 
     >        telefon as t 
     > where a.KundenID = e.KundenID 
     > and a.KundenID = t.KundenID;
+-----+-----+-------+-----+-------+
| aid | eid | e_aid | tid | t_aid |
+-----+-----+-------+-----+-------+
|   1 |   1 |     1 |   1 |     1 |
|   3 |   3 |     3 |   2 |     3 |
+-----+-----+-------+-----+-------+
2 rows in set (0.02 sec)


Left Join (generiert Nullwerte):

mysql> select a.KundenID as aid, 
     >        t.tid as tid, 
     >        t.KundenID as t_aid 
     >   from artikel as a left join telefon as t 
     >        on a.KundenID = t.KundenID;
+-----+------+-------+
| aid | tid  | t_aid |
+-----+------+-------+
|   1 |    1 |     1 |
|   2 | NULL |  NULL |
|   3 |    2 |     3 |
+-----+------+-------+
3 rows in set (0.00 sec)


Unterschiedliche Counts:

mysql> select count(a.KundenID) as acount, 
     >        count(t.KundenID) as tcount 
     >   from artikel as a left join telefon as t 
     >        on a.KundenID = t.KundenID;
+--------+--------+
| acount | tcount |
+--------+--------+
|      3 |      2 |
+--------+--------+
1 row in set (0.01 sec)

Die Tabelle a ist hier die aufspannende Tabelle, die Tabelle t ist die aufgespannte Tabelle. An den Stellen, an denen t keine zu a passenden Werte hat, tauchen Nullwerte in t auf. Da die Relation nun nicht mehr symmetrisch ist, muss man zwischen a.KundenID und t.KundenID unterscheiden. Insbesondere sind die count()-Werte beider Spalten unterschiedlich.

Da a.KundenID und t.KundenID unterschiedlich sind, muß man auch zwingend mit qualifizierten Namen arbeiten und kann nicht mehr einfach KundenID schreiben.

Ein gemischter Join verwendet Equijoins und Left Joins, wie es gerade paßt:


mysql> select a.KundenID as aid, 
     >        e.eid as eid, e.KundenID as e_aid, 
     >        t.tid as tid, t.KundenID as t_aid 
     >   from artikel as a, 
     >        email as e left join telefon as t 
     >        on a.KundenID = t.KundenID 
     >  where a.KundenID = e.KundenID;
+-----+-----+-------+------+-------+
| aid | eid | e_aid | tid  | t_aid |
+-----+-----+-------+------+-------+
|   1 |   1 |     1 |    1 |     1 |
|   2 |   2 |     2 | NULL |  NULL |
|   3 |   3 |     3 |    2 |     3 |
+-----+-----+-------+------+-------+
3 rows in set (0.01 sec)

13.7 Was ist Aggregation? Was ist GROUP BY?

Antwort von Kristian Köhntopp

Mit Hilfe der GROUP BY-Clause kann man in SQL Daten aggregieren, also Äquivalenzklassen über den gefundenen Elementen bilden und mit den so gefundenen Teilmengen arbeiten.

Gegeben sei eine Menge von Tupeln, etwa (1, 2), (1, 3), (2, 3), (2, 2), (3, 17), (2, 21). Man kann diese Menge jetzt in Teilmengen unterteilen, das wäre dann in der Mathematik eine Relation. Die Elemente, die gemeinsam in einer Teilmenge stehen, stehen dann in einer Relation zueinander.

Eine Relation ist zum Beispiel kleiner als x. Nimmt man zum Beispiel die Menge |N und die Relation kleiner als 10, dann teilt diese Relation die Menge |N in zwei Teilmengen, nämlich die Menge der natürlichen Zahlen, die die Relation erfüllen (also die Zahlen 1, 2, 3, ..., 9) und die Menge der natürlichen Zahlen, die die Relation nicht erfüllen (die Zahlen 10, ...).

Ebenso kann man eine Äquivalenzrelation definieren. Eine solche Relation definiert mehrere Teilmengen und die Elemente einer Teilmenge sind gleich. In |N mit == als Relation ist das witzlos, da die Teilmengen dann einelementig sind, aber mit den o.a. Tupeln kann man ein sinnvolles Beispiel definieren, wenn man als Äquivalenzrelation Gleichheit des ersten Elementes definiert. Man bekommt dann die folgenden Teilmengen:


Die Menge 1 == { (1, 2), (1, 3) }
Die Menge 2 == { (2, 3), (2, 2), (2, 21) }
Die Menge 3 == { (3, 17) }

Angenommen, die Tupel seien eine Tabelle


CREATE TABLE beispiel (
  x integer,
  y integer
);

dann würde man die o.a. Tupel als


INSERT INTO beispiel (x, y) values (1, 2);
INSERT INTO beispiel (x, y) values (1, 3);
INSERT INTO beispiel (x, y) values (2, 3);
INSERT INTO beispiel (x, y) values (2, 2);
INSERT INTO beispiel (x, y) values (2, 21);
INSERT INTO beispiel (x, y) values (3, 17);

definieren und bekäme die Äquivalenzrelation aus dem Beispiel als


SELECT x AS mengenname FROM beispiel GROUP BY x;
+------------+
| mengenname |
+------------+
|          1 |
|          2 |
|          3 |
+------------+
3 rows in set (0.01 sec)

d.h. die Tupel (x, y) mit gleichem x bilden jeweils eine Menge. Wir sehen uns von diesen Tupeln jeweils nur die x an.

Die Mächtigkeit der Mengen 1, 2 und 3 kann man mittels count() bestimmen:


SELECT x AS mengenname, COUNT(x) AS maechtigkeit
  FROM beispiel
  GROUP BY x;
+------------+--------------+
| mengenname | maechtigkeit |
+------------+--------------+
|          1 |            2 |
|          2 |            3 |
|          3 |            1 |
+------------+--------------+
3 rows in set (0.00 sec)

Man kann sich auch das Tupel (x, y) wieder ausgeben lassen:


SELECT x, y FROM beispiel GROUP BY x;
+------+------+
| x    | y    |
+------+------+
|    1 |    2 |
|    2 |    3 |
|    3 |   17 |
+------+------+
3 rows in set (0.00 sec)

MySQL wählt hier irgendein y, da ja per Definition alle (x, y) innerhalb einer Teilmenge gleich sind und jedes Element der Teilmenge daher als Repräsentant der Teilmenge gewählt werden kann.

13.8 Was ist der Unterschied zwischen connect und pconnect?

Antwort von Kristian Köhntopp

In PHP bieten die meisten Datenbanken zweiconnect()-Funktionen an: Eine gewöhnliche und eine pconnect()-Funktion. Verwendet man CGI PHP, unterscheiden sich beide Funktionen nicht.

Verwendet man das PHP-Modul, werden die mit einem connect() hergestellten Datenbankverbindungen am Ende der Seite geschlossen. Mit pconnect() hergestellte Verbindungen bleiben jedoch geöffnet. Dies dient einzig und alleine dazu, das ständige Öffnen und Schließen von Netzwerkverbindungen zu vermeiden, denn der Verbindungsaufbau ist bei einigen Datenbanken (etwa Oracle) sehr aufwendig.

Es ist daher empfehlenswert, in jedem Fall die pconnect()-Variante zu verwenden (aber: Vergleiche Webserver verstehen und tunen. Es können sehr viele offene Datenbankverbindungen entstehen).

13.9 Wie kann ich mein Datenbankpaßwort gegen Spionage sichern?

Antwort von Kristian Köhntopp

Viele PHP-Scripte enthalten Connectinformationen mit Paßworten und anderen wichtigen Daten, die nicht in falsche Hände fallen dürfen. Um diese Scripte gegen Zugriff von außen (über den Webserver) zu sichern, gibt es folgende Maßnahmen:

Die connect()-Funktionen in PHP verlangen alle, daß das Datenbankpaßwort im Klartext angegeben wird. Das bedeutet, daß das Paßwort entweder im PHP-Code im Klartext angegeben ist oder vom Code in Klartext entschlüsselt werden kann. Wenn jemand die Dateien mit den Paßworten oder dem Entschlüsselungscode lesen kann, dann bedeutet dies auch, daß das Klartextpaßwort dieser Person bekannt wird. Die betreffende Person braucht den Entschlüsselungscode nicht zu verstehen - sie braucht ihn nur auszuführen und er wird zwangsläufig das Klartextpaßwort passend für die Connect-Funktion liefern müssen.

Daraus folgt, daß Schutz der Datenbankpaßworte nur durch Schutz der entsprechenden Quelltextdateien möglich ist. Es ist Aufgabe des Hostingenvironments beim Provider, diesen Schutz zu bieten, indem entweder Zugriffsrechte an den Dateien entsprechend gesetzt sind oder indem sogar eine virtuelle Dateiumgebung mit chroot() eingerichtet wird.

13.10 MySQL oder PostgreSQL?

Antwort von Kristian Köhntopp

von Kristian Köhntopp, Lutz Donnerhacke und Sebastian Bergmann.

MySQL ist eine sehr populäre Datenbank, die sich vor allen Dingen durch Geschwindigkeit und geringen Speicherverbrauch sowie durch einfache Handhabung auszeichnet. MySQL verfügt über eine sehr gute Dokumentation, ist auch für die Windows-Plattform verfügbar und seit Ende Juni 2000 unter der GPL frei verfügbar. Seit Version 3.23.16 gibt es experimentellen Support für Transaktionen auf der Basis der Sleepycat DB3-Bibliothek, aber noch keine Trigger oder Rules.

Das Buch MySQL von Paul DuBois (englische Version) erläutert die Datenbank umfassend und enthält eine Reihe von allgemeinen und speziell auf MySQL bezogenen Optimierungstips. Mit phpMyAdmin von Tobias Ratschiller existiert eine einfach zu bedienende, in PHP geschriebene Administrationsoberfläche für MySQL.

PostgreSQL ist der großteils geglückte Versuch, eine freie Implementation von SQL92 aus einem SQL-fremden Konzept (Ingres) abzuleiten. Dazu gehören Transaktionen in verschiedenen Abschottungsgraden, Subselects, eigene Datentypen, Operatoren und Aggregatfunktionen, Trigger, Rules ('Trigger', die in die Optimierungsplanung eingehen) und Views. Es ist somit möglich, daß die Datenbank die Konsistenz des Datenbestandes aus sich heraus erzwingt und so Direktzugriffe ohne korrigierende Frontends gestattet. Die Geschwindigkeit von PostgreSQL ist dadurch allerdings vermindert. Verzichtet man auf Datanbankkonsistenz auf der Platte im Falle von OS-Abstürzen, so wird PostgreSQL deutlich schneller. Ebenso wie MySQL fehlen auch PostgreSQL noch einige elementare Funktionen zur vollen SQL92-Kompatibilität, im Falle von PostgreSQL sind dies zum Beispiel Outer Joins. Die Möglichkeiten von PostgreSQL machen es notwendig, die Datenbank vorab sorgfältig zu planen.

Inzwischen existiert mit phpPgAdmin eine von Dan Wilson nach PostgreSQL portierte Version von phpMyAdmin.

13.11 Wie komme ich bei meinem Provider an die Datenbank?

Antwort von Kristian Köhntopp

Solche Fragen klärt man am Besten mit dem Telefonsupport des betreffenden Providers.

13.12 Wie kann ich auf einen ODBC-Server (MSSQL, Access) zugreifen?

Antwort von Kristian Köhntopp

In Windows kann man einfach den mitgelieferten ODBC-Treiber verwenden. Eine Beschreibung in englischer Sprache befindet sich im englischen PHP-Manual.

In Unix kann man für den Zugriff auf einen Microsoft SQL Server den Sybase-CT Treiber verwenden, der ein weitgehend kompatibles Protokoll verwendet. Sybase bietet eine frei verfügbare Version der benötigten Bibliotheken für Linux zum download an.

Alternativ kann man auch einen kommerziellen ODBC-Treiber für Unix verwenden, etwa den Treiber von OpenLink Software oder den iODBC-Treiber, der dem Adabas-Paket für Suse Linux beiliegt.


14. Datenbanken: MySQL

14.1 Kommt MySQL mit mehr als x Datensätzen pro Tabelle klar? Wie stabil ist MySQL?

Antwort von Kristian Köhntopp

MySQL ist als Datenbank äußerst stabil und auch bei großen Datenmengen extrem schnell und effizient. Die Grenzen von MySQL liegen nicht so sehr in der Größe der Tabellen oder der Anzahl von Datensätzen, sondern in der Komplexität der Datenmodelle, die damit implementiert werden kann.

MySQL speichert Daten mit Index in Baumstrukturen. Auf diese Datenstrukturen kann mit logarithmischer Komplexität zugegriffen werden, d.h. für eine Tabelle mit n Datensätzen sind log(b, n) Zugriffe notwendig, bis der gesuchte Datensatz gefunden ist. b ist die Basis des Logarithmus. Wäre b gleich 2, dann wären zum Zugriff auf eine Tabelle mit 1.000 Datensätzen maximal 10, auf eine Tabelle mit 1.000.000 Datensätzen maximal 20 und auf eine Tabelle mit 1.000.000.000 maximal 30 Zugriffe notwendig, um einen beliebigen Zieldatensatz über den Index zu finden. Tatsächlich ist die Basis jedoch nicht 2, sondern weit größer. Sie ist abhängig von der internen Blockgröße der Datenbank und der mittleren Satzlänge in einem Index. Man kann annehmen, daß sie je nach Art der Daten zwischen 20 und 40 liegt. Damit wären zum Finden eines Datensatzes aus 1.000 Datensätzen maximal 3, aus 1.000.000 Datensätzen maximal 5 und aus 1.000.000.000 Datensätzen maximal 7 Vergleiche und Plattenzugriffe notwendig.

Entsprechend sind die Erfahrungen, die mit MySQL berichtet werden: Im Rahmen der Begrenzungen des Betriebssystems (maximale Dateigröße 2 GB?) kommt MySQL mit beliebig großen Tabellen problemlos klar.

Beschränkungen ergeben sich in MySQL aus dem Fehlen bestimmter Eigenschaften wie Erzwingung referentieller Integrität (keine foreign key Prüfungen, siehe dazu auch die Bemerkungen über PostgreSQL) und Transaktionen. Das Fehlen dieser Eigenschaften macht die Implementierung von Datenbankschemata sehr mühsam, die schreibend auf mehr als eine Tabelle zur Zeit zugreifen.

Man kann abschätzen, ob MySQL für eine Aufgabe das passende Tool ist, indem man sich das geplante Datenbankschema und die geplanten Transaktionen auf diesem Schema ansieht, alle n:m und Sternbeziehungen isoliert und dann feststellt, in welchen dieser Beziehungen schreibende Zugriffe notwendig sind, die mehr als eine Tabelle aktualisieren.

MySQL ist geeignet für alle Modelle, die read-mostly sind oder die weitaus überwiegend Schreibzugriffe auf einzelne Tabellen haben. MySQL ist nicht optimal geeignet, wenn ein Modell sehr viele Schreibzugriffe hat, wenn ein Modell mehr als 2 Schreibzugriffe hat, die mehr als eine Tabelle gleichzeitig aktualisieren oder wenn ein Modell zwingend auf referentielle Integrität angewiesen ist, aber mehr als eine Anwendung schreibend auf den Bestand zugreift.

14.2 Wie greife ich auf eine MySQL-Datenbank zu?

Antwort von Kristian Köhntopp

Mit Hilfe der Funktion mysql_connect() kann man eine Datenbankverbindung zu einem MySQL-Server aufbauen, um dann mit mysql_select_db() die Datenbank auf dem Server auszuwählen. Das Resultat ist eine Link-ID, die man bei allen anderen MySQL-Funktionen angeben kann.

In der php3.ini kann man festlegen, welche Datenbank und welcher Benutzername und welches Paßwort die Funktion verwenden soll, wenn diese Angaben beim Funktionsaufruf weggelassen werden, auch wenn es nur untermittelschlau ist, dies zu tun:


; default host for mysql_connect() (doesn't apply in safe mode)
mysql.default_host  = 
; default user for mysql_connect() (doesn't apply in safe mode)
mysql.default_user  =
; default password for mysql_connect() (doesn't apply in safe mode)
mysql.default_password  =  

; Note that this is generally a *bad* idea to store passwords
; in this file. *Any* user with PHP access can run
; 'echo cfg_get_var("mysql.default_password")' and reveal that
; password!  And of course, any users with read access to this
; file will be able to reveal the password as well.

Kontaktaufnahme mit dem Server und Festlegen der Datenbank:


  $link = mysql_connect("localhost","ich","geheim");
  if (!$link)
    die("Kann den Server nicht erreichen.");
  if (!mysql_select_db("meinedaten", $link))
    die("Kann die Datenbank nicht anwählen.");

Das Senden einer Anfrage an die Datenbank erfolgt mit Hilfe der Funktion mysql_query(). Diese Funktion liefert einen Result-Identifier, mit dem dann das Ergebnis abgefragt werden kann: Mit der Funktion mysql_num_rows() bestimmt man die Anzahl der Zeilen des Ergebnissis, mit Hilfe der Funktion mysql_fetch_array() kann man die jeweils aktuelle Zeile des Ergebnisses einlesen.


$query = sprintf("select * from meinetabelle order by id");
$result = mysql_query($query, $link);
if (!$result)
{
  print mysql_error();
     die("Query $query ist ungültiges SQL.");
}

$zeilen = mysql_num_rows($result);
printf("Das Ergebnis hat %d Zeilen.\n", $zeilen);

while($avar = mysql_fetch_array($result)):
  printf("Spalte bla hat den Wert %s\n", $avar["bla"]);

mysql_free_result($result);
mysql_close($link);

14.3 "0 is not a MySQL result index"

Antwort von Kristian Köhntopp

Die Funktion mysql_query() liefert als Ergebnis 0 bzw. false, wenn die gesendete Query ungültig oder syntaktisch falsch ist. Es ist notwendig, diesen Fall abzufangen um die Meldung zu verhindern. Mit den Funktionen mysql_error() und mysql_errno()kann man auf die letzte Fehlermeldung bzw. die letzte Fehlernummer des MySQL-Servers zugreifen, wodurch sich die Fehlerursache meist leicht ermitteln läßt.

14.4 Mein Script verbraucht so viel Speicher beim Datenbankzugriff.

Antwort von Kristian Köhntopp

Wenn das Script so geschrieben ist, daß es in einer Schleife SELECT-Anfragen stellt, ohne mysql_free_result() auf die Result-Sets aufzurufen, dann wird es immer mehr Speicher verbrauchen.


while(irgendwas) {
  $res = mysql_query("SELECT wunderquery");
  machwas_und_generiere_insert_oder_update();

  // mysql_free_result($res);
}

In diesem Fall wird bei jedem Schleifendurchlauf für das SELECT-Ergebnis neuer Speicher bestellt und dieser niemals wieder freigeben. Der PHP-Prozess wird über alle Grenzen wachsen. Entfernt man die beiden Kommentarzeichen in der Schleife, wird dies nicht passieren.

Alternativ kann man PHP4 verwenden. Das dort enthaltene Reference Counting gibt den Speicher automatisch frei.

14.5 Windows: "Call to unsupported or undefined function: mysql_connect()"

Antwort von Kristian Köhntopp

Das offizielle PHP3-Binary für Windows ist modular aufgebaut. Die verschiedenen Module liegen als DLL vor und müssen geladen werden. Dies kann entweder zur Laufzeit mit Hilfe der Funktion dl() geschehen oder durch die passenden Einträge in der php3.ini:


;;;;;;;;;;;;;;;;;;;;;;
; Dynamic Extensions ;
;;;;;;;;;;;;;;;;;;;;;;
; directory in which the loadable extensions (modules) reside
extension_dir   =  c:\php3 

; if you wish to have an extension loaded automatically, use the
; following syntax:  extension=modulename.extension
; for example, on windows,
; extension=msql.dll

;Windows Extensions
extension=php3_mysql.dll

14.6 Unix: "Call to unsupported or undefined function: mysql_connect()"

Antwort von Kristian Köhntopp

Höchstwahrscheinlich haben Sie das MySQL-Modul bei der Installation von PHP3 nicht mitkompiliert. Zuerst muss MySQL installiert werden (der Default-Installationspfad ist /usr/local bei Unix, /usr in Suse Linux), danach erst kann PHP installiert werden. Dabei beantworten Sie im "setup"-Skript folgende Frage mit "y" für Ja:


Whether to build PHP with MySQL support. More info about MySQL
can be found at http://www.tcx.se/. If you answer `yes', the
default directory is `/usr/local'.  
MySQL support? (`yes', `no' or dir) [no] : y

Bei der nächsten Frage geben Sie Ihren MySQL-Installationspfad an oder bestätigen den Defaul-Wert mit Enter:


Enter MySQL install directory [/usr/local]

14.7 "Call to unsupported or undefined function: mysql_errno()"

Antwort von Kristian Köhntopp

Manche älteren Versionen von MySQL kennen die Funktion mysql_errno() nicht. In diesem Fall muß man sich mit mysql_error() behelfen oder - besser - die MySQL-Version aktualisieren und ggf. den PHP-Interpreter mit dem neuen MySQL-Support neu bauen.

14.8 "MySQL-Server has gone away"

Antwort von Kristian Köhntopp

Mögliche Ursachen dafür werden in der MySQL-Dokumentation in Kapitel 18.2.1 diskutiert. Die Fehlermeldung kann durch einen Idle-Timeout auf der Datenbankverbindung (8 Stunden) oder durch zu große Datenpakete beim Arbeiten mit BLOBs und TEXT-Feldern auftreten. In letzteren Fall empfiehlt die MySQL-Dokumentation das Setzen der MySQL-Konfigurationsvariablen max_allowed_packet=# auf einen großen Wert (einige MB).

Ein Benutzer berichtete vom Auftreten dieses Fehlers beim Arbeiten mit UPDATE-Anweisungen in einer breiten Tabelle, die auch bis zu 150 KB große TEXT-Felder enthielt. Abhilfe war hier das Auslagern der TEXT-Felder in eine separate Tabelle. Dies brachte zugleich auch einen großen Performancegewinn.

14.9 Wie kann ich eine CSV-Datei in MySQL importieren?

Antwort von Kristian Köhntopp

In MySQL gibt es die Anweisung LOAD DATA INFILE zum Importieren von Dateien im CSV-Format in die Datenbank. Diese Anweisung wird vom Datenbankserver ausgeführt, d.h. die Datei muß auf dem Rechner liegen, auf dem der Datenbankserver abläuft und die Datei muß word-readable sein. Man benötigt file_priv, um dieses Kommando ausführen zu können.

Seit Version 3.22.6 von MySQL gibt es die Kommandovariante LOAD DATA LOCAL INFILE zum Importieren von Daten im CSV-Format. Dieses Kommando wird auf dem MySQL-Client (also im PHP-Interpreter) ausgeführt. Die Datei muß also auf dem Rechner liegen, auf dem der Client läuft und durch den Client lesbar sein. Man benötigt keine besonderen Privilegien, um dieses Kommando ausführen zu können. Dies ist die empfohlene Variante des Kommandos, falls die zur Verfügung steht.

Das folgende SQL-Kommando liest eine Datei ein, bei der die Datensätze optional mit Anführungszeichen eingeschlossen sind und durch Semikolons getrennt sind. Vorhandene Datensätze in der Tabelle, die ebenfalls im Import enthalten sind, werden durch den Import überschrieben.


LOAD DATA LOCAL 
        INFILE '/home/www/servers/www.servername.de/tmp/import.csv'
        REPLACE
        INTO TABLE tabellenname
        FIELDS
                TERMINATED BY ';'
                OPTIONALLY ENCLOSED BY '"';

Eine vollständige Beschreibung des Kommandos in englischer Sprache ist Bestandteil des MySQL Manuals unter der URL http://www.mysql.com/Manual/manual.html#LOAD_DATA.

Will man die Daten manuell laden, darf man die Zeile nicht mit explode() zerlegen, weil dies bei Datensätzen versagt, die selbst Kommata enthalten. Stattdessen bietet PHP die Funktion fgetcsv() an.

14.10 Wie kann ich eine CSV-Datei aus MySQL exportieren?

Antwort von Kristian Köhntopp

Die Umkehrung von LOAD DATA INFILE ist das SELECT INTO OUTFILE, eine Variante des regulären SELECT.


SELECT
    INTO OUTFILE '/home/www/servers/www.servername.de/tmp/export.csv'
    FIELDS
            TERMINATED BY ';'
            OPTIONALLY ENCLOSED BY '"'
    FROM ...;

Das Kommando wird die Datei auf dem Rechner anlegen, auf dem der Datenbankserver läuft und die Datei wird dem Benutzer gehören, unter dessen User-ID der Datenbankserver abläuft. Der Datenbankserver wird eine existierende Datei nicht überschreiben. Zur Ausführung des Kommandos ist file_priv notwendig.

Eine vollständige Beschreibung des Kommandos in englischer Sprache ist Bestandteil des MySQL Manuals unter der URL http:/www.mysql.com/Manual/manual.html#SELECT.

Wenn man kein file_priv hat, muß man sich stattdessen eine entsprechende Funktion in PHP selber bauen. Dabei ist folgendes zu beachten:

Eine zweispaltige Tabelle, die die Tupel ( a; 10,4) und (b; Er sagte: "Hallo, Du!" ) enthält, muß nach dem Export also so aussehen:


a,"10,4"
b,"Er sagte: ""Hallo, Du!"""

14.11 Wie kann ich die Datensätze der letzten 2 Wochen listen?

Antwort von Kristian Köhntopp

Ein Vollständiges Beispiel folgt. Zunächst die Definition einer einfachen Tabelle mit einigen Werten:


mysql> create table beispiel (
    ->   id integer not null auto_increment primary key,
    ->   nutzlast varchar(80) not null,
    ->   changed timestamp not null );
Query OK, 0 rows affected (0.00 sec)

mysql> insert into beispiel ( nutzlast ) values ( "eins");
Query OK, 1 row affected (0.02 sec)

mysql> insert into beispiel ( nutzlast ) values ( "zwei");
Query OK, 1 row affected (0.00 sec)

mysql> insert into beispiel ( nutzlast ) values ( "drei");
Query OK, 1 row affected (0.00 sec)

mysql> select * from beispiel;
+----+----------+----------------+
| id | nutzlast | changed        |
+----+----------+----------------+
|  1 | eins     | 20000126204514 |
|  2 | zwei     | 20000126204517 |
|  3 | drei     | 20000126204520 |
+----+----------+----------------+
3 rows in set (0.02 sec)

mysql> update beispiel set changed = "20000101123456" where id = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from beispiel where changed > "20000115000000";
+----+----------+----------------+
| id | nutzlast | changed        |
+----+----------+----------------+
|  1 | eins     | 20000126204514 |
|  3 | drei     | 20000126204520 |
+----+----------+----------------+
2 rows in set (0.02 sec)

mysql> quit
Bye

In diesem Beispiel ist eine Tabelle beispiel angelegt worden, die neben einer Nutzlastspalte mit dem Namen nutzlast auch noch einen automatisch vergebenen Primärschlüssel id und ein automatisch aktualisiertes Änderungsdatum enthält. Wir haben das Änderungsdatum für den Datensatz mit der Nummer 2 künstlich zurückgestellt.

Das folgende Beispielscript listet nun alle Datensätze, die in den letzten 2 Wochen geändert worden sind. Es verwendet die Klasse DB_Sql aus PHPLIB zum Zugriff auf die Datenbank, aber die Umstellung auf die Verwendung direkter Datenbankfunktionen ist trivial.


kris@valiant:~/Source/php3 > cat /tmp/probe.php3
#! /home/kris/Source/php3/php -q
<?php
  # PHPLIB Schnellkonfiguration - so nicht verwenden!
  include("/home/kris/www/todo/php-lib/php/db_mysql.inc");

  class DB_Bla extends DB_Sql {
    var $Database = "test";
  }

  # Hier geht es los: Zeitgrenze bestimmen (86400 Sekunden/Tag)
  $twoweeks = date("YmdHis", time()-14*86400);

  # Query generieren
  $query = sprintf("select id,
                    nutzlast,
                    date_format(changed, '%Y-%m-%d %H:%i:%s') as changed
                    from beispiel
                    where changed > '%s'",
              $twoweeks);
  printf("query = %s\n", $query);

  # Query senden und Resultat einlesen.
  $db = new DB_Bla;
  $db->query($query);
  while ($db->next_record()) {
    printf("%s %s %s\n",
      $db->f("id"),
      $db->f("nutzlast"),
      $db->f("changed"));
  }
?>
kris@valiant:~/Source/php3 > /tmp/probe.php3
query = select id,
               nutzlast,
               date_format(changed, '%Y-%m-%d %H:%i:%s') as changed
               from beispiel
               where changed > '20000112210135'
1 eins 2000-01-26 20:45:14
3 drei 2000-01-26 20:45:20

14.12 Wie kann ich eine Tabelle nach IP-Nummern sortieren lassen?

Antwort von Kristian Köhntopp

IP-Nummern sind 32-Bit Zahlen. Die natürliche Sortierreihenfolge ergibt sich, wenn man die IP-Nummer in eine Zahl umrechnet und dann nach dieser Zahl sortiert. Also


0.0.0.1   -> 1
0.0.1.0   -> 256
a.b.c.d

ipnr =
  a * 256*256*256
+ b * 256*256
+ c * 256
+ d

Die Aufgabe besteht also darin, einen SELECT-Ausdruck zu finden, sodaß man


select ip,
       ... as a,
       ... as b,
       ... as c,
       ... as d
  from iptest;

+----------------+------+------+------+------+
| ip             | a    | b    | c    | d    |
+----------------+------+------+------+------+
| 1.2.3.4        | 1    | 2    | 3    | 4    |
| 10.11.12.13    | 10   | 11   | 12   | 13   |
| 100.101.102.10 | 100  | 101  | 102  | 10   |
| 111.11.1.0     | 111  | 11   | 1    | 0    |
| 111.10.3.10    | 111  | 10   | 3    | 10   |
| 193.98.110.1   | 193  | 98   | 110  | 1    |
| 193.174.3.10   | 193  | 174  | 3    | 10   |
+----------------+------+------+------+------+
7 rows in set (0.00 sec)

ausrechnen kann. Hat man das, kann man sehr leicht ein


update iptest set ipnr=
       a * 256*256*256
+      b * 256*256
+      c * 256
+      d;

generieren und dann mit


select * from iptest order by ipnr;

die gewünschte Ausgabe generieren. Bleibt das Problem, geeignete Ausdrücke für die Platzhalter ... zu finden.

Man kann nicht mit substring() alleine arbeiten, da durch Vorbedingungen bei der Eingabe sowohl 001.002.003.004 als auch 1.2.3.4 legal ist. Es ist also notwendig, mittels locate() die Positionen der Punkte zu bestimmen und dann mit variablen Positionen zu arbeiten.

Die folgende, vergleichsweise elegante Lösung stammt von Martin Ramsch:


  SELECT ip,
         (( FLOOR(ip)*256
           +FLOOR(SUBSTRING_INDEX(ip,'.',-3))
          ) *256
          +FLOOR(SUBSTRING_INDEX(ip,'.',-2))
         )*256
         +FLOOR(SUBSTRING_INDEX(ip,'.',-1)) AS ipnr
  FROM iptest
  ORDER BY ipnr;

Dabei spielt man damit, daß der String 1.2.3 im numerischen Kontext einfach als die Zahl 1.2 interpretiert wird. Sauberer wäre es, die Textteile hinter dem Komma immer noch wegzulassen - aber dann wird die Lösung monströser:


  SELECT ip,
         (( SUBSTRING_INDEX(ip,'.',1) *256
           +SUBSTRING_INDEX(SUBSTRING_INDEX(ip,'.',-3),'.',1)
          ) *256
          +SUBSTRING_INDEX(SUBSTRING_INDEX(ip,'.',-2),'.',1)
         )*256
         +SUBSTRING_INDEX(SUBSTRING_INDEX(ip,'.',-1),'.',1) AS ipnr
  FROM iptest
  ORDER BY ipnr;

14.13 Wie lösche ich alle Datensätze, die älter als n Tage sind?

Antwort von Kristian Köhntopp

Die betreffende Tabelle sollte ein Datumsfeld haben, etwa ein selbstaktualisierendes Feld vom Typ TIMESTAMP oder ein manuell aktualisiertes Feld vom Typ DATE. Die folgenden drei Queries löschen jeweils alle Datensätze, die älter als 30 Tage sind, mit steigender Effizienz.


1. delete
       from kalender as k
      where (to_days(current_date) - to_days(k.datum)) > 30

2. delete
       from kalender as k
      where to_days(k.datum) < to_days(current_date)-30;

3. delete
       from kalender as k
      where k.datum < date_add(current_date, interval -30 day)

Die erste Query ist vergleichsweise langsam, denn hier ist die linke Seite der Query ein Ausdruck, der für jede Zeile berechnet werden muß. Der Spaltenname k.datum taucht auf der linken Seite in einer Funktionsanwendung auf, sodaß keine Indices angewendet werden können.

Die zweite Query ist insofern optimiert, als daß der konstante Teil der Rechnung auf die rechte Zeit gebracht werden kann, sodaß diese Seite der Ungleichung zu einer Konstanten optimiert werden kann. Die linke Seite der Query ist jedoch noch immer eine Funktionsanwendung, sodaß ein full table scan notwendig ist.

Die dritte Query ist durchoptimiert: Hier ist die linke Seite der Ungleichung ein reiner Spaltenausdruck, die rechte Seite zu einer Konstanten optimierbar. Wenn ein INDEX(k.datum) existiert, kann er in dieser Query angewendet werden, um den Zugriff zu beschleunigen.

14.14 Wie kann ich Bilder in einer MySQL-Datenbank speichern?

Antwort von Martin Jansen

Die Vor- und Nachteile dieser Methode werden im Kapitel "Datenbanken" im Abschnitt "Ist es sinnvoll, Bilder in einer Datenbank abzulegen?" diskutiert.

Eine sehr gelungene Anleitung, um Binärdaten (also auch Bilder) in einer MySQL-Datenbank zu speichern, beschreibt Florian Dittmer auf http://www.phpbuilder.com/columns/florian19991014.php3.

14.15 Wie kann ich einen zufälligen Eintrag aus einer MySQL-Tabelle auswählen?

Antwort von Martin Jansen

Um einen zufälligen Eintrag aus einer Tabelle auszuwählen, ist folgendes Skript geeignet:


<?php
$host     =    "localhost";
$user     =    "user";
$pass     =    "demo_password";

$datab    =    "demo_db";
$table    =    "test";

/* Verbindung zur Datenbank aufbauen */
$db = @mysql_connect($host,$user,$pass) or die(mysql_error());
@mysql_select_db($datab,$db);

/* Anzahl der Gesamteintraege auslesen */
$result = @mysql_query("SELECT COUNT(*) FROM $table");
$row = mysql_fetch_row($result);

/* Zufallszahl erzeugen */
mt_srand((double)microtime()*1000000);
$number = mt_rand(0,$row[0]-1);

/* Zufaelligen Eintrag auswaehlen */
$result = @mysql_query("SELECT * FROM $table LIMIT $number,1");
?>

Ab MySQL 3.23 besteht dann auch noch die Möglichkeit, mit Hilfe von SELECT * FROM tabelle ORDER BY RAND() LIMIT 1 das zufällige Auswählen der Datenbank zu überlassen.

14.16 Ich habe eine Tabelle mit n Einträgen und möchte auf jeder Seite m davon anzeigen.

Antwort von Kristian Köhntopp

In MySQL kann man zu diesem Zweck die LIMIT-Direktive verwenden, die m Einträge ab Position s einer geordneten Tabelle anzeigt. In anderen Datenbanken muß man sich eine Zeilennummer definieren und kann dann einen Teil der Tabelle mittels einer BETWEEN-Clause auswählen.


# MySQL
mysql> select * from tabelle limit s,m;

Es ist nicht effizient, alle n Datensätze der Tabelle zu selektieren und dann alle Datensätze vor Position s zu überlesen.

Antwort von Daniel T. Gorski

Mit Hilfe eines solchen SQL-Statements kann man sich dann leicht eine Funktion schreiben, die den entsprechenden Ausschnitt der Tabelle anzeigt und Links zum vorhergehenden und folgenden Tabellenausschnitt enthält. Im Folgenden möchten wir hier diese einfache "Blättern"-Funktion realisieren, die uns erlaubt, über die Ergebnisse einer Datenbank-Query vor- und zurück zu browsen.

Es wird davon ausgegangen, daß eine einfache MySQL-Datenbank (deren Name über die Variable $database definiert wird) vorhanden ist. Diese enthält die von uns benötigte Tabelle (Variable $table), über die wir "blättern" wollen.


CREATE DATABASE nameDerDatenbank;    
USE nameDerDatenbank;    
CREATE TABLE nameDerTabelle (ID int(10) unsigned NOT NULL,
                             INHALT text NOT NULL);

Diese Datenbankstruktur kann z.B. mit phpMyAdmin, einem anderem PHP-Script oder direkt mit dem MySQL-Monitor erstellt werden. Um Ausgabeergebnisse zu erhalten, muß die Tabelle selbstverständlich zuerst mit Inhalt gefüllt werden - an dieser Stelle gehen wir davon aus, daß die Tabelle mehrere Einträge enthält.


<?php
// Daniel T. Gorski  dtg/240900/18:49/01
// Achtung: die Definition der $user- und $passwort-Variablen
// _sollte_ in einer externen Datei außerhalb des Document-Root
// festgelegt werden. Diese Datei muß dann an dieser Stelle
// [mit include() oder require()] importiert werden.
// Mehr dazu in dieser dclp-FAQ unter: "Wie kann ich mein
// Datenbankpaßwort gegen Spionage sichern?"

// Datendefinition für Datenbankverbindung.
$host     = "localhost";  // MySQL - Zielrechner.
                          // Normallerweise ist es "localhost", bzw.
                          // synonym "127.0.0.1", also der Rechner,
                          // auf dem auch _dieses_ Script läuft.
$user     = "deinLogin";         // Dein Userlogin.
$password = "deinPasswort";      // Dein Datenbankpasswort.

$database = "nameDerDatenbank";  // Gewünschte Datenbank
                                 // innerhalb von MySQL
$table    = "nameDerTabelle";    // Der Name der Datenbanktabelle

// Datendefinition für die Clientausgabe
$start = (isset($start)) ? abs((int)$start) : 0; 
$limit = 10;                     // Datensätze pro Ausgabeseite

// Verbindung zu MySQL-Datenbank herstellen oder sterben.
@mysql_connect($host,$user,$password)
   or die("Abbruch: Verbindung zu '$host'"
         ." konnte nicht hergestellt werden.");

// Benötigte Datenbank auswählen oder sterben.
@mysql_select_db($database)
   or die("Abbruch: Datenbank '$database' konnte nicht"
         ." selektiert werden.<br><br>MySQL sagt: ".mysql_error());

// Feststellen der Anzahl der verfügbaren Datensätze.
$resultID = @mysql_query("SELECT COUNT(ID) FROM ".$table);
$total    = @mysql_result($resultID,0);

// Ggf. $start korrigieren (falls Parameter in
// der URL manipuliert wurde)
$start    = ($start >= $total) ? $total - $limit : $start;

// Datenbankabfrage ausführen.
$query    = "SELECT ID,INHALT FROM ".$table
           ." LIMIT ".$start.",".$limit;
$resultID = @mysql_query($query);

// Ergebnisse lesen und an den Client ausgeben
while ($data = mysql_fetch_array($resultID))
{
  echo $data["ID"].": ".$data["INHALT"]."<br>";
}  

// Zurück- und Vorblättern
if ($start > 0)
{
  $newStart = ($start - $limit < 0) ? 0 : ($start-$limit);
  echo "<a href=".$PHP_SELF."?start=".$newStart
      .">&lt;&lt; zurück</a>";
}

if ($start + $limit < $total)
{
  $newStart = $start + $limit;
  echo " <a href=".$PHP_SELF."?start=".$newStart
      .">vor &gt;&gt;</a>";
}

// Die benutzte (nichtpersistente) Verbindung zu der MySQL-Datenbank,
// wird nach dem Script-Ende automatisch geschlossen.
// That's it.    
?>

14.17 Wozu ist auto_increment nützlich? Wie erfahre ich den Wert des letzten Inkrements?

Antwort von Daniel T. Gorski

Ganzzahlige Datenbankfelder in MySQL können mit dem Attribut auto_increment versehen werden. Wird über die betreffende Tabelle eine INSERT-Query ausgeführt, so wird automatisch der Wert des mit auto_increment gekennzeichneten Feldes um Eins erhöht (inkrementiert), ohne daß dieses in der INSERT-Query explizit angegeben werden muß bzw. darf. Dies ist bei "flachen" Tabellen ohne Relationen nützlich z.B. bei Gästebüchern.

Mit dem MySQL-Monitor erzeugtes Beispiel:


mysql> CREATE DATABASE foo;
mysql> USE foo;

mysql> CREATE TABLE bar (
    ->    ID int(10) unsigned NOT NULL auto_increment,
    ->    INHALT varchar(32) NOT NULL,
    ->    PRIMARY KEY (ID)
    -> );

mysql> DESCRIBE bar;
+--------+------------------+------+-----+---------+----------------+
| Field  | Type             | Null | Key | Default | Extra          |
+--------+------------------+------+-----+---------+----------------+
| ID     | int(10) unsigned |      | PRI | 0       | auto_increment |
| INHALT | varchar(32)      |      |     |         |                |
+--------+------------------+------+-----+---------+----------------+

mysql> INSERT bar SET INHALT='erster Datensatz';
mysql> INSERT bar SET INHALT='zweiter Datensatz';

mysql> SELECT * FROM bar;
+----+-------------------+
| ID | INHALT            |
+----+-------------------+
|  1 | erster Datensatz  |
|  2 | zweiter Datensatz |
+----+-------------------+
2 rows in set (0.00 sec)

Wie man sehen kann, wird das Feld "ID" automatisch erhöht. Logischerweise darf nur ein Feld mit dem auto_increment Attribut versehen werden. Zusätzlich muß dieses Feld als Index definiert werden - z.B. als Primär-Schlüssel (PRIMARY KEY).

Um den Wert des letzten Inkrements erfahren, stellt PHP die Funktion mysql_insert_id() zur Verfügung:


<?php
// Es wird davon ausgegangen, daß $host, $user und
// $passwort korrekt initialisiert sind

// Verbindung zu MySQL-Datenbank herstellen oder sterben.
$linkID = mysql_connect($host,$user,$password)
   or die("Abbruch: Verbindung zu Host '$host' konnte"
         ." nicht hergestellt werden.");                  

// Benötigte Datenbank auswählen oder sterben.
@mysql_select_db("foo")
   or die("Abbruch: Datenbank '$database' konnte nicht"
         ." selektiert werden.<br><br>MySQL sagt: ".mysql_error());

// INSERT ausführen           
@mysql_query("INSERT bar SET INHALT='dritter Datensatz'");

//  In unserem Beispiel ergibt das beim erstmaligen Aufruf "3"
//  dann "4", dann "5" etc.
echo mysql_insert_id($linkID);
?>

Die Funktion mysql_insert_id() liefert nichts zurück, wenn vorher keine INSERT-Query ausgeführt wurde; sie liefert einen falschen Wert, wenn der Typ des auto-increment-Feldes als BIGINT definiert wird, für die meisten Anwendungen sollte aber der Typ INT UNSIGNED mehr als ausreichend sein.

14.18 Wie lege ich den Initialwert des auto_increment fest? Läuft dieser Wert über?

Antwort von Daniel T. Gorski

Üblicherweise benutzt man Werte ohne Vorzeichen (UNSIGNED) für den Typ des auto_increment-Feldes. Ab der MySQL Server-Version 3.23 ist es auch nicht mehr möglich, in diesen Feldern negative Zahlen zu führen.

Geeignete Datentypen für auto_increment wären:

Für die meisten Anwendungen ist der Datentyp INT UNSIGNED mehr als ausreichend - immerhin ermöglicht dieser eine Adressierung von über vier Milliarden Datensätzen!

Der Initialwert des auto_increment-Feldes ist 1. Um diesen Wert zu ändern, muß man ihn explizit setzen:


mysql> DELETE FROM bar;

mysql> INSERT bar SET INHALT='erster Datensatz';

mysql> SELECT * FROM bar;
+----+------------------+
| ID | INHALT           |
+----+------------------+
|  1 | erster Datensatz |
+----+------------------+

mysql> INSERT bar SET ID='1000', INHALT='zweiter Datensatz';

mysql> SELECT * FROM bar;
+------+-------------------+
| ID   | INHALT            |
+------+-------------------+
|    1 | erster Datensatz  |
| 1000 | zweiter Datensatz |
+------+-------------------+

mysql> INSERT bar SET INHALT='dritter Datensatz';

mysql> SELECT * FROM bar;
+------+-------------------+
| ID   | INHALT            |
+------+-------------------+
|    1 | erster Datensatz  |
| 1000 | zweiter Datensatz |
| 1001 | dritter Datensatz |
+------+-------------------+

Der auto_increment-Wert läuft nicht über - d.h. er wird nicht wieder negativ (bzw. Null bei UNSIGNED), wenn er über seinen Wertebereich hinaus adressiert wird. Stattdessen wird MySQL einen Fehler melden:


mysql> INSERT bar SET ID='4294967295', INHALT='letzter Datensatz';

mysql> SELECT * FROM bar;
+------------+-------------------+
| ID         | INHALT            |
+------------+-------------------+
|          1 | erster Datensatz  |
|       1000 | zweiter Datensatz |
|       1001 | dritter Datensatz |
| 4294967295 | letzter Datensatz |
+------------+-------------------+

mysql> INSERT bar SET INHALT='geht noch einer rein?';
ERROR 1062: Duplicate entry '4294967295' for key 1

14.19 Wie realisiere ich eine Volltextsuche mit MySQL?

Antwort von Matthias P. Wuerfl

Um eine Volltextsuche für eine Website zu realisieren eignen sich speziell dafür erstellte Tools besser. Siehe hierzu auch Wie kann ich eine Volltextsuche realisieren?.

Liegen die Inhalte der Website in einer MySQL-Tabelle, so kann man jedoch auch MySQL zur Suche verwenden. Für den "Hausgebrauch" sollte das auf wenig belasteten Servern oft reichen. MySQL bietet hierzu ab der Version 3.23.23 die Möglichkeit einen Volltextindex anzulegen.

Um die Spalte einer Tabelle mit einem solchen Index zu belegen muß das SQL-Statement ALTER TABLE tabellenname ADD FULLTEXT (textpalte) ausgeführt werden, welches einen entsprechenden Wortindex anlegt. Anschliessend kann mit einer Query wie SELECT * FROM tabellenname WHERE MATCH textspalte1 AGAINST 'suchtext' der Index durchsucht werden. Dieser Wortindex reagiert nur auf ganze Worte, es kann also nicht nach Teilworten oder Wortkombinationen gesucht werden. Die Suche nach "Bauer" findet also nicht "Bauernhof".

Der Ausdruck MATCH a AGAINST b gibt einen Zahlenwert zurück, der die Relevanz des gefundenen Datensatzes wiedergibt, er kann also auch im SELECT-Teil eines SQL-Statements sinnvoll eingesetzt werden. Im ORDER BY-Teil des Statements braucht er nicht vorzukommen, denn MySQL sortiert automatisch nach Relevanz, wenn im WHERE-Teil der Volltextindex abgefragt wird.


SELECT   * FROM tabellenname 
WHERE    MATCH textspalte AGAINST ('wort1 wort2')

...gibt alle Datensätze aus, in denen eines der Suchworter in der textspalte vorkommt - nach Relevanz absteigend sortiert.

Hat man eine MySQL-Version älter als 3.23.23, dann kann man auch eine Volltextsuche realisieren, jedoch geht diese dann wesentlich langsamer vonstatten und belastet den Datenbankserver unverhältnismässig stark, da MySQL hier nicht den Index benutzen kann.


SELECT   * FROM tabellenname
WHERE    textspalte LIKE '%wort1%' 
OR       textspalte LIKE '%wort2%'

Das Prozentzeichen hat im LIKE-Statement von SQL die Funktion, die man in anderen Situationen auch vom Sternchen (*) her kennt. Diese Query findet auch Teilwörter. Die Suche nach "Bauer" findet also auch "Bauernhof".

14.20 Meine Datenbankabfrage/Mein SQL-Statement funktioniert nicht.

Antwort von Guido Haeger

Häufig steht man vor dem Problem, dass ein SQL-Statement fehlerhaft ist und somit nicht zum gewünschten Ergebnis führt. Mit den in der Regel sehr aussagekräftigen Fehlermeldungen des MySQL-Servers ist es meist jedoch recht einfach, die Fehlerursache zu finden und zu beseitigen. Auf diese Fehlermeldungen kann man gezielt mit der Funktion mysql_error() zugreifen.


function mysql_errorhandler($problem, $query = "")
{
        echo "<font color='#FF0000'><b>Datenbankfehler:</b></font><br>\n";
        echo "Problem: $problem <br>\n";
        if($query != "")
        {
                echo "Query: $query <br>\n";
        }
        echo "MySQL: ".mysql_errno()." - ".mysql_error()."<br><br>\n";
}

// Verbindung zum Datenbankserver herstellen
if(!$db = @mysql_connect("host", "user", "password"))
{
        mysql_errorhandler("Verbindungsaufbau gescheitert.");
}

// Datenbank auswählen
if(!@mysql_select_db("database"))
{
        mysql_errorhandler("Auswahl der Datenbank gescheitert.");
}

// Beispiel für ein SQL-Statement
$query = "SELECT * FROM table WHERE x = '$x'";
$result = @mysql_query($query);

if(!$result)
{
        mysql_errorhandler("Datenbankabfrage gescheitert", $query);
}

Mit den Funktionen mysql_num_rows() bzw. mysql_affected_rows() kann man zusätzlich die Anzahl der gefundenen Datensätze bei einem SELECT-Statement bzw. die Anzahl der betroffenen Datensätze bei einem UPDATE-/INSERT-Statement überprüfen.


// Beispiel für ein Select-Statement
$query = "SELECT * FROM table WHERE x = '$x'";
$result = @mysql_query($query);

if(!$result)
{
        mysql_errorhandler("Datenbankabfrage gescheitert", $query);
}
else
{
        echo mysql_num_rows()." Datensätze gefunden.<br>\n";
}

// Beispiel für ein UPDATE-Statement
$query = "UPDATE table SET a = '$a' WHERE x = '$x'";
$result = @mysql_query($query);

if(!$result)
{
        mysql_errorhandler("Datenbankabfrage gescheitert", $query);
}
else
{
        echo mysql_affected_rows()." Datensätze geändert.<br>\n";
}

Man sich das Fehler-Handling wesentlich vereinfachen, in dem man z.B. die MySQL-Klasse der PHPLIB verwendet.


15. Datenbanken: Oracle

15.1 Ora oder OCI?

Antwort von Thomas Fromm

Wenn eine Oracle Version ab 8.0.4 zur Verfügung steht sollte die OCI Schnittstelle verwendet werden. Diese unterstützt z.B. LOBs (Large Objects) und wird zudem noch weiterentwickelt.

15.2 Ich habe Oracle-Support mit --with-oci8 in PHP eincompiliert, nun startet der Apache nicht mehr.

Antwort von Thomas Fromm

Die häufigste Ursache ist:

Die shared libraries für den Oracle-Support werden nicht gefunden.

Lösung: Das Verzeichnis $ORACLE_HOME/lib in /etc/ld.so.conf eintragen und ldconfig aufrufen.

Eine weitere Möglichkeit wäre ein Fehler in der glibc-2.1. Behoben kann dies werden, indem den Apache neu gelinkt wird und dabei die Option -lpthread zu den LDFLAGS hinzugefügt wird. Dieser Fehler wurde in der glibc-2.2 behoben.

linux:/usr/src/apache_1_3_14/ # CFLAGS='-lpthread' ./configure ...

15.3 Unix: "Call to unsupported or undefined function: OCILogon()"

Antwort von Thomas Fromm

Wenn die Konfiguration von PHP mit --with-oci8 korrekt erfolgt ist, dann kann mit einiger Sicherheit angenommen werden, daß ORACLE nicht korrekt installiert wurde. Falls man die Fehlermeldungen von configure und make überlesen hat, ist es möglich, daß der Oracle-Support nicht oder nur fehlerhaft in PHP einkompiliert wurde. Dies geschieht meist dann, wenn man übersieht, daß die Headerdateien, die von PHP benötigt werden, in der ORACLE Clientinstallation nicht enthalten sind. Die Dateien befinden sich in $ORACLE_HOME/rdbms/demo/ und $ORACLE_HOME/rdbms/public/.

Lösung: Nachinstallieren von Pro*C/C++ und der Demodateien aus der ORACLE-Server Distribution, danach Neukompilierung von PHP.

15.4 "Warning: ORA-12154: TNS:could not resolve service name"

Antwort von Thomas Fromm

Diese Meldung kann zwei Ursachen haben:

Der TNS/-Name, der bei der Verbindung zu einem ORACLE-Server benötigt und in OCILogon() übergeben wird ist falsch, oder der TNS/-Name ist in der Datei tnsnames.ora im Verzeichnis $ORACLE_HOME/network/admin/ nicht eingetragen.

Lösung: Der Eintrag in der Datei tnsnames.ora muß korrekt sein, was man mit tnsping oder SQL*Plus überprüfen kann. Dieser Eintrag muß dann genau so der Funktion OCILogon() übergeben werden.

Achtung: Kommt stattdessen die Meldung "oci_open_server: Error while trying to retrieve text for error ORA-12154", so ist die Umbgebungsvariable ORACLE_HOME falsch oder nicht gesetzt. Der nächste Abschnitt enthält weitere Informationen zu Umgebungsvariablen.

15.5 Der Webserver verbraucht jetzt viel mehr Speicher als ohne Oracle, mache ich was falsch?

Antwort von Thomas Fromm

Nein. Durch das Einbinden der Oracle Libs verbraucht der Webserver in der Regel erheblich mehr Speicher. Beim Apachen kann das schonmal auf 20 MB je Child anwachsen im Betrieb.

15.6 Umlaute, die in die Datenbank eingetragen wurden, werden nicht korrekt dargestellt.

Antwort von Thomas Fromm

Sowohl beim Eintragen als auch beim Auslesen der Daten in Textfeldern ist darauf zu achten, daß der verwendete Client die richtigen NLS/-Parameter verwendet. Die Clients bekommen diese Information über die Umgebungsvariablen NLS_LANG und ORA_NLS33. Der richtige Wert für NLS_LANG ist der Datenbank selber zu entnehmen. Hat die Datenbank zum Beispiel als Character Set die Einstellung WE8ISO8859P9, so sollte die Variable {SPRACHE}_{LAND}.WE8ISO8859P9 lauten, wobei {SPRACHE} und {LAND} nur für die Steuerung der Meldungen, die der Client zurückgibt, zuständig sind, und vom mit dem ORACLE-Client installierten Sprachpaket abhängig sind.

Die Einstellung von ORA_NLS33 dient dazu, dem Client mitzuteilen, wo sich die Dateien befinden, die die Prompts in verschiedenen Sprachen beinhalten.

Ein Beispiel für korrekte Einstellung:


export ORACLE_HOME=/opt/oracle/OraHome1
export ORA_NLS33=$ORACLE_HOME/ocommon/nls/admin/data
export NLS_LANG GERMAN_GERMANY.WE8ISO8859P9

Nun ist es noch wichtig, dass die Variablen von PHP korrekt initialisiert werden. Dabei ist darauf zu achten, dass man beim Modul-PHP diese Variablen vor dem Start des apache setzt - im Script apachectl ist beispielsweise ein guter Platz dafür.

15.7 "Warning: ORA-12705: invalid or unknown NLS parameter value specified"

Antwort von Thomas Fromm

Die im vorigen Punkt erwähnten Einstellungen sind nicht korrekt. Der Datenbankserver versteht entweder das Character Set nicht oder der Pfad in ORA_NLS33 ist nicht korrekt.

15.8 Gibt es auto_increment unter Oracle?

Antwort von Thomas Fromm

Nein. Jedoch kann auto_increment via Trigger emuliert werden.

Beispiel:

Wer von MySQL nach Oracle portiert vermisst wohl als erstes das auto_increment Feature von MySQL Spalten. Dieses läßt sich jedoch unter der Nutzung von Sequencen und Triggern bei Oracle erstellen:


rem Wir brauchen zum einen einen Zähler,
rem der hochzählt (dazu die Sequence)
 
create sequence zaehler_der_tabelle_xy
       increment by 1 start with 1 cache 2;
 
rem Jetzt die eigentliche Tabelle
 
CREATE TABLE xy (
        id_xy NUMBER(20,0) PRIMARY KEY,
        bla_xy VARCHAR2(4000)
);
 
rem Nun ist noch ein Trigger vonnöten, der 
rem die neue Id von der Sequence übergeben
rem bekommt und vor dem insert diesen Wert
rem auf die entsprechende Spalte überträgt:
 
CREATE TRIGGER trigger_primary_key BEFORE INSERT ON xy
       REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW
Begin
select zaehler_der_tabelle_xy.nextval into :NEW.id_xy from DUAL;
End;
/

Bei jedem insert wird nun der Wert von id_xy automatisch hochgesetzt. Zu beachten ist, daß es besser ist, für jede Tabelle eine eigene Sequence zu erstellen, denn sonst müssen sich alle Tabellen die fortlaufenden Zahlen teilen.

Alternativ kann auch ohne Trigger gearbeitet werden und die nächste Zahl per Hand selectiert werden:


$stmt=OCIParse($conn, "SELECT zaehler_der_tabelle_xy.nextval FROM DUAL");
OCIExecute($stmt);
OCIFetch($stmt);
$nextid=OCIResult($stmt, "NEXTVAL");

15.9 Ich verwende das obige Beispiel. Wie kann ich nun mysql_insert_id() emulieren?

Antwort von Thomas Fromm

Der aktuellen Wert der Sequenz zaehler_der_tabelle_xy kann durch Verwendung von currval (abgeleitet von Current Value) ermittelt werden. Mit dem folgenden Codestück ist es möglich, den zuletzt eingefügten Wert abzufragen:


// INSERT in die Tabelle

$stmt = OCIParse($conn, "INSERT INTO xy (BLA_XY) VALUES 'BLA'");
OCIExecute($stmt, OCI_DEFAULT);

//Abfrage der Sequence
$stmt = OCIParse($conn,"SELECT zaehler_der_tabelle_xy.currval
                 AS CV FROM DUAL");
OCIExecute($stmt, OCI_DEFAULT);
OCIFetch($stmt);
$last_id=OCIResult($stmt, "CV");
OCICommit($conn);

Achtung: Das selectieren des currval funktioniert nur innerhalb derselben Transaktion (daher auch beim OCI_DEFAULT). Will man den aktuellen höchsten ID-Wert ermitteln, ist es besser, unter Verwendung der SQL-Funktion MAX() den höchsten Wert direkt aus der Tabelle abzufragen.

15.10 Wie selectiere ich nur bestimmte Zeilen (LIMIT unter MySQL)?

Antwort von Thomas Fromm

Da Oracle über kein Limit verfügt gestaltet sich die Abfrage etwas komplizierter (das Beispiel funktioniert erst ab Version 8.1):


SELECT *
  FROM
   (SELECT ROWNUM rownum2, inline_view1.*
      FROM
       (SELECT ROWNUM rownum1, ename, hiredate
          FROM emp
          ORDER BY hiredate
       ) inline_view1  -- zum Sortieren (ROWNUM hier noch ungeordnet)
   ) inline_view2      -- ROWNUM spiegelt jetzt die Sortierung wider
WHERE rownum2 BETWEEN 5 AND 7

Dies ist nicht sonderlich schnell, weil die innere Abfrage alle Zeilen auswählt. Für Versionen ab 8.1.6 geht auch folgendes:


SELECT *
  FROM
   (SELECT ROW_NUMBER()
    OVER(ORDER BY hiredate) rownum1, ename, hiredate
      FROM emp
   ) inline_view1
WHERE rownum1 BETWEEN 5 AND 7
/

Will man lediglich n Zeilen ausgeben tuts auch dies:


SELECT * FROM 
  (SELECT ename, hiredate FROM emp ORDER BY hiredate)
WHERE ROWNUM < 6

15.11 Wie speichere ich Datensätze mit mehr als 2000 Zeichen ab?

Antwort von Thomas Fromm

Um diese Datenfelder abzuspeichern, muss zuerst Speicher angefordert werden, dies geschieht mit OCIBindByName().


$req="INSERT INTO wurstbrote (name) VALUES (:name)";
$stmt=OCIParse($req);
// nun binde ich den Inhalt von $wurstbrotname
// an den Oracle Platzhalter :name
OCIBindByName($stmt,":name",$wurstbrotname,-1);
OCIExecute($stmt);

Trotz dieser Umständlichkeit gestaltet sich das Lesen/Schreiben von grösseren Datensätzen performanter als z.B. bei MySQL.

15.12 Wie bearbeite ich LOBs mit PHP?

Antwort von Thomas Fromm

Diese Codebeispiele beschreiben insert, update und select:


// INSERT:
$req = "INSERT INTO bdata (description, data) VALUES
        (:description, EMPTY_BLOB()) returning data into :data";
$stmt = OCIParse($conn, $req);
$lob = OCINewDescriptor($conn, OCI_D_LOB);
OCIBindByName($stmt, ":description", $description, -1);
OCIBindByName($stmt, ":data", $lob, -1, OCI_B_BLOB);
OCIExecute($stmt, OCI_DEFAULT);
if($lob->save($bdata)) {
 OCICommit($conn);
} else {
 echo "Problems: Couldn't upload Lob\n";
}
OCIFreeDesc($lob);
OCIFreeStatement($stmt);

// Für die verwendeten Typen beim OCIBindByName auf jeden Fall mal
// ins PHP Handbuch schauen.

// UPDATE:
// Das SELECT FOR UPDATE sperrt den Eintrag für 
// andere Schreibzugriffe
// Diese Sperre wird erst beim nächsten Commit aufgehoben
$req="SELECT data FROM bdata WHERE id='5' FOR UPDATE";
$stmt=OCIParse($req);
OCIExecute($stmt, OCI_DEFAULT);

$req="UPDATE bdata SET data=:data WHERE id='5'";
$stmt=OCIParse($req);
OCIBindByName($stmt, ":data", $data, -1);
OCIExecute($stmt, OCI_DEFAULT);
OCICommit($conn);

// SELECT
$req="SELECT data FROM bdata WHERE id='5'";
$stmt=OCIParse($req);
OCIExecute($stmt);
OCIFetch($stmt);
$bdatalob=OCIResult($stmt, "DATA");
$bdata=$bdatalob->load();

15.13 Wie nenne ich Spalten um?

Antwort von Thomas Fromm

Oracle bietet dazu nicht direkt eine Möglichkeit. Sofern man DBA Rechte hat kann man dies über einen Midnighthack lösen:

(Ist mit Vorsicht zu geniessen und am besten nicht zu benutzen :-)


update SYS.COL$ col set col.NAME = 'neuer_name'
where col.NAME = 'alter_name' and col.OBJ# in (
    select ob.OBJ# from SYS.OBJ$ ob, SYS.USER$ us
    where ob.OWNER# = us.USER# and us.NAME = 'besitzername'
    and ob.NAME = 'alter_name'
);

Besitzername ist der Name des Besitzers der Tabelle.

Wichtig: Alle Namen müssen in Grossbustaben angegeben werden!

15.14 Wie kann ich SQL Skriptdateien in Oracle ausführen?

Antwort von Thomas Fromm

Einfach Sqlplus starten und dann:


SQL> @meinedatei.sql

15.15 Welche freien Tools gibts für Oracle?

Antwort von Thomas Fromm

Zu empfehlen ist der Oracle Objectmanager von OraSoft. Seit kurzem gibt es auch ein ähnliches in PHP geschriebenes Tool phpOracleAdmin. Wer TOAD von Windows gewohnt ist, wird sich auf TOra freuen, dies ist eine freie, ja man könnte sagen, PL/SQL IDE.

15.16 Ich bekomme ein Oracle Fehlernummer ORA-XXXXX, wo stehen die Fehlercodes?

Antwort von Thomas Fromm

Oracle Fehlermeldungen bestehen aus einem Fehlerbereich (ORA, OCI ...) und einer 5-stelligen Fehlernummer. Die komplette Fehlerbeschreibung bekommt man mit: 


linux:# / oerr ora <nummer>

Vorrausgesetzt die Pfade ins $ORACLE_HOME/bin sind gesetzt, erscheint der volle Fehlertext. Kommt allerdings eine Fehlermeldung der Art: 


Cannot find /u01/8.1.6/rdbms/mesg/orad.msg file.

ist die Oracle Installation eine teilweise ans deutsche angepasste Version. (oraus.msg ist die original Datei und orad.msg ist eine deutsche Version) Da die orad.msg nciht in allen Fällen vorhanden ist, empfiehlt es sich einen symbolischen Link zu setzen. 


linux:# / cd /u01/8.1.6/rdbms/mesg/
linux:# / ln -s oraus.msg orad.msg

Dannach sollte man zumindest die englischen Fehlertexte erhalten.

15.17 Welche Bücher zu Oracle sind empfehlenswert?

Antwort von Thomas Fromm

Für das nötige Basiswissen empfiehlt sich die "Oracle Referenz", die auch Einsteigern u.a. den Zugang zu PL/SQL erleichtert.

"Oracle8 für den DBA" trägt zum alltäglichen Umgang mit Oracle bei. Gerade im produktiven Einsatz, zeigt sich, das die administrative Seite der Datenbank nicht zu unterschätzen ist.

Gerade bei Webapplikationen ist Performance und Reaktionszeit wichtig, ich empfehle hier "Oracle 8. Tuning.".
Während die Oracle Referenz ins Regal eines jeden Entwicklers gehört, der auf dieser Datenbank Applikationen entwickelt, bieten die beiden anderen Bücher eine Abrundung der Nachschlagewerke für den allgemeinen Umgang. Die dort aufgeführten Beispiele sind verständlich geschrieben und leicht nachzuvollziehen. (Ein bisschen gewöhnungsbedürftig ist allerdings bei Oracle Press Bücherübersetzungen die Indizierung...)

Wenn man mehr auf Optimierung und Performance ausgerichtet ist, dem kann ich nur "Oracle PL/SQL Programmierung" ans Herz legen. Neben einer ausführlichen Behandlung von PL/SQL werden dort auch wichtige Packages wie z.B. DBMS_JOB, der Oracle interne Cronjobmechanismus erklärt. Zusätzlich gibt es auch ein Kapitel, welches die Einbindung von Externen Prozeduren (welche man in C oder Java Programmieren kann) erläutert anhand von Beispielen.


16. phpMyAdmin

16.1 Was ist phpMyAdmin?

Antwort von Tobias Ratschiller

phpMyAdmin ist eine in PHP geschriebene Verwaltungsoberfläche für MySQL. Weitere Informationen dazu finden Sie auf der Homepage.

16.2 Ich bin kein MySQL-Administrator. Wie kann ich phpMyAdmin nur für mich selbst installieren?

Antwort von Tobias Ratschiller

Holen Sie sich die Distribution (TarGz or Zip) von der phpMyAdmin-Homepage. Bitte folgen Sie dann den Anweisungen in der Datei INSTALL; für's erste Ausprobieren genügt es, in die Datei config.inc.php3 Ihren MySQL-Benutzernamen und -Passwort einzutragen.

16.3 Ich bin MySQL-Administrator und möchte ein Exemplar phpAdmin für alle meine User installieren.

Antwort von Tobias Ratschiller

Seit phpMyAdmin 2.0.3 ist es möglich, eine zentrale Kopie von phpMyAdmin zu installieren, in die sich die einzelnen Benutzer mit Benutzername und Passwort einloggen. phpMyAdmin benutzt dafür das Rechte-System von MySQL. Benutzer müssen daher korrekt in das Rechte-System eingetragen sein: Für jeden Benutzer, der auf phpMyAdmin zugreifen können soll, muss ein Eintrag in die mysql.user und mysql.db-Tabelle gemacht werden. Um dem Benutzer foo Zugriff auf die Datenbank foo_db zu geben, würden Sie folgende SQL-Statements benutzen:


INSERT INTO user (Host, User, Password, Select_priv, Insert_priv,
                 Update_priv, Delete_priv, Create_priv, Drop_priv,
                 Reload_priv, Shutdown_priv, Process_priv,
                 File_priv, Grant_priv, References_priv, Index_priv,
                 Alter_priv)
          VALUES ('localhost', 'foo', PASSWORD('bar'), 'N', 'N',
                 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N',
                 'N', 'N')
INSERT INTO db   (Host, Db, User, Select_priv, Insert_priv,
                 Update_priv, Delete_priv, Create_priv, Drop_priv,
                 Grant_priv, References_priv, Index_priv, Alter_priv)
          VALUES ('localhost', 'foo_db', 'foo', 'Y', 'Y', 'Y', 'Y',
                 'Y', 'Y', '', '', '', '')

Bitte beachten Sie, daß Sie nach dieser Änderung ein FLUSH PRIVILEGES-Statement ausführen müssen, damit sie wirksam wird.

Seit phpMyAdmin 2.0.6 werden auch Wildcards im Rechte-System unterstützt; damit können Sie dem Benutzer foo beispielsweise Zugriff auf alle Datenbanken geben, deren Name mit foo_ beginnt. Weitere Informationen zum Setup eines solchen Benutzers finden Sie im MySQL-Handbuch.

16.4 Wieso kann ich den Inhalt meiner Tabelle nicht editieren?

Antwort von Tobias Ratschiller

Bis phpMyAdmin 2.0.6 können Sie den Inhalt einer Tabelle nur dann ändern, wenn ein Primärschlüssel in der Tabelle gesetzt ist. Ab 2.0.6 können Sie den Inhalt in allen Fällen editieren. Falls kein Primärschlüssel existiert, wird allerdings der Inhalt aller Zeilen mit zu der aktuellen Zeile äquivalenten Inhalten geändert, da es im relationalen Datenbankmodell unmöglich ist, diese Zeilen voneinander zu unterscheiden.

Es ist gutes Datenbankdesign, wenn man für eine jede Tabelle eine Spalte (atomarer Primärschlüssel) oder eine Kombination von Spalten (zusammengesetzter Primärschlüssel) als Primärschlüssel deklariert, sodaß keine zwei Zeilen existieren können, die in den als Primärschlüssel deklarierten Spalten dieselben Werte haben können. Alle Zeilen werden durch ihre Primärschlüsselwerte überhaupt erst unterscheidbar.

16.5 Wieso werden TIMESTAMP-Felder nicht auf die aktuelle Zeit gesetzt, wenn ich eine neue Zeile einfüge?

Antwort von Tobias Ratschiller

phpMyAdmin trägt einen leeren String ('') ein, wenn Sie keinen Wert angeben. MySQL konvertiert dies zu 0000-00-00 00:00:00. Um den Default-Wert eines Feldes einzutragen (im Fallse von TIMESTAMP die aktuelle Zeit) geben Sie im Eintragsformular "null" (ohne Anführungszeichen) als Wert ein.

16.6 Wieso kann ich in phpMyAdmin mehrere durch Semikolon getrennte SQL-Statements ausführen, nicht aber mit normalen PHP-Funktionen?

Antwort von Tobias Ratschiller

SQL selbst definiert nur einzelne Anweisungen, keine Anweisungsfolgen. phpMyAdmin trennt die Zeichenkette auf und generiert dann automatisch mehrere einzelne Anfragen. PHP selbst macht das nicht, also müssen Sie selbst die Anweisungen nacheinander einzeln senden.


17. PHPLIB

17.1 Was ist PHPLIB?

Antwort von Kristian Köhntopp

PHPLIB ist eine Sammlung von Klassen, mit denen man Webanwendungen einfacher und sicherer schreiben kann. PHPLIB realisiert Sessions und Sessionvariablen, also Variablen, die ihren Wert am Ende einer Seite behalten und auf die nächste Seite mitgeschleppt werden. Dadurch braucht man solche Variablen nicht mehr in HIDDEN-Feldern von Formular zu Formular durchzuschleifen.

Auf der Grundlage solcher Variablen implementiert PHPLIB dann eine Reihe von weiteren Funktionen, zum Beispiel eine bessere und flexiblere Benutzerauthentisierung, als dies mit dem Webserver oder blanken PHP möglich ist, und ein System zur Kontrolle von Zugriffsrechten auf Webseiten und deren Funktionen.

Außerdem enthält PHPLIB eine Reihe von unabhängigen Klassen, die Bedienelemente für Webseiten und Hilfsfunktionalität zur Handhabung von Formularen bereitstellen.

17.2 Wo kann ich PHPLIB bekommen?

Antwort von Kristian Köhntopp

Die Website von PHPLIB ist http://phplib.netuse.de/. Die Entwicklung von PHPLIB wird durch NetUSE AG gefördert.

Informationen über PHPLIB finden sich in englischer Sprache als Bestandteil des Downloads und auf der Website. Außerdem existiert noch eine deutsche Anleitung unter der URL http://www.koehntopp.de/kris/artikel/phplib-deutsch/. Dieser Text bildet außerdem Kapitel 24 von PHP 4. Dynamische Webauftritte professionell realisieren. Er ist mit dem 3. Reprint der 1. Auflage dazugekommen.

17.3 Mein Provider hat PHPLIB nicht installiert.

Antwort von Kristian Köhntopp

PHPLIB ist eine Sammlung von PHP3-Scripten. Sofern man bei einem Provider PHP3 ausführen kann, kann man dort PHPLIB installieren und anwenden. Die Verwendung von PHPLIB ist einfacher, wenn der Provider den Zugriff auf die php3.ini gestattet oder das Setzen gewisser Konfigurationsvariablen auf andere Weise erlaubt.

PHPLIB funktioniert besser, wenn track_vars eingeschaltet sind und include_path und auto_prepend_file korrekt gesetzt sind.

17.4 Ich habe keinen Zugriff auf die php.ini.

Antwort von Kristian Köhntopp

In der Anleitung zu PHPLIB ist die Rede davon, verschiedene Parameter in der php.ini anzupassen. Wenn man bei einem Webhoster untergebracht ist, der keinen Zugriff auf diese Parameter gestattet, hat man verschiedene Optionen, PHPLIB dennoch zu verwenden.

17.5 "Oops, php3_SetCookie called after header has been sent!"

Antwort von Kristian Köhntopp

PHP puffert seine Ausgaben nicht. Die Funktionen setcookie() und header() können nur verwendet werden, solange noch keine einzige Ausgabe durch das PHP-Script gemacht wurde.

Fügt man bei der Bearbeitung der prepend.php3 oder local.inc Leerzeichen oder Leerzeilen an, kommt es bei der Einbindung dieser Dateien jedoch zur Ausgabe dieser Zeichen durch das PHP-Script, und damit sind diese beiden Funktionen nicht mehr durch PHPLIB verwendbar. Auch vor dem Aufruf von page_open() durch die eigentliche Seite dürfen keine Ausgaben gemacht werden.

17.6 GET-Mode oder Cookie-Mode? Sind Cookies böse?

Antwort von Kristian Köhntopp

Jede Form von Session-Management basiert auf zwei grundlegenden Dingen:

Grundsätzlich gibt es zwei Methoden, die Session-ID von einer Seite zu nächsten weiterzugeben: Entweder die ID wird per Cookie "unsichtbar" als Bestandteil des Requests weitergegeben, oder sie wird auf irgendeine Weise Bestandteil der URL, etwa als GET-Parameter mit einem ? an die URL angehängt, als PATH_INFO an die URL angehängt, als regulärer Pfadbestandteil, der von mod_rewrite herausgepult wird oder als Bestandteil des Hostnamens mit einem Wildcard A-Records im DNS. PHPLIB unterstützt direkt die Weitergabe der Session-ID als Cookie und als GET-Parameter, über mod_rewrite und einige minimale Änderungen ist jedoch auch die Weitergabe als Pfadbestandteil oder Hostname möglich.

Ist die Session-ID in irgendeiner Form Bestandteil der URL, bekommt man das Problem, daß die URL mit der Session-ID gebookmarked wird oder - schlimmer - irgendwo abgedruckt wird. In diesem Fall kann es dazu kommen, daß zwei Benutzer dieselbe Session verwenden, die nichts miteinander zu tun haben. Dies kann bei Cookies niemals der Fall sein. Daher ist es aus technischer Sicht auf jeden Fall günstiger, Cookies zur Propagation der Session zu verwenden.

Die Presse und schlecht informierte Verbraucherschützer haben Cookies jedoch einen schlechten Ruf eingebracht. Dort wird behauptet, Cookies seien üble Instrumente, um den Kunden zu tracken und sein Verhalten im Web auszuspionieren. Tatsächlich ist es so, daß man wiedererkennbare Benutzer und ihr Verhalten aufzeichnen und auswerten kann - ohne Wiedererkennung sind jedoch auch keine Warenkörbe, personalisierte Websites oder andere individuelle Services möglich.

Andererseits schützt das Ablehnen eines Cookies auch nicht vor dem Tracking und dem folgenden Auswerten des Benutzerverhaltens: Wie oben gezeigt, kann man Session-IDs auch auf andere, schlechtere Weise als durch Cookies weiterverbreiten. Wenn es eine Website also darauf abgesehen hat, einen Benutzer zu tracken, dann hilft das Ablehnen des Cookies exakt gar nichts. Stattdessen ist es notwendig, den Kontakt zu dieser Website vollständig abzubrechen (dies ist insbesondere dann der Fall, wenn man sich nicht von einer Banneragentur wie DoubleClick ausspionieren lassen möchte. Ablehnen des Cookies nutzt auch hier nichts. Stattdessen muß man sich einen Proxy wie etwa JunkBuster installieren und auf diesem allen Datenverkehr in Richtung DoubleClick erden). Anders gesagt: nicht die Cookies sind böse, sondern das, was manche Firmen damit und jeder anderen Form von eindeutiger Identifikation anstellen.

Um bei den Benutzern optimal zu funktionieren, die umsichtigerweise Cookies nicht abgeschaltet haben, aber einen sinnvollen Fallback in allen anderen Fällen zu bieten, gibt es in PHPLIB die Betriebsart fallback_mode. Ist die Variable mode in Session auf cookie gestellt, kann man durch Einstellen eines fallback_mode von get dafür Sorgen, daß PHPLIB zunächst einmal versucht, eine Session mit Cookies aufzubauen. Hat der Anwender jedoch Cookies abgeschaltet, wird transparent auf GET-Modus umgeschaltet.

17.7 Was ist das Sevenval-Patent?

Antwort von Kristian Köhntopp

Die Firma Sevenval hat sich die Speicherung der Session-ID im Hostnamen patentieren lassen. Das Patent von Walkowiak, Olaf und Sponagl, Paul umfaßt laut Abstract folgendes:

In a method for providing state information in a stateless data communications protocol, the state information being provided between a client and a server site, said server site being accessible at each of cluster of site names, one site name of a said cluster of site names is used for accessing said server site, said site name containing the encoded state information. A computer program product and an apparatus compromise corresponding features. The invention creates a way of providing state information in a stateless data communication protocol with very little effort.

In der Patentschrift:

... merits of the inventors have shown that the configuration of both the nameserver and the server site in the way described are possible with very little programming effort.

Im Wesentlichen versucht die Schrift, auf ca. 30 Seiten mit vielen unnötigen Worten anhand von Apache, Bind, PHP, mod_rewrite und mod_unique zu erklären, wie die Technik funktioniert - genauso, wie es jeder nach wenigen Minuten selber nachkonfiguriert hat. Klar ist, daß zwar die Idee, nicht aber die Konfiguration geschützt ist.

Die Erfindung umfaßt, unter Benutzung obiger "said" Methode insbesondere:

Im anschließenden Gespräch ergaben sich noch folgende Infos:

17.8 Warum verwendet PHPLIB nicht die IP-Nummer des Browsers als Schutz gegen eine Übernahme der Session?

Antwort von Kristian Köhntopp

PHPLIB versucht, Sessions Benutzern zuzuordnen. IP-Nummern sind konstruktionsbedingt immer den Netzwerkinterfaces von Rechnern und nicht Benutzern zugeordnet. Das ist eine vollkommen andere Sache und die Auswertung von IP-Nummern würde zu Fehlern im Betrieb führen.

Auch eine Auswertung der Headerzeile X-Forwarded-For, die manche Proxies setzen, ist nicht sinnvoll:

17.9 Warum sind die Session-IDs von PHPLIB so lang?

Antwort von Kristian Köhntopp

PHPLIB verwendet als Session-ID eine kryptographische Prüfsumme (MD5-Verfahren) über einen String mit einer geheimen Komponente. Auf diese Weise ist die Session-ID einer PHPLIB-Session nicht vorhersagbar und ratbar. Würde PHPLIB fortlaufende Nummern als Session-IDs verwenden, könnte der Inhaber der Session 17 davon ausgehen, daß auch die IDs 16 und 18 belegt sind und versuchen, seine Session-ID zu ändern und so eine fremde Session zu übernehmen.

Dadurch, daß PHPLIB quasi-zufällige, nicht ratbare IDs verwendet, ist diesem Angriff ein Riegel vorgeschoben.

17.10 Was schreibe ich denn nun in meine local.inc?

Antwort von Kristian Köhntopp

In einer typischen Installation von PHPLIB wird keine der mitgelieferten Dateien verändert mit Ausnahme der Dateien prepend.php3, local.inc und setup.inc.

In der Datei prepend.php3 wird festgelegt, welche Dateien auf jeder PHPLIB nutzenden Seite per require() eingebunden werden. Standardmäßig wird ein MySQL-Datenbankinterface und die SQL-Storagemethode eingebunden. Speichert man seine Sessiondaten nicht in einer SQL-Datenbank oder verwendet man kein MySQL, so muß man diese Datei anpassen.

In der Datei setup.inc stehen die Anweisungen, die bei der Initialisierung einer neuen Benutzersession ausgeführt werden, wenn in der Session-Definition die Option auto_init verwendet wird (siehe Dokumentation von PHPLIB). Dies ist ein fortgeschrittenes Feature und wird zunächst nicht benötigt. Es ist per Default ausgeschaltet.

In der Datei local.inc findet die eigentliche Anpassung von PHPLIB statt. Die Klassendefinitionen von PHPLIB werden in der Regel nicht direkt verwendet, sondern man definiert sich eigene Klassen, die die Definitionen von PHPLIB erweitern und auf die eigenen Verhältnisse anpassen. Diese Anpassungen werden in der Regel in der Datei local.inc abgelegt.

Um mit PHPLIB arbeiten zu können, muß man minimal eine Datenbankklasse als Unterklasse von DB_SQL definieren, eine Speichermethode als Unterklasse von z.B. CT_Sql definieren und die Eigenschaften der eigenen Session als Unterklasse von Session festlegen. Minimal muß eine local.inc also folgendermaßen aussehen:


class DB_Beispiel extends DB_Sql {
  var $Host     = "localhost"; # Name des Datenbankservers
  var $Database = "beispiel";  # Name der MySQL-Datenbank
  var $User     = "web";       # Name des Datenbankusers
  var $Password = "";          # Paßwort des Datenbankusers

  var $Debug = 0;              # SQL-Debugging ausgeschaltet
}

class Beispiel_CT_Sql extends CT_Sql {
  # Name der Datenbankklasse von oben
  var $database_class = "DB_Beispiel";

  # Name der active_sessions-Tabelle
  var $database_table = "active_sessions";
}

class Beispiel_Session extends Session {
  # Name der Klasse aus der Definition
  var $classname = "Beispiel_Session";

  # Irgendein String, aber geheim muß er sein
  var $magic          = "Hocuspocus";

  # Verwende Cookies, um die Session weiterzugeben
  var $mode           = "cookie";

  # Verwende Sessioncookies 
  # (Diese werden nicht in der cookies.txt gespeichert)
  var $lifetime       = 0;

  # Name der CT_Sql-Unterklasse
  var $that_class     = "Beispiel_CT_Sql";

  # Garbage Collection mit 5% Wahrscheinlichkeit,
  var $gc_probability = 5;

  # Caching aller Seiten komplett verbieten
  var $allowcache     = "no";
}

17.11 ERROR 1146: Table 'xyz.active_sessions' doesn't exist!

Antwort von Kristian Köhntopp

PHP hat ein Misfeature mit dem Namen Connection reuse. Diese Eigenschaft bewirkt, daß man keine zwei Datenbankverbindungen mit denselben Connectparametern (in MySQL: Username, Paßwort, Host) öffnen kann. Versucht man eine zweite Verbindung mit denselben Eigenschaften zu öffnen, liefert PHP immer die erste Link-ID zurück.


kris@valiant:~/www/kris.koehntopp.de/pages/php > ~/bin/php -q
<?php
  $link = mysql_connect("localhost", "kris", "");
  print $link;
1

  $link2 = mysql_connect("localhost", "kris", "");
  print $link;
1

Diese Eigenschaft wäre dann nützlich, wenn ein Link tatsächlich zustandslos und gegen jedes beliebige andere Link austauschbar wäre. Das ist jedoch nicht der Fall: In MySQL hat ein Link zum Beispiel eine aktuelle Datenbank, die mit mysql_select_db() oder mit dem SQL-Kommando use gesetzt werden kann. Setzt der Code oben auf $link eine aktuelle Datenbank a und verwendet man dann gedankenlos $link2 mit einer anderen Datenbank b, dann werden Statements auf $link nicht mehr korrekt funktionieren, weil es sich in Wirklichkeit um dasselbe Link handelt.

Dieselbe Situiation, nur viel gefährlicher, entsteht bei anderen Datenbanken mit Transaktionen, weil der aktuelle Zustand eines Links hier die offene Transaktion mit einbezieht: Ein commit auf $link kann hier vermeintlich unabhängige Daten auf dem vermeintlich getrennten $link2 mit committen.

Zu dem Fehler table 'active_sessions does not exist!' kommt es immer genau dann, wenn mit mehr als einer Datenbank und mit mehr als einer Datenbank-Klasse gearbeitet wird, aber Benutzername, Paßwort und Hostname in beiden Klassen gleich sind. Abhilfe kann mit einer der folgenden drei Methoden geschaffen werden:

17.12 Wie kann ich mit PHPLIB und Frames arbeiten?

Antwort von Kristian Köhntopp

Wenn man auf einer Seite die Sessionvariablen nicht ändert, dann braucht man dort den neuen Zustand nicht mit page_close() zu speichern, d.h. man kann das page_close() auf dieser Seite weglassen.

Meist sieht ein Frameset wie folgt aus:


+- frameset -----------+
|      |               |
| nav  | content       |
|      |               |
|      |               |
|      |               |
|      |               |
|      |               |
+------+---------------+

Wenn frameset und nav den Zustand nicht ändern, brauchen diese Frames auch kein page_close().

Verwendet man GET-Mode in Frames, dann muß die Session-ID durch die Frames nach unten durchgereicht werden.


<?php page_open(array("sess" => "Example_Session")) ?>
<frameset cols="150,*">
 <frame name="nav"     src="<?php $sess->purl("nav.php3" ?>">
 <frame name="content" src="<?php $sess->purl("content.php3"?>">
</frameset>

17.13 Internet Explorer: Meine Seiten werden nicht aktualisiert.

Antwort von Kristian Köhntopp

Internet Explorer cached sehr aggressiv. Nur wenn man das Caching einer Seite vollständig verbietet, werden Seiten korrekt aktualisiert. Dazu ist in der eigenen Unterklasse von Session in der Datei local.inc die Option allowcache auf no zu stellen.


class Example_Session extends Session {
        ...
        var $allowcache = "no"; ## ab Version 7.2c: "passive"
        ...
}

In PHPLIB 7.2c ist eine neue Einstellung passive für die Variable allowcache dazugekommen. In dieser Einstellung wird für den Netscape-Server das lokale Caching der Seiten erlaubt, für den Internet Explorer werden besondere Header gesendet, die diesem das Caching der Seite verbieten. Dies sollte in den meisten Fällen die Probleme lösen. Wo sie es nicht tun, muß weiterhin mit $allowcache = "no" gearbeitet werden.

17.14 Wie kann ich Reloads durch den User erkennen und verhindern?

Antwort von Kristian Köhntopp

Gewöhnlich macht man dies, indem man mit Session arbeitet und bei jedem Formular eine eindeutige ID "Challenge" als Hidden-Variable in das Formular mit aufnimmt, die man sich außerdem in einer lokalen Sessionvariablen auf dem Server merkt.

Wenn das Formular abgesendet wird, vergleicht man die gelieferte Challenge mit der lokal gemerkten Challenge und akzeptiert das Formular nur dann, wenn beide übereinstimmen.

Wenn das Formular verarbeitet wird, löscht man die Challenge in der Sessionvariablen nach Abschluß der Verarbeitung. Wird das Formular ein weiteres Mal versendet, liefert es die alte Challenge aus der Hidden-Variable, deren Gegenstück in der Sessionvariablen aber bereits gelöscht wurde.

Man kann dies sehr schön mit einem generischen Formularvalidator automatisieren, dann hat man gar keine Arbeit mehr damit.

17.15 Wie kann ich meine Variablen initialisieren und registrieren?

Antwort von Kristian Köhntopp

Wenn ein Benutzer zum ersten Mal Kontakt mit einer Webanwendung unter PHPLIB aufnimmt, dann kann dies auf einer beliebigen Seite geschehen - nicht notwendigerweise die Startseite der Anwendung. Eine Webanwendung muß jedoch zum korrekten Funktionieren eine Reihe von Variablen vorbelegen und diese beim Sessionmanagement registrieren. Für diesen und andere Zwecke (etwa: Sessionanalyse und Statistik) kennt die Session-Klasse von PHPLIB eine Variable auto_init. Diese Variable kann den Namen einer Include-Datei enthalten, die beim Start einer Session genau einmal geladen und ausgeführt wird. Per Konvention benennt man diese Datei setup.inc.

Der Inhalt von setup.inc wird von innerhalb einer Funktion geladen und ausgeführt. Daher ist es notwendig, alle zu registrierenden und initialisierenden Variablen mit der Anweisung global zu importieren. Um eine Variable $s auf einen Startwert 17 zu setzen und beim Sessionmanagement zu registrieren, würde setup.inc das folgende Aussehen haben:


<?php
  # $s importieren
  global $s;

  # $s auf den Startwert setzen.
  $s = 17;

  # Den Namen "s" beim Sessionmanagement registrieren.
  $sess->register("s");
 ?>

17.16 Wie kann ich auto_init benutzen, um Session-Statistiken zu erfassen?

Antwort von Kristian Köhntopp

Wenn man beim Start einer Session den Referer, die Adresse des Anwenders und seinen Browsertyp erfassen möchte, dann muß man eine Tabelle in der Datenbank erzeugen, die diese Daten speichern kann. Eine solche Tabelle kann zum Beispiel so aussehen:


CREATE TABLE session_stats (
  p_sid varchar(32) NOT NULL,
  p_name varchar(32) NOT NULL,
  p_start_time varchar(14) DEFAULT '' NOT NULL,
  p_referer varchar(250) NOT NULL,
  p_addr varchar(15) NOT NULL,
  p_user_agent varchar(250) NOT NULL,
  INDEX session_identifier (p_name, p_sid),
  INDEX start_time (p_start_time)
);

In dieser Tabelle werden neben den genannten Angaben noch die Session-ID des Anwenders und die Startzeit der Session erfaßt. Dies geschieht mit Hilfe eines Datenbankobjektes und der folgenden setup.inc-Datei:


<?php

# Importiere die zu erfassenden Daten
global $HTTP_REFERER, $REMOTE_ADDR, $HTTP_USER_AGENT;

# Erzeuge ein Datenbankobjekt zum Zugriff auf die Tabelle
$db  = new DB_Example;

# Datum des Eintrags bestimmen
$now = date("YmdHis", time());

# Query für den Eintrag generieren
$query = sprintf("insert into session_stats 
                  ( p_name, p_sid, p_start_time,
                    p_referer, p_addr, p_user_agent ) 
         values ( '%s', '%s',  '%s', '%s', '%s', '%s' )",
            $sess->name,
            $sess->id,
            $now,
            $HTTP_REFERER,
            $REMOTE_ADDR,
            $HTTP_USER_AGENT);

# Query absenden
$db->query($query);

?>

Erfaßt man diese Daten z.B. in einem Webshop und merkt man sich auch die Session-ID und das Datum einer Bestellung, kann man die Anzahl der Sessions ermitteln, die zu einer Bestellung geführt haben sowie die Länge dieser Sessions.

17.17 Wie kann ich eine Datei mit einem Paßwort schützen?

Antwort von Kristian Köhntopp

Um eine Datei mit einem Paßwort schützen zu können, muß in der Datei local.inc eine eigene Unterklasse von Auth erzeugt werden, die den Benutzernamen und das Paßwort des Benutzers prüft. Falls der Benutzer korrekte Angaben gemacht hat, muß diese Klasse die Benutzer-ID (uid) des Benutzers zurückliefern, andernfalls false. Außerdem muß die Klasse Funktionen enthalten, die einen Loginbildschirm malen.

PHPLIB kann gegen jede beliebige Datenbank mit Paßworten authentisieren, weil das Framework von PHPLIB keine Authentisierung selbst mitbringt. Es ist stattdessen die Aufgabe des PHPLIB-Anwenders, sich selbst eine solche Funktion zum Vergleichen von Paßworten zu schreiben. Zum Glück liefert PHPLIB in der standardmäßig gelieferten local.inc ein Beispiel für eine solche Funktion mit, die in diesem Beispiel gegen einen MySQL-Server authentisiert.

In der Unterklasse von Auth muß der PHPLIB-Anwender zwei Funktionen schreiben: Die Funktion auth_loginform() muß ein Loginformular malen, und die Funktion auth_validatelogin() muß die Daten aus dem Loginformular übernehmen und dann gegen eine Paßwort-Datenbank prüfen.

Der einfachste Fall sieht aus wie folgt:


class Beispiel_Auth extends Auth {
  var $classname = "Beispiel_Auth";

  var $lifetime  = 15;
  var $userpass  = array(
    "kris" => "test",
    "root" => "geheim"
  );

  function auth_loginform() {
    global $sess;
    global $_PHPLIB;

    include($_PHPLIB["libdir"] . "loginform.ihtml");

  }

  function auth_validatelogin() {
    global $username, $password;

    if ($this->userpass[$username] == $password)
      return $username;

    return false;
  }
}

Eine Anmeldung gegen diese Klasse gilt für 15 Minuten. Nach 15 Minuten ohne Seitenabruf ist der Benutzer automatisch ausgeloggt und muß sich erneut beim System anmelden. Dies wird durch die Variable $auth->lifetime bewirkt, die hier im Beispiel auf den Wert 15 gesetzt wird.

Der Beispielcode oben verwendet die Daten aus der Datei loginform.ihtml, um einen Loginbildschirm zu zeichnen. Diese Datei kann nach den Bedürfnissen der Anwendung umgestaltet werden, oder der Code in der Funktion auth_loginform() kann komplett neu geschrieben werden und so beliebige Loginformulare erzeugen.

Die Funktion auth_validatelogin() bindet die Formularvariablen username und password aus dem Loginformular mit Hilfe der Anweisung global ein, und prüft dann, ob der angegebene Benutzername in Verbindung mit dem Paßwort gültig ist. Dazu wird einfach in einem statischen Array nachgeschlagen, das zulässige Username-Paßwort Kombinationen auflistet.

Ist das angegebene Paßwort korrekt, wird einfach der Benutzername als interne User-ID zurückgegeben (PHPLIB verläßt sich an anderer Stelle darauf, daß eine User-ID den Typ varchar(32) hat). Andernfalls wird false geliefert.

Kompliziertere Vergleiche sind möglich. In der mitgelieferten local.inc zum Beispiel wird eine Anmeldung gegen eine MySQL-Datenbanktabelle durchgeführt. Die Logik hinter der Funktion ist jedoch dieselbe: Mit Hilfe des Benutzernamens und des Paßwortes wird in der Datenbanktabelle nachgeschlagen und eine User-ID oder false generiert.

Auf einer Seite kann nun der folgende Code eingebunden werden:


<?php
  page_open(array(
    "sess" => "Beispiel_Session",
    "auth" => "Beispiel_Auth"
  );
?>

Dieser Text ist nur sichtbar, wenn das Paßwort stimmt.
<?php
  page_close();
 ?>

Wieder wird die page_open()-Funktion verwendet, um PHPLIB die Namen der zu verwendenden Klassen bekannt zu machen. Außerdem ist wichtig, daß auf den Seiten page_close() aufgerufen wird, damit der Zähler für das automatische Timeout korrekt aktualisiert wird.

17.18 Wie kann ich mich gegen einen LDAP-Server authentisieren?

Antwort von Kristian Köhntopp

17.19 Wie kann ich Zugriffsrechte in PHPLIB definieren?

Antwort von Kristian Köhntopp

17.20 Wie kann ich einen Warenkorb realisieren?

Antwort von Kristian Köhntopp

17.21 Wie kann ich eine Menünavigation erzeugen?

Antwort von Kristian Köhntopp

17.22 Was sind Templates? Warum sind Templates nützlich?

Antwort von Martin Jansen

Templates bieten die Möglichkeit, Code und Design einer Seite sehr gut zu trennen. Beim Templateparsing wird eine reine HTML-Seite durch PHP analysiert und Platzhalter in der HTML-Seite durch Werte ersetzt, die PHP vorgibt (zum Beispiel mit Daten aus einer Datenbank). Nach der Analyse und dem Ersetzen der Platzhalter wird der geparste HTML-Code ausgegeben.

Bei der Benutzung von Templates kann der Webdesigner in aller Ruhe seine HTML-Seiten inkl. aller Grafiken, Tabellen, Stylesheets etc. erstellen, ohne sich Gedanken über die PHP-Skripte machen zu müssen, die eingesetzt werden sollen. Der PHP-Programmierer kann sich parallel dazu ganz auf die Entwicklung der Skripte konzentrieren und muss nur dafür sorgen, daß die Daten bereit stehen, um in das Template integriert zu werden.

Ein Beispiel für die Anwendung der Template-Klasse der PHPLIB:

Die HTML-Datei, die der Webdesigner erstellt hat, könnte zum Beispiel so aussehen:


<html>

<head>
  <title>Templates mit der PHPLIB</title>
</head>

<body>

<h2>
{UEBERSCHRIFT}
</h2>

<p>
{TEXT}
</p>

</body>

</html>

Das dazu passende PHP-Skript sieht so aus:


<?php
  
  /* Include-Datei der Template-Klasse */
  include("/pfad/zur/datei/template.inc");
  
  /* Template-Klasse initialisieren */
  $tpl = new Template;
  
  /* Datei bestimmen, die verwendet werden soll */
  $tpl->set_file(array("page" => "content.tpl"));
  
  /* Werte für Platzhalter festlegen */
  $tpl->set_var(array("UEBERSCHRIFT" => $ueberschrift,
                         "TEXT"         => $text));
  
  /* Template parsen */
  $tpl->parse("OUT","page")
  
  /* geparstes Template ausgeben */
  $tpl->p("OUT");
  
?>



18. Webserver und PHP

18.1 Apache: Kann ich PHP auch auf .html-Dateien anwenden?

Antwort von Kristian Köhntopp

Prinzipiell kann man Dateien mit beliebigen Endungen durch PHP verarbeiten lassen, wenn man Zugriff auf die Serverkonfiguration hat, also auch die Endung .html. Wenn die Modulversion von PHP verwendet wird, muß dazu


AddType application/x-httpd-php3 .html

in der httpd.conf eingetragen werden. Wird stattdessen die CGI-Version von PHP verwendet, muß der Eintrag


Action php3-script /cgi-bin/php
AddType php3-script .html

lauten. Solange die Modulversion von PHP verwendet wird und in den verarbeiteten Dateien keine PHP PIs (SGML Processing Instructions, die PHP einschalten: <?php und ähnlich) vorkommen, ist der Mehraufwand in CPU-Belastung und Zeitverbrauch kaum meßbar. Wird stattdessen die CGI-Version von PHP verwendet, entspricht der Mehraufwand dem Ausliefern jeder HTML-Seite durch ein CGI-Programm. Das kann beträchtlich sein.

18.2 Apache: Wie kann ich ein Verzeichnis mit einem Paßwort schützen?

Antwort von Kristian Köhntopp

Die Konfiguration des Webservers ist auch noch einmal ausführlich im Handbuch zum Apache Webserver beschrieben. Philipp Imhof hat auf seinem Webserver ein Script installiert, daß eine an den jeweiligen Beispielfall angepaßte Anleitung erstellt.

Im einfachsten Fall verwendet man HTTP Basic Authentication. Dazu ist mit dem Apache-Programm htpasswd eine Paßwortdatei anzulegen, die die Benutzernamen und Paßworte enthält. Optional kann man auch eine Gruppendatei anlegen, die pro Zeile den Namen einer Benutzergruppe und, durch einen Doppelpunkt getrennt, die Benutzer in dieser Gruppe enthält.


kris@valiant:~/www/kris.koehntopp.de > htpasswd -c etc/htpasswd kris
New password: test
Re-type new password: test
Adding password for user kris
kris@valiant:~/www/kris.koehntopp.de > htpasswd etc/htpasswd marit
New password: test
Re-type new password: test
Adding password for user marit
kris@valiant:~/www/kris.koehntopp.de > echo "users: kris marit" >>
etc/htgroup

Die Option -c steht dabei für create; die Paßwortdatei wird neu angelegt. Ohne die Option werden Benutzer zu einer existierenden Paßwortdatei zugefügt bzw. die Paßworte existierender Benutzer werden geändert. Im Beispiel wird weiterhin eine Gruppe users angelegt, der die Benutzer kris und marit angehören.

Ein Verzeichnis kann man nun durch das Anlegen einer .htaccess-Datei schützen (dazu muß AllowOverride AuthConfig in der httpd.conf für dieses Verzeichnis gesetzt sein) oder indem man die entsprechenden Konfigurationsanweisungen direkt in einen <Directory>-Block für dieses Verzeichnis in die httpd.conf einsetzt. Im einfachsten Fall sind dies die Anweisungen


AuthType Basic
AuthName MyRealm
AuthUserFile /home/www/servers/www.koehntopp.de/etc/htpasswd
AuthGroupFile /home/www/servers/www.koehntopp.de/etc/htgroup

require valid-user

Die Anweisung AuthType legt fest, auf welche Weise der Browser das Paßwort übermittelt - bei der sehr unsicheren Basic-Authentication wird es nahezu im Klartext übertragen. Für den zu schützenden Bereich muß mit AuthName ein Name festgelegt werden. Die Direktiven AuthUserFile/ und AuthGroupFile legen die Datenquellen für die Authentisierung fest.

Damit ein Anwender den geschützten Bereich betreten kann, muß er die in der require-Anweisung geforderten Bedingungen erfüllen. Im einfachsten Fall muß er einfach nur in der htpasswd-Datei stehen und das passende Paßwort wissen. Dies ist der Fall bei require valid-user. Mit komplizierteren Anweisungen kann man den Zutritt auch noch auf bestimmte Gruppen (require group users) oder auf bestimmte Benutzer (require user kk) einschränken.

In CGI PHP hat man in einem auf diese Weise geschützten Bereich Zugriff auf den Benutzernamen in der Variablen $REMOTE_USER und die verwendete Authentisierungsmethode in $AUTH_TYPE.


<?php
echo "AUTH_TYPE = $AUTH_TYPE<br>\n";
echo "REMOTE_USER = $REMOTE_USER<br>\n";
?>

Im Apache-Modul hat man in einem geschützten Bereich außerdem Zugriff auf die vollen Authentisierungsdaten.


<?php
echo "AUTH_TYPE = $AUTH_TYPE<br>\n";
echo "REMOTE_USER = $REMOTE_USER<br>\n";
$a = getallheaders();
$au = split(" ", $a["Authorization"], 2);
list($u, $p) = split(":", base64_decode($au[1]));
echo "Decodiert zu User $u, Password $p<br>\n";

18.3 Apache: Wie kann ich ein Verzeichnis mit PHP mit einem Paßwort schützen?

Antwort von Kristian Köhntopp

Dies ist in englischer Sprache ausführlich im PHP Handbuch beschrieben. Das Feature steht nur dann zur Verfügung, wenn PHP als Apache-Modul betrieben wird.


<?php
function check_pw($u, $p) {
  ...
}

if (!isset($PHP_AUTH_USER) or !check_pw($PHP_AUTH_USER,$PHP_AUTH_PW))
{
    Header("WWW-Authenticate: Basic realm=\"My Realm\"");
    Header("HTTP/1.0 401 Unauthorized");
    echo "Text to send if user hits Cancel button\n";
    exit;
} else {
    echo "Hello $PHP_AUTH_USER.<P>";
    echo "You entered $PHP_AUTH_PW as your password.<P>";
}
?>

Nur wenn PHP selbst die Authentisierung vornimmt, stehen die Variablen PHP_AUTH_USER und PHP_AUTH_PW zur Verfügung. Sie können verwendet werden, um den Benutzer in einer Datei, einer Datenbank oder einer anderen Datenquelle nachzuschlagen und das Paßwort zu überprüfen.

18.4 Kann ich mit CGI PHP ein Verzeichnis mit einem Paßwort schützen?

Antwort von Kristian Köhntopp

Nicht mit den Bordmitteln von PHP. Zwar kann man durch Senden eines Status-Header die Authentisierung auslösen, aber PHP übermittelt nicht die notwendigen Variablen an das Script zurück, wenn der Benutzer sich angemeldet hat.

Stattdessen sollte man PHPLIB und das Auth-Objekt verwenden.

18.5 Wie kann ich mit PHP die Bildschirmauflösung des Browsers herausfinden?

Antwort von Kristian Köhntopp

Das geht im allgemeinen Fall nicht. PHP wird auf dem Server ausgeführt, nicht im Browser des Zielsystems.

Falls der Browser des Zielsystems JavaScript kann, falls dieser Browser JavaScript nicht disabled hat, falls die Firewall auf dem Weg zum Zielsystem nicht JavaScript ausfiltert und falls man an geeigneter Stelle ein Formular statt eines Links verwendet, dann kannst man in diesem Formular die Auflösung des Zielsystem durch JavaScript ermitteln lassen und an seine Site zurücksenden lassen. Mit den übrigen Fällen (Auflösung des Zielsystems nicht bekannt) muß man dennoch fertig werden.

Die Tatsache, daß die Bildschirmauflösung des Zielsystems bekannt ist, hilft natürlich nicht beim Design, solange man nicht auch weiß

Man kann aus diesen Gründen davon ausgehen, daß eine auflösungsabhänige Darstellung auch dann auf mehr als der Hälfte der Zielsysteme nicht korrekt gerendert werden kann, auch wenn die Bildschirmauflösung des Zielsystems bekannt ist.

18.6 Wie kann ich das Caching einer Seite verhindern?

Antwort von Kristian Köhntopp

In HTTP 1.0 kann man das Caching einer Seite nur unvollständig steuern. Die ersten Webcaches haben die Lebensdauer von Seiten im Cache auf der Grundlage des Erzeugungsdatums geschätzt oder, wenn ein Expires-Header angegeben, diesen beachtet. Später ist der spezielle Header Pragma: no-cache eingeführt worden, um das Caching von Seiten durch Webcaches und Browser zu verbieten.

Erst mit HTTP 1.1 kann eine spezielle Cache-Steuerung hinzu, die zwischen privaten (browsereigenen) Caches und öffentlichen Caches unterschied. Über den besonderen Header Cache-Control kann man die Lebensdauer von Seiten in Caches steuern.

Microsoft kocht zusätzlich noch eine Spezialsuppe, indem sie für den MSIE 5.x spezielle Cache-Control Extensions definieren.

Um das Caching einer Seite zu erlauben, kann man den folgenden Code verwenden:


$expire = 15; # Lebensdauer der Seite im Cache in Minuten

$exp_gmt = gmdate("D, d M Y H:i:s", time() + $expire * 60) ." GMT";
$mod_gmt = gmdate("D, d M Y H:i:s", getlastmod()) ." GMT";

# HTTP 1.0
header("Expires: " . $exp_gmt);
header("Last-Modified: " . $mod_gmt);

# HTTP 1.1
header("Cache-Control: public, max-age=" . $expire * 60);

Um das Caching einer Seite auf private Caches zu begrenzen muß man Code wie diesen nehmen:


$expire = 15; # Lebensdauer der Seite im Cache in Minuten

$mod_gmt = gmdate("D, d M Y H:i:s", getlastmod()) ." GMT";

# HTTP 1.0 kennt keine privaten Caches, also nix cachen
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . $mod_gmt);

# HTTP 1.1
header("Cache-Control: private, max-age=" . $expire * 60);

# MSIE 5.x special
header("Cache-Control: pre-check=" . $expire * 60);

Um das Caching einer Seite zu verhindern, ist der folgende Code passend:


header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") ." GMT");
header("Cache-Control: no-cache");
header("Pragma: no-cache");
header("Cache-Control: post-check=0, pre-check=0");

Ein anderer Trick, mit dem man das Caching einer Seite gut verhindern kann, ist das Anhängen von Parametern an die URL einer Seite in der Form http://www.meinserver.de/bla.php?x=y oder das Einfügen von benutzerspezifischen Komponenten in die URL (in den Hostnamen, den Pfad oder den Dateinamen).

18.7 "Document contains no data"

Antwort von Kristian Köhntopp

Dies ist eine Netscape-Fehlermeldung, die dann auftritt, wenn der Webserver als Antwort auf einen Request genau null Bytes Antwort liefert. Wenn dies geschieht und die Seite eigentlich eine Ausgabe erzeugen sollte, dann hat möglicherweise der PHP-Interpreter ein Problem und stürzt mit einem Coredump ab.

In diesem Fall sollte man versuchen, den Fehler zu reproduzieren und das kürzestmögliche (!) Script, das den Fehler erzeugt, in den Bugreport auf mit einbinden.

18.8 Wie erzeuge ich mit PHP einen Redirect auf eine andere Seite?

Antwort von Kristian Köhntopp

Um einen Redirect zu erzeugen, muß man den HTTP-Header Location senden und dort die neue URL angeben. Zum Senden von HTTP-Headerzeilen verwendet man die PHP-Funktion header(). Diese Funktion kann nur dann verwendet werden, wenn PHP noch keinen HTTP-Body ausgegeben hat, wenn also weder Fehlermeldungen, Leerzeilen, Leerzeichen noch HTML ausgegeben worden sind.


# Redirect-Ziel
$target = "http://www.ziel.de/zielseite.html";

header("Location: $target");

RFC 2068 schreibt hier im Abschnitt 14.30 Location eine sog. absoluteURI vor, d.h. Anweisungen a la Location: index.html sind nicht standardkonform.

18.9 Was sind Sessions und warum sind sie nützlich?

Antwort von Kristian Köhntopp

Bei einer Session wird jedem Browser, der auf eine Webanwendung zugreift, eine Kennnummer gegeben, mit der man folgende Zugriffe dieses Browsers wiedererkennen kann. Auf dem Webserver werden unter dieser Kennnummer eine Reihe von PHP-Variablen gespeichert, die auf diese Weise von Seite zu Seite weitergereicht werden. Man erzielt damit einen ähnlichen Effekt wie mit <INPUT TYPE="hidden">-Variablen, die von Seite zu Seite weitergereicht werden, vermeidet jedoch eine Reihe von Nachteilen dieser Variablen:

Eine Webanwendung muß man sich aus zwei getrennten Teilen bestehend vorstellen. Der eine Teil, bestehend aus dem Webserver und allen anderen involvierten Rechnern auf dieser Seite der Firewall, ist grundsätzlich vertrauenswürdig. Daten, die von solchen Maschinen kommen, sind Bestandteil des durch den Serveradministrator kontrollierten Bereiches und daher mit großer Wahrscheinlichkeit korrekt und nicht kompromittiert.

Der andere Teil ist alles jenseits der Firewall, einschließlich des Browsers des Benutzers. Daten, die von dort kommen, sind nicht vertrauenswürdig:

Bei konventionellen Anwendungen werden mit jedem GET- oder POST-Request Parameter an den Server gesendet. Es handelt sich um sichtbare Formulardaten oder versteckte Formulardaten aus HIDDEN-Feldern oder statisch an URLs angehängte GET-Parameter sowie um Cookies. Diese Daten muß der Server jedes Mal wenn er sie erhält validieren und gegebenenfalls ablehnen. Das ist sehr, sehr schwierig zu machen und die meisten Anwendungen machen es nicht korrekt. Sie haben daher scheunentorgroße Zugangslöcher, die ein Anwender zum Hijacken der gesamten Webservermaschine benutzen kann. Diese Maschine wird dann zum Einfallstor für das gesamte Subnetz, in dem sich diese Maschine physikalisch befindet.

Bei Anwendungen mit Sessions wird bei jedem Request die Session-ID gesendet und diese hat eine Form, die sie schwer manipulierbar macht. Manipuliert der Benutzer die Session-ID, so erwischt er mit an Sicherheit grenzender Wahrscheinlichkeit eine ungültige Session-ID und startet so eine neue Session. Die Anwendung initialisiert sich dann korrekt und der User startet, als hätte er die Anwendung soeben frisch aufgerufen. Manipulation der Session-ID wird also von der Anwendung erkannt und führt zu einem definierten und unschädlichen Verhalten.

Außerdem erhalten auch Anwendungen mit Sessions ebenfalls Daten aus Formularen. In der Regel sind dies keine HIDDEN-Felder, denn der Zustand wird nun auf dem Server als Teil der Session gehalten. Stattdessen handelt es sich ausschließlich um sichtbare Formulardaten. Diese müssen beim Übergang aus dem Kontrollbereich des Benutzers in die Session natürlich validiert werden. Danach verbleiben sie jedoch in der Session und können als vertrauenswürdig angesehen werden.

Vertrauen bedeutet in diesem Kontext, daß die Daten mindestens die zugesicherten Eigenschaften haben, auf die bei der Validierung getestet worden ist. Die Anwendung darf natürlich nicht darauf vertrauen, daß die Daten Eigenschaften haben, die nicht validiert worden sind. Wenn man bei der Übernahme von Formulardaten in Sessiondaten einen Benutzernamen auf \w{3,8} testet, dann kann man in der Anwendung zwar darauf vertrauen, daß der Benutzername keine bösen Sonderzeichen enthält, nicht leer ist und in die Datenbankfelder paßt, aber nicht darauf vertrauen, daß der Benutzername auch die Eigenschaft "eindeutig" und "noch nicht vergeben" erfüllt.

In PHP3 lassen sich Sessions mit Hilfe von PHPLIB erzeugen. In PHP4 kann man alternativ auch die eingebauten Sessions der Sprache verwenden.

18.10 Wie kann ich mit PHP WAP-Seiten erzeugen?

Antwort von Kristian Köhntopp

Die Seite muß den korrekten Content-Type bekommen. Dies erreicht man, indem man sein Script mit einer passenden header()-Anweisung beginnen läßt.


header("Content-Type: application/vnd.wap.wml");

18.11 Wie bringe ich eine Suchmaschine dazu, meine Seiten zu indizieren?

Antwort von Kristian Köhntopp

Lies den Artikel von Tobias Ratschiller in der Suchfibel zu diesem Thema.


19. Content Management Systeme

19.1 Was ist ein Content Management System? Warum ist es nützlich?

Antwort von Björn Schotte

Laut ContentManager ist ein Content Management System ein "Softwaresystem für das Administrieren von Webinhalten mit Unterstützung des Erstellungsprozesses basierend auf der Trennung von Inhalten und Struktur".

Bei sehr vielen Websites kommt es nicht darauf an, dass man besonders tolle PHP-Applikationen erstellt. Viel wichtiger ist, dass man das Tagesgeschäft erledigen kann, ohne durch fehlerhafte Programme, umständliche Bedienungen, Heranziehen von Softwareentwicklern vom normalen Ablauf gestört zu werden. Ein CMS unterstützt dieses Vorhaben, indem es eine Website in mehrere Bereiche aufteilt und über eine (meist webbasierte) Oberfläche den einzelnen Mitarbeitern zugänglich macht.

Das ermöglicht es auch nicht mit HTML versierten Mitarbeitern, Inhalte der Website zu pflegen. Den Rest, also die Integration des Inhalts in die Struktur, erledigt das CMS. Ein anderer Mitarbeiter, der zum Beispiel in HTML sehr fit ist, wird Zugriff auf das Layoutmodul des CMS haben und dort sogenannte HTML-Templates pflegen. In vielen CMSen sind diese Templates normaler HTML-Code, bei dem durch Schlüsselwörter definiert wird, an welcher Stelle welcher Inhalt gesetzt werden soll.

Sehr nützlich bei einem CMS sind auch noch die verschiedenen Zugriffsrechte für einzelne Benutzer(gruppen). Das macht eine Kontrolle möglich, zum Beispiel das die Sekretärin nur im Inhaltsbereich Daten eingeben darf, aber keinen Zugriff auf das Layoutmodul des CMS hat.

Der Nutzer, der die Website oder Teile davon pflegt, kommt also gar nicht mehr in Kontakt mit z.B. FTP-Programmen. Scheinbar komplizierte Technik wird in eine übersichtliche Oberfläche verpackt, damit auch weniger versierte Nutzer die Inhalte pflegen können. Module wie zum Beispiel Mediendatenbanken, die Bilder, Sounds, Dateien etc. verwalten, machen eine Pflege selbst komplexer Sites zum Kinderspiel.

19.2 Welche PHP-basierten Content Management Systeme gibt es?

Antwort von Björn Schotte

Systeme, die kostenlos zur Verfügung stehen:

Kommerziell erhältliche Systeme:


20. Häufig nachgefragte Standardscripte

20.1 Wie kann ich eine schummelsichere Abstimmung codieren?

Antwort von Kristian Köhntopp

Man kann keine schummelsichere Abstimmung im Web realisieren, die nicht unfair berechtigte Abstimmende ausschließt, außer man treibt sehr großen Aufwand.

Typische Schutzmechanismen, die jedoch nur Pseudosicherheit geben, sind : Die Seite nimmt Parameter per Post, prüft den Referer und versucht einen Cookie zu setzen. Wer den Cookie hat, kann nicht mehr abstimmen. Zum Mogeln muß man der Seite also die richtigen Parameter aus dem Formular mit einem Post füttern, darf den Referer nicht vergessen und darf den Cookie nicht annehmen. Das war alles.

Eine etwas kompliziertere Absicherung wäre ein Formular, das als hidden-Parameter eine Challenge hat, die auch auf dem Server als Session-Variable verbleibt. Ein Abstimmungsergebnis wird dann für die aktuelle Session nur angenommen, wenn die richtige Challenge mit in den Parametern ist. Das macht das Schummeln per Script ein wenig schwieriger, aber nicht unmöglich (mit LWP in Perl oder libwww in C problemlos möglich, mit PHP3 ein Code mit den preg-Funktionen.

Mit SSL-Browser-Zertifikaten auf der Clientseite wäre es noch schwieriger zu fälschen, aber da gelangt man langsam in Regionen, wo es für den Wahlveranstalter wirklich teuer wird.

Auf der Basis der IP-Nummern kann man nicht arbeiten, da viele Benutzer dieselbe IP-Nummer zu haben scheinen, wenn sie über Proxy-Server hereinkommen oder Benutzer auf einem Mehrbenutzer-Rechner sind (unfairer Ausschluß). Andererseits ist es für einen geübten Hacker oder einen Provider sehr leicht, von einer ganzen Reihe unterschiedlicher IP-Nummern mehrfach abzustimmen.

20.2 Wie kann ich einen HTTP POST-Request absenden?

Antwort von Kristian Köhntopp

Das Script muß einen Socket mit der Funktion fsockopen() zum Zielserver öffnen und auf diesem Socket dann einen HTTP POST-Request simulieren.

Anbei ein vollständiges Beispiel, das mit CGI-PHP auf der Unix-Kommandozeile verwendet werden kann. Das Script fälscht Einträge in einer Abstimmung auf dem Host www.linux.com, wo es für PHP als beste Scriptsprache stimmt.


#! ./php -q
<?php

function PostToHost($host, $path, $referer, $data_to_send) {
  $fp = fsockopen($host,80);
  printf("Open!\n");
  fputs($fp, "POST $path HTTP/1.1\n");
  fputs($fp, "Host: $host\n");
  fputs($fp, "Referer: $referer\n");
  fputs($fp, "Content-type: application/x-www-form-urlencoded\n");
  fputs($fp, "Content-length: ".strlen($data_to_send)."\n");
  fputs($fp, "Connection: close\n\n");
  fputs($fp, "$data_to_send\n");
  printf("Sent!\n");
  while(!feof($fp)) {
      $res .= fgets($fp, 128);
  }
  printf("Done!\n");
  fclose($fp);

  return $res;
}

$data = "pid=14&poll_vote_number=2";

printf("Go!\n");
$x = PostToHost(
              "www.linux.com",
              "/polls/index.phtml",
              "http://www.linux.com/polls/index.phtml?pid=14",
              $data
);

20.3 Wie kann ich eine Volltextsuche realisieren?

Antwort von Kristian Köhntopp

Eine Suchmaschine fuer Volltext wird man in den meisten Faellen nicht in PHP und mit einer SQL-Datenbank programmieren, sondern sinnvollerweise für diesen Anwendungszweck spezialisierte Software verwenden. SQL-Datenbanken sind nur dann optimal eingesetzt, wenn die Art der Daten und die Art der Anfragen den Einsatz von Indices möglich machen. Die meisten SQL-Datenbanken sind von Haus aus nicht besonders gut eingerichtet, um Indices über Volltext verwalten zu können: Zum einen können viele SQL-Datenbanken BLOB und TEXT-Felder gar nicht indizieren. Zum anderen können die meisten Datenbanken vorhandene Indices nicht nutzen, wenn der Suchausdruck nicht ohne Wildcard am vorderen Rand der Spalte verankert ist, d.h. wenn die Suche die Form LIKE '%suchwort' hat. MySQL bietet ab der Version 3.23.23 die Möglichkeit, einen Volltextindex anzulegen. Eine Anleitung dazu befindet sich in Wie realisiere ich eine Volltextsuche mit MySQL?

Einige populäre Volltextsuchmaschinen:

In der Newsgroup wurde der folgende Text zum Studium empfohlen: Managing Gigabytes; Compressing and Indexing Documents and Images, Ian H. Witten, Alistair Moffat, Timothy C. Bell; Morgan Kaufmann Publishers; ISBN: 1558605703

20.4 Wie kann ich mit PHP News lesen und schreiben?

Antwort von Kristian Köhntopp

Mit Hilfe der IMAP-Bibliothek und IMAP-Funktionen kann man auch auf Newsserver zugreifen.

Auf den Webseiten von Floh findet man einen in PHP geschriebenen Newsclient. Dieser verwendet jedoch nicht die IMAP-Funktionen, sondern bildet diese Funktionen manuell nach, da das IMAP-Modul bei vielen PHP-Installationen von Webhostern nicht verfügbar ist.

20.5 Wie kann ich einen Onlineshop mit PHP realisieren?

Antwort von Kristian Köhntopp

Fertige Onlineshoplösungen in PHP:

20.6 Wie kann ich die IP des Users erfahren?

Antwort von Martin Jansen

Dieses Skript greift auf die Umgebungs-Variablen zurück, um die IP des Users zu bestimmen, der die Anfrage sendet. Der Variablen $host wird gleichzeitig der anhand der IP aufgelöste Hostname zugewiesen.


<?php
/* IP bestimmen */
$ip = getenv("REMOTE_ADDR");

/* IP auflösen und Host bestimmen */
$host = gethostbyaddr($ip);
?>

20.7 Wie kann ich ein JPEG-Bild verkleinern?

Antwort von Kristian Köhntopp

Auf http://www.phpwelt.de/tutorials/tutorials.php?tid=16 wird dies beschrieben.

20.8 Wie kann ich die Performance zweier Befehle vergleichen?

Antwort von Martin Jansen


<?php
  function start_timer($event) {
    printf("timer: %s<br>\n", $event);
    list($low, $high) = split(" ", microtime());
    $t = $high + $low;
    flush();

    return $t;
  }

  function next_timer($start, $event) {
    list($low, $high) = split(" ", microtime());
    $t    = $high + $low;
    $used = $t - $start;
    printf("timer: %s (%8.4f)<br>\n", $event, $used);
    flush();

    return $t;
  }

  $t = start_timer("start Befehl 1");

  /* Hier den ersten Befehl einfuegen */

  $t = next_timer($t, "start Befehl 2");
  
  /* Hier den zweiten Befehl einfuegen */

  $t = next_timer($t, "finish");
?>

Möchte man zum Beispiel den Performance-Unterschied zwischen mysql_fetch_row und mysql_fetch_array bestimmen, fügt man die beiden Befehle an die beiden mit Kommentaren versehenen Stellen im Skript ein.

Antwort von Sebastian Bergmann

Eine Alternative zur oben dargestellten Methode der Performance Messung bieten die PEAR Klassen Benchmark_Timer und Benchmark_Iterate.

Die Benchmark_Timer Klasse stellt die benötigte Funktionalität zur Verfügung, um Marken zu setzen, und die zwischen zwei Marken verstrichene Zeit zu berechnen.


<?php
  // PEAR::Benchmark_Timer inkludieren
  require_once "PEAR/Benchmark/Timer.php";

  // Timer Instanz erzeugen
  $Timer = new Benchmark_Timer;

  // Marke "Beginn For-Schleife" setzen
  $Timer->setMarker("Beginn For-Schleife");

  for ($i = 0; $i < 10000; $i++) { }

  // Marke "Ende For-Schleife" setzen
  $Timer->setMarker("Ende For-Schleife");

  // Zeit zwischen den beiden Marken berechnen
  print $Timer->timeElapsed("Beginn For-Schleife","Ende For-Schleife");
?>

Die Benchmark_Iterate Klasse leitet sich von der Benchmark_Timer Klasse ab und ermöglicht die wiederholte Ausführung einer Funktion, um so die mittlere Ausführungsgeschwindigkeit derselben zu ermitteln.


<?php
  // Funktion, die wir benchmarken wollen
  function foo()
  {
    print "bar<br>";
  }

  // PEAR::Benchmark_Iterate inkludieren
  require_once "PEAR/Benchmark/Iterate.php";

  // Benchmark Instanz erzeugen
  $Benchmark = new Benchmark_Iterate;

  // Benchmark ausführen
  $result = $Benchmark->run("foo", 100);

  // Ergebnis ausgeben
  print "Mittlere Ausführungsgeschwindigkeit: " . $result["mean"];
?>

20.9 Wie kann ich den Inhalt eines Verzeichnisses samt dem Inhalt aller Unterverzeichnisse ausgeben?

Antwort von Guido Haeger

Um nicht nur den Inhalt des aktuellen Verzeichnisses, sondern auch den Inhalt aller Unterverzeichnisse ausgeben zu können, muss man eine rekursive Funktion verwenden. Diese ruft sich bei Bedarf selbst auf. Im nachfolgenden Beispiel durchläuft die Funktion show_dir jeweils das aktuelle Verzeichnis. Wird Datei gefunden, wird der Dateiname ausgegeben. Findet die Funktion ein Verzeichnis, dann wird der Verzeichnisname fett ausgegeben und die Funktion ruft sich mit dem Unterverzeichnis als Parameter selbst wieder auf.


<?PHP

function show_dir($dir, $pos=2)
{
     if($pos == 2)
     {
          echo "<hr><pre>";
     }
     
     $handle = @opendir($dir);
     while ($file = @readdir ($handle)) 
     {
        if (eregi("^\.{1,2}$",$file))
        {
          continue;
        }
   
     if(is_dir($dir.$file))
        {
          printf ("% ".$pos."s <b>%s</b>\n", "|-", $file);
          show_dir($dir.$file."/", $pos + 3);               
        }
        else
        {
          printf ("% ".$pos."s %s\n", "|-", $file);
        }
     }
     @closedir($handle); 
     
     if($pos == 2)
     {
          echo "</pre><hr>";
     }
     
}

show_dir("special/");

?>

20.10 Wie kann ich aus einem Zahlenbereich von x bis y, zufällig n Zahlen auswählen, so daß keine Zahl doppelt vorkommt?

Antwort von Guido Haeger

Häufig werden dazu Schleifen programmiert, die pro Durchlauf eine Zufallszahl aus dem vorgegebenen Bereich ermitteln. Danach wird geprüft, ob diese Zufallszahl schon ermittelt wurde. Ist das der Fall, wird die Schleife erneut durchlaufen. Da insbesondere bei PHP3 die Abarbeitung von Schleifen nicht besonders effizient ist, kann es bei dieser Methode sehr lange dauern, bis die gewünschte Anzahl eindeutiger Zufallszahlen ermittelt wurde.


<?PHP

function generate_numbers($min, $max, $anz)
{
     $array = range($min, $max);
     srand ((double)microtime()*1000000);
     for($x = 0; $x < $anz; $x++)
     {
          $i = rand(1, count($array))-1;
          $erg[] = $array[$i];          
          array_splice($array, $i, 1);  
     }
     return $erg;
}

// 5 eindeutige Zahlen im Bereich von 1 bis 100 ermitteln
$zufalls_array = generate_numbers(1, 100, 5);
echo join("; ", $zufalls_array);

?>

20.11 Wie kann ich zählen, wie oft auf einen Link geklickt wurde?

Antwort von Martin Jansen

Dem Skript liegt folgende Struktur der MySQL-Tabelle zugrunde:


mysql> describe counter;
+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| id    | int(10) unsigned |      | PRI | 0       | auto_increment |
| url   | char(255)        |      |     |         |                |
| count | int(11)          |      |     | 0       |                |
+-------+------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

Das Feld url enthält die URL, die aufgerufen wird. Das Feld count enthält die Anzahl der Klicks auf url.


<?php
     
// Zugangsdaten fuer die Datenbank
// Diese sollten der Sicherheit halber
// in ein Verzeichnis außerhalb des
// Document-Root ausgelagert werden.

$host     =    "localhost";
$user     =    "user";
$pass     =    "demo_password";

$datab    =    "demo_db";
$table    =    "counter";

// Verbindung zum MySQL-Server aufbauen
$db = @mysql_connect($host,$user,$pass);

if ($db) {          
     if (@mysql_select_db($datab,$db)) {
          // Eintrag fuer die per GET uebergebene URL um 1 erhoehen.
          $query = "UPDATE $table SET count = count + 1 WHERE url = '$url'";
          $result = @mysql_query($query);
     }
}

// Auf uebergebene URL weiterleiten
Header("Location: ".$url);
     
?>

Anwendungsbeispiel:

<a href="count.php?url=http://www.martin-jansen.de">Link</a>

Als Parameter für die Datei count.php wird die URL übergeben, auf die weitergeleitet werden soll. In count.php wird nun der Datensatz in der Tabelle, der $url als Wert für das Feld url enthält um 1 erhöht und es wird auf die neue URL weitergeleitet.

20.12 Wie kann ich das Datum der letzten Änderung einer Datei erfahren?

Antwort von Daniel T. Gorski


<?php
// Modifikationszeit von (Wie heiße ich?)
$unixTime = filemtime($HTTP_SERVER_VARS["PATH_TRANSLATED"]);

if ($unixTime) {
  echo "Letzte Änderung: ".date("d M Y, H:i:s", $unixTime);
} else {
  echo "Datei existiert nicht! Falscher Pfad?";
}
?>

Möchte man unter UNIX-Derivaten das Datum der letzten Änderung der zugehörigen Inodes (z.B. Änderungen der Filesystem-Rechte) erfahren, so ist die Funktion filectime() zu benutzen.

20.13 Wie kann ich ein Forum mit PHP realisieren?

Antwort von Martin Jansen

Die folgenden Foren sind in PHP geschrieben:

20.14 Wie biete ich meine Seiten mehrsprachig an?

Antwort von Martin Jansen

Auf der Seite http://www.php-center.de/artikel/i18n.php3 findet man einen Artikel, der sich mit der Internationalisierung von PHP-generierten Seiten befasst.

20.15 Wie kann ich ermitteln, wieviele Besucher gerade meine Seite betrachten?

Antwort von Markus Dobel

Es gibt keine zuverlässige Methode, dies herauszufinden, da HTTP ein zustandsloses Protokoll ist und daher keine Stati "eingeloggt, ausgeloggt" wie bei FTP, Telnet etc. vorhanden sind. Der Client (Browser) baut eine Verbindung zum Webserver auf, fordert eine Seite mit all ihren Elementen (Bildern, JavaScript etc.) an und beendet danach die Verbindung wieder. Es entsteht also immer nur eine punktuelle, temporär andauernde Verbindung zwischen dem Webserver und dem Client des Anwenders.

Mit Hilfe von PHPlib und den sess_*-Funktionen von PHP4 kann man einen Besucher zwar wiedererkennbar machen, man kann damit jedoch nur feststellen, wann ein Besucher zuletzt eine Seite angefordert hat.

Wie lange er diese liest und daher noch "auf der Seite online" ist, kann man nicht herausfinden. Er könnte 30 Minuten an dem Text einer Seite lesen oder aber auch das Browserfenster direkt nach Anforderung der Seite schließen; der Server weiss nur, wann er die Seite wem ausgeliefert hat.

Es gibt einige Denkansätze, wie man diese fehlende Funktionalität nachbauen könnte, welche jedoch alle von der Zusammenarbeit mit dem Browser des Besuchers abhängig sind, um halbwegs zuverlässig zu arbeiten und/oder nur Näherungsweise an die reellen Gegebenheiten herankommen.

Ein paar davon bauen auf Javascript auf, welches man von vornherein ausklammern sollte, da nicht jeder Browser Javascript beherrscht und auch angeschaltet hat. Diese Methode ist also unzuverlässig.

Eine weitere Idee basiert darauf, einen kleinen "Blindframe" auf der Seite zu platzieren, welcher regelmäßig automatisch per META-Tag neugeladen wird. Auch wird darauf vertraut, dass der automatische Refresh wirklich bei jedem Besucher ausgeführt wird und man verpflichtet seine Besucher dazu, einen Browser zu benutzen, der Frames beherrscht. Darüberhinaus vergrault man sich auf Dauer einige Besucher, die die ständige Netzaktivität des Browsers irritiert oder nervt.

Zuletzt könnte man noch auswerten, wieviele Benutzer innerhalb der letzten x Minuten eine Seite angefordert haben und daher Annahmen darüber treffen, ob diese Besucher noch da sind. Auch dies ist keine zuverlässige Aussage.

Zusammenfassend lässt sich also sagen, dass die Aussage "x User online" (wie sie auf vielen Sites zu finden ist) reines Blendwerk sind. Es ist technisch nicht möglich, diese Aussagen zu treffen.

20.16 Wie erstelle ich eine Webmail-Oberfläche mit PHP?

Antwort von Martin Jansen

Für diesen Zweck gibt es folgende fertige Skripte:

20.17 Wie überprüfe ich Links auf ihre Gültigkeit?

Antwort von Johannes Frömter

Indem man mit fsockopen() eine Verbindung zum Webserver herstellt, einen HTTP-HEAD-Request auf die zu überprüfende Ressource absetzt und die Antwort auswertet.

Wer dies nicht selber programmieren will, kann Scripts wie z.B. die Funktion phpLinkCheck benutzen.


21. Guter Code

21.1 Vermeide globale Variablen.

Antwort von Kristian Köhntopp

In PHP Version 3 werden eine ganze Menge Daten aus der Prozessumgebung und aus dem Internet in den globalen Namensraum importiert. Das Verhalten von PHP ist dabei leicht unterschiedlich, je nachdem, ob das Modul oder die CGI-Version verwendet werden.

Es gelten die folgenden Regeln:

Ein anderer Fehler, der leicht ausbeutbar ist, ist die Verwendung nicht initialisierter globaler Variablen. Durch Einsetzen von Werten als GPC-Parameter kann ein Angreifer diese Variablen mit beliebigen Startwerten vorbelegen, so daß die Annahme nicht zutreffend ist, daß uninitialisierte Werte immer 0, "" oder false sind. Durch Hochsetzen des Warning Levels (siehe error_reporting und error_reporting()) kann man solche Probleme zuverlässig aufspüren.

In PHP Version 4 ist der globale Namensraum wesentlich besser kontrollierbar. Man kann das automatische Importieren von externen Variablen in den globalen Namensraum gut verhindern. Werte finden sich stattdessen in getrennten, gegenseitig nicht überschreibbaren Namensräumen (vordefinierten Hashes mit unterschiedlichen, nichtüberlappenden Namen). Der Default ist jedoch PHP Version 3-Kompatibilität - man muß die Default-Importe zunächst einmal abschalten (siehe register_globals).

Der Namensraum von Funktionen und Klassen ist wesentlich besser kontrollierbar. Es ist daher empfehlenswert, so viel Funktionalität als möglich in Funktionen oder Klassen abzulegen und dort kontrolliert Variablen als Funktionsparameter oder über global() zu importieren.

21.2 Halte Code links. Verwende Wächter statt Schachtel-if.

Antwort von Kristian Köhntopp

In der strukturierten Programmierung erzeugt man Schleifen ohne Schleifenkurzschlüsse mit continue oder break und Funktionen mit genau einem Funktionsausgang durch return. Dadurch entsteht häufig Code mit sehr vielen geschachtelten Abfragen.

Meistens versucht man vor der eigentlichen Nutzlast einer Schleife oder einer Funktion eine Reihe von Vorbedingungen zu testen, die für den erfolgreichen Einsatz der Nutzlast sichergestellt sein müssen. Der generierte Code sieht dann wie folgt aus:


if (vorbedingung) {
  if (vorbedingung2) {
    if (vorbedingung3) {
      doit(); // Nutzlast
    } else {
      handle_error3();
  } else {
    handle_error2();
  }
} else {
  handle_error();
}

Dies ist sehr schwer zu lesen und zu verstehen, weil der eigentliche Zweck der Funktion tief geschachtelt und sehr unübersichtlich versteckt ist. Geübte Programmierer verwenden stattdessen absichtlich die in der strukturierten Programmierung verpönten Schleifenkurzschlüsse und frühzeitigen Funktionsausstiege in einer bestimmten Form, um besser lesbaren Code zu schreiben:


if (!vorbedingung)
  handle_error();

if (!vorbedingung2)
  handle_error2();

if (!vorbedingung3)
  handle_error3();

doit; // Nutzlast

Dieser Code skaliert sich besser: Egal wieviele Vorbedingungen zu erfüllen sind - die Einrücktiefe bleibt konstant. Außerdem steht der normale Fall jetzt in Falllinie und der Fehlercode ist als Ausnahme eingerückt und zur Seite gedrängt.

Ein praktisches Beispiel: Der folgende Code zum Durchlesen eines Verzeichnisses


$d = dir("d:/logfiles");
while($entry=$d->read()) {
  if (($entry != ".") && ($entry != "..")) {
    doit();
  }
}
$d->close();

wird durch die Umstellung zu


$d = dir("d:/logfiles");
while ($entry = $d->read()) {

  if ($entry == "." or $entry == "..")
        continue;

  doit();
}
$d->close();

21.3 'or' und 'and' sparen Klammern.

Antwort von Kristian Köhntopp

PHP hat neben den klassischen C-Operatoren && und || auch die gleichbedeutenden Operatoren and und or. Diese haben jedoch eine andere, niedrigere Priorität, so daß die obligatorischen Klammern um verknüpfte Bedingungen entfallen können. So wird


if (($bla == 0) && ($fasel == 1)) { ... }

zum gleichbedeutenden


if ($bla == 0 and $fasel == 1) { ... }

21.4 Prüfe importierte Parameter. Traue niemandem.

Antwort von Kristian Köhntopp

Parameter, die als GET, POST oder COOKIE-Werte in das lokale Programm gekommen sind, sind nicht vertrauenswürdig. Techniken, die Variablen als GET-Parameter am Ende einer URL durchschleifen oder als HIDDEN-Variablen in Formularen weitergeben (Ping-Pong-Technik), sind daher prinzipbedingt fehlerhaft und können niemals sicher sein.

Auch dürfen Variablen aus GPC-Quellen niemals ungeprüft direkt verwendet werden, sondern müssen zwingend erst einmal auf Ungefährlichkeit geprüft werden. Bei Parametern, die gegen die Empfehlung per Ping-Pong weitergereicht werden, muß dieser Test jedesmal (auf jeder Seite) wieder gemacht werden, da ja ein Anwender oder ein böswilliges Programm die Werte inzwischen geändert haben könnte.

Eine Webanwendung kontrolliert nur den Bereich bis zur Firewall. Der Browser des Anwenders befindet sich auf der anderen Seite der Firewall und ist daher als Feindesland anzusehen. Daten, die von dort kommen, können unerwartete Werte oder unerwartete Formate haben. Insbesondere kann eine Webanwendung keine Annahmen über bestimmte Eigenschaften des Browsers machen, wie zum Beispiel "JavaScript wird zuverlässig ausgeführt", "Cookies werden akzeptiert und verändern ihre Werte nicht spontan" oder "Referer-Header sind vertrauenswürdig".

Es gelten daher die folgenden Empfehlungen:

Zusammenfassend: Traue niemandem. Validiere allen Input oder stirb.

21.5 Von HTML zu PHP: Schreibe Formularverarbeitungen in Normalform.

Antwort von Kristian Köhntopp

Da jede HTML-Datei auch ein gültiges, bedeutungsgleiches PHP-Programm ist, existiert ein einfacher und systematischer Weg, um von einem HTML-Formular zu einem PHP-Programm zu kommen, das dieses Formular bearbeitet. Hält man sich an diesen Weg, wird das resultierende Formular zugleich ein bestimmtes Format haben.

Der erste Schritt ist die Entwicklung eines reinen HTML-Formulares, das die zu verarbeitenden Daten abfragt.

In einem zweiten Schritt wird man aus diesem Formular ein sogenanntes Affenformular machen, indem man dafür sorgt, daß das Formular sich selbst aufruft und seine alten Werte immer wieder einsetzt. Es heißt Affenformular, weil eine Million Affen dieses Formular eine Million mal aufrufen können, ohne etwas zu bewirken.


<form action="<?php print $PHP_SELF ?>">
<input type="text" 
       name="textfeld" 
       value="<?php print $textfeld ?>"><br>
<input type="submit"
       name="do_form_x"
       value="Ausführen">
</form>

Das Affenformular enthält an zwei Stellen PHP-Code: Die Action des Formulares ist das Formular selbst und durch das Einsetzen von $PHP_SELF kann das Formular seine eigene Adresse bestimmen und sich so selbst aufrufen. Und außerdem werden die Defaultwerte der verschiedenen Formularfelder durch PHP wieder mit den Ausgangswerten belegt. Wenn das Formular also abgesendet wird und die Eingabewerte nicht korrekt sind, wird das Formular wieder dargestellt und die alten Werte werden als Standardwerte wieder eingesetzt, damit der Anwender sie korrigieren kann, ohne sie noch einmal eingeben zu müssen.

Der dritte Schritt besteht dann darin, eine Reihe von Funktionen zu codieren, die die Werte aus diesem Formular validieren und ggf. die passenden Fehlermeldungen erzeugen, die dann an den geeigneten Stellen wieder in das Formular eingesetzt werden. Das Endresultat muß dann eine Variable haben, an der entscheidbar ist, ob der Formularinhalt gültig ist oder nicht.


<?php
#
# Funktion zum Drucken von Fehlermeldungen
#
function errmsg($msg) {
   ?>
   <font color="#ff0000"><b><?php print nl2br($msg) ?></b></font>
   <?php
}
 
#
# Überprüft Eingabewerte für $textfeld auf Korrektheit.
function validate_textfeld($val) {
   $msg = "";
   if (strlen($val) < 3)
      $msg .= "Die Eingabe muß mindestens 3 Zeichen lang sein.\n";
   
   if (ereg("[ \t]", $val))
      $msg .= "Die Eingabe darf keine Leerzeichen "
             ."oder Tabulatoren enthalten.\n";

   return $msg;
}

#
# Für jedes Formularfeld werden nun ein oder mehrere
# Validatoren aufgerufen und das Ergebnis der Überprüfung
# gemerkt.
#
$valid = true;
if (isset($textfeld)) {
   $error["textfeld"] = validate_textfeld($textfeld);
   if ($error["textfeld"] != "")
      $valid = false;
}
  
?>
<form action="<?php print $PHP_SELF ?>">
<input type="text"
       name="textfeld"
       value="<?php print $textfeld ?>"><br>
<?php
# Ggf. Fehlermeldung ausdrucken.
if ($error["textfeld"] != "")
   print errmsg($error["textfeld"]);
?>
<input type="submit"
       name="do_form_x"
       value="Ausführen">       
</form>
<hr>
<?php if ($valid and isset($do_form_x)): ?>
<!-- Nutzlast -->
<?php endif; ?>

Schließlich kann man im vierten Schritt daran gehen, die validierten Formulardaten einer Bearbeitungsfunktion zu übergeben und diese Funktion die eigentliche Arbeit machen zu lassen. Das Ergebnis dieser Verarbeitung wird in den meisten Fällen unterhalb des Formulars dargestellt werden, so daß man mit den Eingabedaten oben gleich die nächste Abfrage starten kann.

An dieser Stelle hat man ein funktionierendes Formular und zwei Baustellen, an denen man weiterarbeiten kann: Zum einen muß man sich Gedanken darüber machen, welche Funktionalität aus diesem Formular an anderer Stelle so auch wieder verwendet werden könnte und sollte sich überlegen, ob man nicht eine Klasse schreiben möchte, in die man diese Funktionalität anwendungsunabhängig kapseln könnte. Auf diese Weise wird man sich Schritt für Schritt eine kleine Bibliothek an Funktionen zulegen, die in vielen anderen Projekten ebenfalls Anwendung finden kann.

Zum anderen muß man sich überlegen, ob man die Verkettung von Bildschirmen, Knöpfen und Aktionen nicht ein wenig globaler lösen kann und welche Struktur man seinem Programm dafür geben wird.

21.6 Trenne Aussehen und Inhalt.

Antwort von Martin Jansen

Die Vorteile der Trennung von Aussehen und Inhalt durch Verwendung von Templates werden im Kapitel Was sind Templates? Warum sind Templates nützlich? erläutert.

Die gängigsten Template-System in PHP sind:


22. PHP 4

22.1 Ist PHP 4 stabil?

Antwort von Kristian Köhntopp

PHP4 wurde Ende Mai als PHP 4.0.0 für den allgemeinen Gebrauch freigegeben. Die bisherigen Erfahrungen sind recht gut.

22.2 Wo bekomme ich PHP4?

Antwort von Kristian Köhntopp

Wähle Download auf der PHP Website oder einem der Mirrors.

22.3 Wie übersetze ich PHP4?

Antwort von Kristian Köhntopp

Um PHP4 als Modul für den mit Suse Linux 6.4 gelieferten Webserver zu übersetzen, verwende ich das folgende Script do-conf:


cp $0 $0.suse
# cvs update -d -P
rm -f .deps
./buildconf
./configure \
        --with-java=/usr/lib/jdk1.1.7 \
        --with-pgsql=/usr/lib/pgsql \
        --with-mysql=/usr \
        --with-ldap=yes \
        --with-gd=yes \
        --with-zlib=yes \
        --with-xml \
        --with-ttf \
        --with-yp \
        --with-ftp \
        --with-snmp \
        --with-config-file-path=/etc/httpd \
        --with-apxs=/usr/sbin/apxs \
        --with-exec-dir=/usr/lib/apache/bin \
        --enable-versioning \
        --enable-track-vars \
        --enable-magic-quotes \
        --enable-safe-mode \
        --enable-sysvsem \
        --enable-sysvshm \
        --enable-thread-safety \
        i386-suse-linux-gnu

make depend
make -j 2

22.4 Was ist neu in PHP4?

Antwort von Kristian Köhntopp

PHP4 besteht aus dem Sprachkern Zend und den Funktionsmodulen, die den eigentlichen Wert von PHP ausmachen.

Änderungen am Sprachkern:

PHP4 ist ein Interpiler

PHP3 war eine interpretierte Sprache. Code wurde nur minimal gecached. Das hatte zur Folge, das Code in Schleifen oder in oft aufgerufenen Funktionen wieder und wieder neu geparsed werden musste, bevor er ausgeführt werden konnte.

PHP4/Zend ist ein Bytecode-Compiler, der beim Programmstart aufgerufen wird und das komplette Programm in eine interne Darstellung einer virtuellen Maschine überführt. Danach beginnt die interpretation des Bytecodes der VM. Dies ist dasselbe Funktionsprinzip wie bei Perl, und ganz ähnlich dem Funktionsprinzip von Java, nur daß dort der Compiler explizit aufgerufen werden muß.

PHP4/Zend ist bei einigen Sprachkonstrukten zehnmal schneller als PHP3. Es liegt in den meisten Anwendungen gleichauf mit Perl oder mit Microsoft Visual Basic.

PHP4 ist threadsafe

PHP4 kann endlich in Multithreaded-Umgebungen eingesetzt werden. Dadurch wird es möglich, PHP4 als Modul im Roxen Webserver, im Internet Information Server, im AOL/Netscape-Server und in einigen anderen Webservern einzusetzen.

PHP4 kann außerdem als CGI-Programm und als Coprozeß zum Webserver eingesetzt werden, der als Java Servlet Engine behandelt wird.

Referenzen sind endlich Teil der Sprache

In PHP3 waren Referenzen nur beim Aufruf von Funktionen verwendbar, um Call-by-Reference zu realisieren. In PHP4/Zend gibt es nun endlich vollständigen Support für Referenzen. Das bedeutet:


$foo = &$a;

In diesem Beispiel wird $foo zu einem alternativen Namen für die Variable $a. Dies funktioniert auch mit Arrays und Objekten.

Referenzzähler und Ressourcenfreigabe

In PHP3 existierten keine Zähler für Datenobjekte außerhalb der Sprache, etwa Image-Handles oder Datenbank-Resultathandles. Diese Ressourcen mußten manuell freigegeben werden.

PHP4/Zend hat für alle Variablen vom Typ Ressource Referenzzähler. Objekte, die nicht mehr referenziert werden, werden automatisch freigegeben. Das bedeutet: Aufrufe zu Funktionen wie mysql_free_result() innerhalb von Schleifen sind nicht mehr zwingend notwendig.

Demnach tut unset() jetzt auch endlich, was es soll.

Session-Support

PHP4 enthält Sessions nach dem Vorbild von PHPLIB als eingebauter Bestandteil der Sprache. Sessions werden standardmäßig als Dateien in einem Verzeichnis abgelegt, aber über Sessionmodule kann auch eine Datenbank, ein Shared Memory Segment oder ein anderer persistenter Speicher verwendet werden.

Anders als PHPLIB kann PHP4 die Session-ID automatisch an URLs anfügen.

Output Buffering

Mit Hilfe der Funktion ob_start() kann eine Ausgabepufferung aktiviert werden. Der Ausgabepuffer wird erst am Ende der PHP-Seite oder mit Hilfe der Funktion ob_end_flush() ausgegeben. Die Funktion ob_end_clean() verwirft den Ausgabepuffer. ob_get_contents() überträgt den Ausgabepuffer in eine Variable.

Headerinformation wie sie von header() und setcookie() erzeugt wird ist ungepuffert. Indem man Ausgabepufferung aktiviert, kann man überall in der Datei Headerinformationen senden, egal ob vorher schon Ausgabe erzeugt wurde oder nicht. Ausgabepufferung erhöht die Latenzen bei der Ausgabe der Webseite je nach Anwendung erheblich und kann PHP sehr viel langsamer erscheinen lassen.

Verbesserter Objektsupport

In PHP4/Zend ist es möglich, die Objektnotation von PHP zu verwenden, um objektorientiert aufgebaute Funktionsbibliotheken zu verwenden. Zum Beispiel verwenden die COM-Erweiterung und die DOMXML-Erweiterung von PHP4 diese Funktionalität.

Außerdem enthält PHP4 nun endlich Funktionen, mit denen Objekte über sich selbst Auskunft geben können: get_class(), phpfunction name="get_parent_class"/>, method_exists(), is_subclass_of()

include() und phpfunction name="eval"/> sind Funktionen

In PHP4 sind die früheren Anweisungen include() und eval() jetzt Funktionen, die einen Wert zurückgeben können. Der Default-Returnwert ist 1, damit Anweisungen wie if (include(...)) funktionieren.

Mit Hilfe der Anweisung return() kann man den Returncode eines include() oder eval()-Aufrufes setzen.

Verbesserter Parser

Der PHP3-Parser hatte Probleme mit Verschachtelungen von Objekten in Arrays. In PHP4/Zend ist dieses Problem behoben.

Shell-Style Here-Documents werden unterstützt.

Es gibt ein foreach()-Schleifenkonstrukt. Syntax:


foreach($my_array as $val)
        print "$val\n";

foreach($my_array as $key => $val)
        print "$key => $val\n";

Boolean

Die Schlüsselworte true und false sind nun Teil der Sprache und es gibt einen Datentyp boolean. Vergleiche werden nun durchgeführt, indem ein fremder Datentyp in Boolean konvertiert wird und dann verglichen wird: In PHP4/Zend ist das Konstrukt 5 == true eine wahre Aussage, weil (boolean) 5 in true konvertiert wird, bevor verglichen wird.

Laufzeitbindung von Funktionsnamen

Funktionen können nun aufgerufen werden, bevor sie deklariert werden.

Namespace aufgeräumt, Konfiguration überarbeitet

Der Import von Variablen in den globalen Namespace war bisher nur schwierig mit der Konfigurationsdirektive gpc_order steuerbar: Zwar konnte man die Reihenfolge unterdrücken, aber es war nicht möglich, den Import von externen Werten in globale Variablen zu verhindern, ohne diese Werte auch in den Track Vars auszuschließen. Umgebungsvariablen und Sessionvariablen waren gar nicht kontrollierbar.

In PHP4 kann man nun den Import von Werten differenziert steuern, und zwar unabhängig voneinander als globale Variablen und als Track Vars.

Ebenso wurde der Konfigurationsmechanismus überarbeitet: Es ist nun möglich, PHP mit denselben Direktiven in der Apache-Konfiguration und in der php.ini zu steuern.

Neue Funktionen und Module

Die Sprache ist außerdem um zahlreiche Funktionen und neue Module erweitert worden.

22.5 Wie kann ich PHP4 (CGI und Apache-Modul) konfigurieren?

Antwort von Johannes Frömter

PHP4 verwendet die globale Konfigurationdatei php.ini (statt php3.ini bei PHP3). Diese Datei ist gegenüber der php3.ini aufgeräumt und umgestaltet, so daß es sich lohnt, bei einem Upgrade von PHP3 auf PHP4 die alte Konfigurationsdatei zu verwerfen und mit einer Kopie der php.ini-dist neu anzufangen.

Standardmäßig liegt die Datei auf UNIX-Systemen unter /usr/local/lib/php.ini und auf Windows-Systemen im Sytemordner, also z.B. unter c:\windows oder c:\winnt.

Findet PHP keine php.ini, arbeitet es mit Default-Werten, die sich kaum von denen aus der php.ini-dist unterscheiden. Ob bzw. welche Konfigurationsdatei verwendet wird, läßt sich mittels


echo get_cfg_var("cfg_file_path");

herausfinden. Neben der php.ini gibt es aber noch andere Möglichkeiten, die PHP-Konfiguration zu beeinflussen:

httpd.conf (zentrale Apache-Konfiguration)

Diese Einstellungen wirken sich auf den ganzen Server (oder virtuelle Hosts oder sog. Container) aus (nur bei PHP als Apache-Modul):


# String- und Integerwerte
# php_value variable wert
php_value max_execution_time 60

# Booleanwerte
# php_flag variable on|off
php_flag track_vars on

# Geschützte Werte, die nur in der zentralen
# Apache-Konfig gesetzt werden dürfen.
# php_admin_value variable wert
# php_admin_flag  variable on|off

.htaccess (Per-Verzeichnis-Konfigurationsdatei)

php_flag und php_value können wie oben beschrieben verwendet werden. Diese Einstellungen wirken sich auf alle PHP-Scripts im aktuellen und allen Unterverzeichnissen aus (nur bei PHP als Apache-Modul).

ini_set (Konfiguration im PHP-Script)

Mit ini_set() kann man zur Laufzeit des Scripts die PHP-Konfiguration manipulieren:


ini_set("max_execution_time", "60");
phpinfo();

Wie man sehen kann, wurde die maximale Ausführungszeit für dieses eine Script verdoppelt (gegenüber der Default-Einstellung).

Diese drei Einstellmöglichkeiten betreffen jeweils den sog. "Local Value", im Gegensatz zum "Master Value", der ausschließlich in der php.ini definiert wird. Effektiv zur Anwendung kommt der "Local Value". Eine Übersicht über die Werte erhält man mit phpinfo().

Hinweis: Bei Änderungen an den Dateien httpd.conf und php.ini muß bei der Modulversion von PHP der Apache neu gestartet werden, um die Änderungen wirksam werden zu lassen.


23. PHP4: Sessions

23.1 Wie realisiere ich Sessions mit PHP4?

Antwort von Daniel T. Gorski

Sessiondaten sind Daten, die z.B. von einem PHP-Script auf dem Server gespeichert werden, und die meistens für die Dauer des Besuchs (Session/Sitzung) auf mehreren Pages einer Website ihre Gültigkeit behalten. Sessiondaten können z.B. dazu benutzt werden, um dem Besucher restriktiven Zugriff (z.B. nach einem Login) auf Teile einer Website zu erlauben - üblicherweise zu einem Administrationsbereich.

In der PHP4-Standardeinstellung werden diese Daten als ASCII- Dateien im /tmp-Verzeichnis (session.save_path) des Servers gespeichert. Diese Dateien enthalten das serialsierte (serialize()) Abbild der von einem Script gespeicherten Variablen. Der Name dieser Dateien setzt sich aus dem Prefix sess_ und einer 32-Zeichen langen, zufällig ausgewählten Zeichenkette (der Session-ID) zusammen.

Die Manipulation dieser Daten bleibt nur dem serverseitig ausgeführtem PHP-Script vorbehalten - der Client (Browser) weiß nicht welche Daten von dem Script gespeichert werden und er kann keinen mittelbaren Einfluß auf diese Daten nehmen. Ausnahme bildet hier die Änderung/Löschung dieser Daten durch den Administrator oder durch ein Script mit entsprechenden Rechten (siehe auch Wie schütze ich Sessiondaten zusätzlich? - weiter unten auf dieser Seite).

Unter diesen Voraussetzungen gelten die Sessiondaten bei einem korrekt installierten Webserver, als sicher und durch den Besucher nicht fakebar.

PHP4 stellt mehrere Funktionen zu Sessionverwaltung zu Verfügung (siehe auch die Sessionsreferenz, die grundlegenden seien hier kurz vorgestellt:

Bei Verwendung dieser Funktionen ist zu beachten, dass eine neue Session nur dann angelegt werden kann, wenn noch keine Ausgabe stattgefunden hat. Mehr Informationen dazu finden sich im Kapitel Warning: Cannot send session cookie - headers already sent....

session_start()

Session(datei) auf dem Server erstellen. Wurde eine gültige Session-ID übergeben, werden die in den Sessiondaten gespeicherten Werte in dem $HTTP_SESSION_VARS-Hash wiederhergestellt und abhängig von der Einstellung der register_globals auch als Variablen reinitialisiert.

Zusätzlich werden, je nach Einstellung des session.gc_probability -Parameters, in diesem Augenblick die angehäuften und nicht mehr benötigten Sessiondateien vom Server gelöscht. Wenn das error_reporting() schärfer eingestellt ist, gibt session_start() eine entsprechende Notice aus. Da diese üblicherweise nicht erwünscht ist, empfiehlt es sich vor die session_start()-Funktion ein "@" zu schreiben, welches diese Warnung unterdrückt.

Also:


<?php
    @session_start();
?>

session_register()

Eine oder mehrere Variablen zur Speicherung in die Session(datei) vormerken. session_register() impliziert ein session_start(). Das bedeutet, daß man session_start() in diesem Fall auch weglassen könnte. Bei den Parametern der session_register() Funktion handelt es sich nicht um die Variablen selbst, sondern um ihre Namen. Diese Parameter besitzen kein führendenes "$".

session_register() merkt eine Variable vor (registriert sie), die dann am Ende des Scriptes in die Session(datei) geschrieben wird, und die zwischen dem Registrieren und dem Scriptende verschiedene Werte haben kann. Es wird jeweils der letzte Wert dieser Variablen in den Sessiondaten gespeichert. D.h. Die Werte der registrierten Variablen sind frühestens, nach der Beendigung des aktuellen Scriptes und beim nächsten session_start() wieder verfügbar.

session_unregister()

Eine oder mehrere Variablen aus der Session(datei) verwerfen. Dem session_unregister() muß ein session_start() oder session_register() vorangehen.

session_destroy()

Die Funktion session_destroy() veranlasst alle Variablen einer Session zu verwerfen und die Session(datei) löschen.

Ein genauerer Einblick in die Funktionsweise dieser Funktionen wird weiter unten gewährt: Was geschieht im Filesystem des Servers wenn ich Sessions benutze?.

Beispiel 1

Beispiel 1: seite1.php


<?php
    // Falls nicht vorhanden, generiert eine Session(datei)
    // auf dem Server.
    // Falls bereits vorhanden, liest Sessiondaten wieder ein und
    // (re-)initialisiert die gespeicherten Variablen.
        
    @session_start();
        
    // Erst wenn das Script terminiert(!), merkt PHP den Inhalt
    // der Variablen $s_userName und $s_userPermissions in
    // den Sessiondaten.
        
    session_register("s_userName","s_userPermissions");
        
    $s_userName         =   "dtg";
    $s_userPermissions  =   "keine :=(";
?>

Beispiel 1: seite2.php


<?php
    // Falls nicht vorhanden, generiert eine Session(datei)
    // auf dem Server.
    // Falls bereits vorhanden, liest Sessiondaten wieder ein und
    // (re-)initialisiert die gespeicherten Variablen.
        
    @session_start();
        
    // Gibt den Inhalt der wiederhergestellten Variablen aus.
    echo "<P>Hallo ".$s_userName.",";
    echo "<P>Du hast ".$s_userPermissions." Zugriffsrechte.";
        
    // Wird in diesem Script kein session_unregister() oder
    // session_destroy() ausgeführt, bleiben die Daten erhalten!
?>

Damit das Folgescript (seite2.php) weiß, in welcher Datei auf dem Server die benötigten Daten zu finden sind, muß ihm die sog. Session-ID übergeben werden. Diese Session-ID repräsentiert den eindeutigen Namen der Session (es ist der bereits erwähnte 32-Zeichen lange String).

Um die Session-ID an eine andere Seite zu übergeben, benutzt PHP4 in der Standardeinstellung Session-Cookies, welche nicht gespeichert werden und mit dem Schließen des Browsers verfallen. In diesen Cookies wird der Name der Session (z.B. PHPSESSID) und die zugehörige Session-ID abgelegt. Bei einem Request auf ein Folgescript wird der Cookieinhalt mitgeschickt, und PHP weiß damit in welcher Sessiondatei die benötigten Daten gespeichert sind.

Dies passiert automatisch, ohne daß zusätzlicher Code geschrieben werden muß. Ist man auf PHP3 angewiesen, kann die PHPLIB ohne erhöhten Aufwand diesen Mechaninsmus abbilden. Diese FAQ enthält ein spezielles Kapitel über die PHPLIB.

Sind die php.ini-Parameter track_vars=On und register_globals=Off (standardmäßig On) eingestellt, dann sind u.a. die Sessiondaten nur über den $HTTP_SESSION_VARS[]-Hash ansprechbar, ansonsten werden die registrierten Variablen automatisch auf ihren letzten Wert reinitialisiert. Das gleiche gilt auch für die anderen Hashes $HTTP_GET_VARS[], $HTTP_POST_VARS[], $HTTP_COOKIE_VARS[], $HTTP_SERVER_VARS[] und $HTTP_ENV_VARS[].

Dadurch, daß die Sessionvariablen reinitialisert werden, ist es ratsam, die Sessionvariablen als solche zu kennzeichnen, damit man mit den lokalen Variablennamen eines Scripts nicht durcheinander kommt. In den Beispielen in diesem Kapitel wird das durch den Prefix s_ vor diesen Variablen realisiert.

Beispiel 2

Hat man z.B. eine komplizierte Suchmaske z.B für eine Datenbank, aus der man eine noch kompliziertere Query basteln muß, über deren Ergebnisse man auf Folgeseiten browsen will, kann es sich lohnen die Grund-Query einmalig zu generieren, und sie dann weiter an das Ausgabe-/Browse-Script zu übergeben, anstatt alle Eckparameter immerwieder zu übergeben und die Query jedesmal neu zu generieren.

Dies darf natürlich nicht über die GET- oder POST-Methode geschehen, denn der User könnte durch die Manipulation dieser Parameter ohne Probleme "böse" Queries an die Datenbank schicken. Ein Datenverlust ist dann vorprogrammiert.

Für diesen Einsatzzweck eignen sich Sessions auch, denn die dynamisch erzeugte Query wird außerhalb der Reichweite des Users auf dem Server gespeichert.

Beispiel 2: seite3.php (Formularseite)


<?php
    @session_start();
        
    if (isset($submit))
        {
            // Hier prüfen, ob _alle_ benötigten Variablen,
            // die an dieses Script übermittelt wurden,
            // legal sind!
            // Hier nur fortfahren, wenn alles in Ordnung ist
        
            session_register("s_meineTolleQuery");  

            // Die Query wird anhand der Variablen,
            // die submitted wurden, erzeugt
            $s_meineTolleQuery = "SELECT ...";
        
            // HTTP-Redirect zu der Ausgabeseite
            header("Location: http://".getenv("SERVER_NAME")
                                      ."/seite4.php");
            exit;
        }
?>
                        
    <FORM action="<?=$PHP_SELF?>" method="post">
        <!--
            Hier ganz viele Radiobuttons, Checkboxen und Inputfelder
        -->
       <INPUT type="submit" name="submit">
    </FORM>

Beispiel 2: seite4.php (Ausgabeseite)


<?php
    @session_start();

    echo "<P>Diese Query wurde mir übermittelt: "
             .$s_meineTolleQuery;
    echo "<P>und jetzt mache ich was draus!";
?>

23.2 Was ist eine Session-ID? Was ist PHPSESSID?

Antwort von Daniel T. Gorski

Die sog. Session-ID ist ein zufällig ausgewählter Schlüssel, der die Sessiondaten auf dem Server eindeutig identifiziert. Dieser Schlüssel kann z.B. über Cookies oder als Bestandteil der URL an ein Folgescript übergeben werden, damit dieses die Sessiondaten auf dem Server wiederfinden kann.

PHPSESSID ist bei PHP4 der Default-Name der Session. Möchte man diesen Namen aus z.B. ästhetischen Gründen modifizieren - vor allem, wenn er als GET-Parameter als Teil der URL sichbar wird - so kann man dies in der php.ini, der Webserverkonfiguration oder direkt mit PHP bewerkstelligen. Der Unterpunkt Wie kann ich den Namen der Session ändern, ohne in die php.ini einzugreifen? geht genauer darauf ein.

Eine Beispiel, wenn die Session-ID als GET-Parameter in der URL übertragen wird:


                            Query-String (alle GET-Parameter)
Protokoll     Subdomain      _________________|__________________
  |              |          /                                    \
http://www.daniel-gorski.de?query=xyz&PHPSESSID=cd45a3f73493d5d...
        |                |            \__________________________/
   Subdomain       Top Level Domain                |
                                       Session-Name und Session-ID

23.3 Wie stelle ich fest, ob der Client die Cookie-Annahme verweigert?

Antwort von Daniel T. Gorski

Am Beispiel der Session-Fallback-Klasse in Wie übergebe ich Session-IDs ohne Cookies an eine andere Seite? Was ist Fallback? kann man nachvollziehen, wie man feststellen kann, ob der Client Cookies akzeptiert oder nicht. Das Prinzip ist simpel: Das aufgerufene Script prüft, ob ihm eine Session-ID übergeben wurde. Falls nicht, forciert das Script einen Request auf sich selbst und übergibt dabei eine frisch erzeugte Session-ID als GET- Parameter an sich selbst. Beim zweiten Durchlauf des Scriptes kann dieses feststellen, ob ihm vom Browser ein Cookie mit der Session-ID übermittelt worden ist, oder ob es die Session-ID aus dem GET-Parameter benutzen soll.

23.4 Wie übergebe ich Session-IDs ohne Cookies an eine andere Seite? Was ist Fallback?

Antwort von Daniel T. Gorski

Viele Browser-User haben in ihrem Browser die Cookies aus diversen Gründen deaktiviert, oder lassen sich jeden Cookie bestätigen. Sollte ein Cookie auf dem lokalen Rechnersystem nicht gesetzt werden können, muß ein Ersatzmechanismus her - ein sog. Fallback. In diesem Fall ist der Fallback (Rückfall / Atavismus) eine Ersatzmethode, die die Übergabe der Session-ID an ein Folgescript ohne Cookies erlaubt.

Man kann den Fallback dadurch realisieren, daß man dem Folgescript die Session-ID quasi "manuell" bei jedem GET


<A href="test.session.php?<?=session_name()."=".session_id()?>">

oder POST - bei Formularen - mittels


<INPUT type  = "hidden"
       name  = "<?=session_name()?>" 
       value = "<?=session_id()?>">

übergibt. ACHTUNG: Das bedeutet, daß jeder Link und jedes Formular innerhalb einer Website mit einem zusätzlichen Parameter versehen werden muß! Wird die Session-ID nicht korrekt übergeben, wird ein Folge-Script nicht auf die Sessiondaten zugreifen können - diese Daten sind für dieses Script "verloren".

PHP4 kann man auch mit dem --enable-trans-sid-Parameter kompilieren. Dann hat dies zufolge, daß wenn der Client (Browser) keine Cookies annehmen kann/will, alle relativen Links einer Webpage mit dem zusätzlichen SessionName=Session-ID Parameter ergänzt werden. Die klingt zunächst gut, aber man sollte beachten, daß durch den zusätzlichen Aufwand, den der PHP-Parser leisten muß, sich diese Technik nicht für High-Traffic-Websites oder Server mit vielen Vhosts eignet. Auch wenn man Projekte realisiert, die auf anderen Webservern laufen sollen, kann man nicht davon ausgehen, daß das betreffende PHP mit --enable-trans-sid kompiliert worden ist.

Die --enable-trans-sid-Technik versagt (PHP 4.0.1.pl2) auch bei folgenden Konstrukten:


<FORM action="<?=$PHP_SELF?>">

weil offensichtlich zuerst an den action-Tag die Session-ID-Information angehängt wird, und dann erst der Wert von $PHP_SELF. Das Ergebnis sieht dann fälschlicherweise so aus:


<FORM action="?PHPSESSID=cd45a3f76f7325099c755b25b/test.session.php">

Fallback-Funktionen

Wie bereits erwähnt, muß man u.U. selbst dafür Sorge tragen, daß der Fallback-Mechnismus greift, wenn der Client keine Cookies annimmt. Um dies zu ermöglichen kann man sich der Funktionaliät der PHPLIB oder einer Klasse wie dieser bedienen:


<? 

// Datei:       class.Session.inc
// Benötigt:    mind. 4.0.1pl2

/**
*   "Manueller" Session-Fallback mit PHP4
*
*   @author     Daniel T. Gorski <daniel.gorski@bluemars.de>
*/

class Session {
    var $version     = 106;     // V1.06
    var $usesCookies = false;   // Client nimmt Cookies an
    var $transSID    = false;   // Wurde mit --enable-trans-sid
                                // kompiliert  

//---------------------------------------------------------

/**
*   Konstruktor - nimmt, wenn gewünscht einen neuen
*   Session-Namen entgegen
*/    
    function Session($sessionName="SESSID") {
        global  $HTTP_POST_VARS,$HTTP_GET_VARS,
                $HTTP_COOKIE_VARS,$QUERY_STRING,$PHP_SELF;

        $this->sendNoCacheHeader();

        //  Session-Namen setzen, Session initialisieren   
        session_name(isset($sessionName)
            ? $sessionName
            : session_name());

        @session_start();
        
        //  Prüfen ob die Session-ID die Standardlänge
        //  von 32 Zeichen hat,
        //  ansonsten Session-ID neu setzen 
        if (strlen(session_id()) != 32)
            {
                mt_srand ((double)microtime()*1000000);
                session_id(md5(uniqid(mt_rand())));
            }
        
        //  Prüfen, ob eine Session-ID übergeben wurde
        //  (über Cookie, POST oder GET)
        $IDpassed = false;

        if  (   isset($HTTP_COOKIE_VARS     [session_name()]) &&
                @strlen($HTTP_COOKIE_VARS   [session_name()]) == 32
            )   $IDpassed = true;

        if  (   isset($HTTP_POST_VARS       [session_name()]) &&
                @strlen($HTTP_POST_VARS     [session_name()]) == 32
            )   $IDpassed = true;

        if  (   isset($HTTP_GET_VARS        [session_name()]) &&
                @strlen($HTTP_GET_VARS      [session_name()]) == 32
            )   $IDpassed = true;
        
        if  (!$IDpassed)  
            {   
                // Es wurde keine (gültige) Session-ID übergeben.
                // Script-Parameter der URL zufügen
                
                $query = @$QUERY_STRING != "" ? "?".$QUERY_STRING : "";
             
                header("Status: 302 Found");
                
                // Script terminiert
                $this->redirectTo($PHP_SELF.$query);
            }
            
        // Wenn die Session-ID übergeben wurde, muß sie
        // nicht unbedingt gültig sein!
        
        // Für weiteren Gebrauch merken    
        $this->usesCookies =
                       (isset($HTTP_COOKIE_VARS[session_name()]) &&
                        @strlen($HTTP_COOKIE_VARS[session_name()])
                        == 32);
    }    
 
### -------------------------------------------------------
/**
*   Cacheing unterbinden
*
*   Ergänze/Override "session.cache_limiter = nocache"
*
*   @param  void
*   @return void
*/    
    function sendNoCacheHeader()    {        
        header("Expires: Sat, 05 Aug 2000 22:27:00 GMT");
        header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
        header("Cache-Control: no-cache, must-revalidate");
        header("Pragma: no-cache");
        header("Cache-Control: post-check=0, pre-check=0");        
    }

### -------------------------------------------------------
/**
*   HTTP-Redirect ausführen (header("Location: ...")
*
*   Diese Methode berücksichtigt auch nicht-standard Ports
*   und SSL. Ein GET-Parameter beim  wird bei Bedarf
*   (Session-ID-Fallback) an die URI drangehängt. Nach
*   dem Aufruf dieser Methode wird das aktive Script
*   beendet und die Kontrolle wird an das Ziel-Script
*   übergeben.
*
*   @param  string  Ziel-Datei (z.B. "index.php")
*   @return void
*/   
    function redirectTo($pathInfo) {
        
        // Relativer Pfad?
        if ($pathInfo[0] != "/")
            {   $pathInfo = substr(getenv("SCRIPT_NAME"),
                                   0,
                                   strrpos(getenv("SCRIPT_NAME"),"/")+1
                                   )
                            .$pathInfo;
            }

        // Läuft dieses Script auf einem non-standard Port? 
        $port    = !preg_match( "/^(80|443)$/",
                                getenv("SERVER_PORT"),
                                $portMatch)
                   ? ":".getenv("SERVER_PORT")
                   : "";
                                         
        // Redirect    
        header("Location: "
               .(($portMatch[1] == 443) ? "https://" : "http://")
               .getenv("SERVER_NAME").$port.$this->url($pathInfo));
        exit;
    }

### -------------------------------------------------------
/**
*   Entfernt mögliche abschließende "&" und "?"
*
*   @param  string  String
*   @return string  String ohne abschließende "&" und "?"
*/
    function removeTrail($pathInfo) {
        $dummy = preg_match("/(.*)(?<!&|\?)/",$pathInfo,$match);
        return $match[0];  
    }

### -------------------------------------------------------
/**
*   Fallback via GET - wenn Cookies ausgeschaltet sind
*
*   @param  string  Ziel-Datei
*   @return string  Ziel-Datei mit - bei Bedarf - angehängter Session-ID
*/
    function url($pathInfo)  {        
        if ($this->usesCookies || $this->transSID) return $pathInfo;

        // Anchor-Fragment extrahieren
        $dummyArray = split("#",$pathInfo);
        $pathInfo = $dummyArray[0];

        // evtl. (kaputte) Session-ID(s) aus dem Querystring entfernen
        $pathInfo = preg_replace(   "/[?|&]".session_name()."=[^&]*/",
                                    "",
                                    $pathInfo);
        
        // evtl. Query-Delimiter korrigieren
        if (preg_match("/&/",$pathInfo) && !preg_match("/\?/",$pathInfo))
            {
                // 4ter Parameter für "preg_replace()" erst ab 4.0.1pl2
                $pathInfo = preg_replace("/&/","?",$pathInfo,1); 
            }
        
        // Restmüll entsorgen
        $pathInfo = $this->removeTrail($pathInfo);
        
        // Session-Name und Session-ID frisch hinzufügen  
        $pathInfo .= preg_match("/\?/",$pathInfo) ? "&" : "?";
        $pathInfo .= session_name()."=".session_id();
        
        // Anchor-Fragment wieder anfügen
        $pathInfo .= isset($dummyArray[1]) ? "#".$dummyArray[1] : "";
        
        return $pathInfo;                       
    }
    
### -------------------------------------------------------
/**
*   Fallback via HIDDEN FIELD - wenn Cookies ausgeschaltet sind
*
*   Ohne Cookies erfolgt Fallback via HTML-Hidden-Field
*   (für Formulare)
*   
*   @param  void
*   @return string  HTML-Hidden-Input-Tag mit der Session-ID
*/
    function hidden() {
        if ($this->usesCookies || $this->transSID) return "";
        return "<INPUT  type=\"hidden\"
                        name=\"".session_name()."\"
                        value=\"".session_id()."\">";
    }
} // of class    
    
?>

Diese Session-Fallback-Klasse mag auf der ersten Blick groß erscheinen - besteht sie doch größtenteils aus Kommentaren und Leerzeilen, um (hoffentlich) die Lesbarkeit zu erhöhen.

Beim Erzeugen einer Instanz dieser Klasse, prüft der Konstruktor, ob eine gültige Session-ID übergeben wurde. Sollte keine Session-ID über Cookie, POST oder GET übergeben worden sein, forciert das Script einen Request auf sich selbst. Beim zweiten Durchlauf kann es dann feststellen, ob ein Cookie gesetzt wurde, oder ob ein Fallback via GET realisert werden muß.

Primär interessieren uns nur drei Methoden dieser Klasse:

Session -> url()

Diese Methode wird mit der Zieldatei (hier test.session.php) als Parameter aufgerufen.


<A href="<?echo $Session->url("test.session.php")?>">
    Test-Link 1
</A>

<A href="<?echo $Session->url("test.session.php?foo=Link2")?>">
    Test-Link 2
</A>

Sie hängt eine Session-ID an die übergebene Pfad-Information - sofern diese noch keine ID enthält und Cookies nicht akzeptiert werden - und gibt den so erzeugten String zurück, der dann mittels echo, print etc. als Teil des HTML-Anchor-Tags ausgegeben werden kann.

Das Ergebnis sieht so aus:


<A href="test.session.php?SESSID=cd45a3f76f7325099c755b25b3493d5d">
    Test-Link 1
</A>

Session -> hidden()

Liefert ein Hidden-Formularelement (HTML-INPUT-Tag), welches den Session-Namen und die Session-ID enthält. Dieses Formularelement wird nur erzeugt, wenn Cookies nicht akzeptiert werden. Ansonsten wird ein Leerstring zurückgegeben.


<FORM action="<?echo $PHP_SELF?>" method="POST">
    <?echo $Session -> hidden()?>
</FORM>

Ergebnis:


<FORM action="/test.session.php" method="POST">
    <INPUT type="hidden" name="SESSID"
           value="cd45a3f76f7325099c755b25b3493d5d">
</FORM>

Session -> redirectTo()

Diese Methode erzeugt einen HTTP-Location-Request (header("Location: http://...");), wobei auch hier bei Bedarf die Session-ID übergeben wird. Der Aufruf dieser Methode hat zufolge, daß das aktive Script beendet wird, und die Kontrolle an das Zielscript übergeben wird.

Um diese Klasse bei der Arbeit betrachen zu können, brauchen wir noch ein Test-Script, welches auf die Methoden dieser Klasse zurückgreift. Dieses Script funktioniert nicht, wenn das kaputtimplementierte --enable-trans-sid aktiv ist.


<?php
//  Datei: test.session.php

    require "class.Session.inc";

//  Session initialisieren
    $Session = new Session();

// --- HTML Ausgabe erst ab hier
//  Siehe auch: "Warning: Cannot send session
//  cookie - headers already sent..."

    echo    "<HTML>";
    echo    "<BODY>";
    
    if ($Session -> usesCookies)
        {   echo "<P>Cookie wurde gesetzt. ";
            echo "Kein Fallback nötig.\n";
        }
    else{
            echo "<P>Der Client nimmt keine Cookies an! ";
            echo "Wir brauchen einen Fallback.\n";
        }
?>

    
    <FORM action="<?echo $PHP_SELF?>" method="POST">
        <?
            //  Wenn nötig wird, die Session-ID im
            //  Hidden-Feld übertagen

            echo $Session -> hidden()
        ?>

        <P><INPUT name="foo" value="<?echo @$foo?>">
        <P><INPUT type="submit">
    </FORM>

    <?
        //  Wenn nötig wird die Session-ID als
        //  GET-Parameter übertagen
    ?>

    <P> <A href="<?echo $Session->url("test.session.php")?>">
            Test-Link 1
        </A>
    <P> <A href="<?echo $Session->url("test.session.php?foo=Link2")?>">
            Test-Link 2
        </A>
    <P> <A href="<?echo $Session->url($PHP_SELF."?foo=Link3")?>">
            Test-Link 3
        </A>
    
    </BODY>
    </HTML>

An dieser Stelle empfehle ich, die Klasse class.Session.inc und das dazugehörige Test-Script test.session.php zu speichern, um die Funktionalität - mit und ohne Cookies - selbst zu testen.

Tip: Um die Lesbarkeit eigener Scripte zu erhöhen kann man statt


<?echo "foo"?>
<?echo $Session->url("test.session.php")?>

auch folgendes schreiben:


<?="foo"?>
<?=$Session->url("test.session.php")?>

23.5 Warning: Cannot send session cookie - headers already sent...

Antwort von Daniel T. Gorski

Wie bei allen anderen HTTP-Headern, darf auch vor dem Setzen von Cookies (Sessionfunktionen von PHP4 benutzen standardmäßig Cookies) kein einziges Byte des Codes an den Client gesendet werden. Erst wenn alle HTTP-Header gesendet worden sind, dürfen entsprechende Daten (z.B. HTML) gesendet werden.

Typische Stolperfallen sind Leerzeichen bzw. -zeilen vor dem ersten <? bzw. <?php Delimiter des Scripts oder mit include() oder require() importierte Scriptfragmente, die natürlich gar keine Ausgabe produzieren dürfen - weder vor dem ersten Delimiter, noch dazwischen, noch nach dem letzten.

Zusätzlich kommt durch die auto_prepend_file-Einstellung in der php.ini, bzw. in der Webserverkonfiguration, eine weitere potentielle Fehlerquelle hinzu.

23.6 Wie kann ich den Namen der Session ändern, ohne in die php.ini einzugreifen?

Antwort von Daniel T. Gorski

In der php.ini wird der Name der Session in dem Parameter session_name festgelegt - standardmäßig auf PHPSESSID. Möchte man ohne Eingriff in die php.ini oder in die Webserverkonfiguration diesen Namen ändern, steht die Funktion session_name() zu Verfügung. Diese Funktion muß vor dem (Re)initialisieren der Sessiondaten (session_start() bzw. session_register()) ausgeführt werden.


<?php
    // Einen anderen Namen für die Session festlegen
    session_name("meineSession");
    @session_start();
?>

23.7 Wie schütze ich Sessiondaten zusätzlich?

Antwort von Daniel T. Gorski

Laufen auf einer Maschine mehrere virtuelle Hosts (vhosts), z.B. bei einem Hoster, muß der verantwortliche Administrator dafür Sorge tragen, daß - wenn überhaupt - der Safe-Mode nur für 100% vertrauenswürdige vhost-Nutzer ausgeschaltet werden darf (safe_mode=Off). Dies würde - neben anderen systemnahen Eingriffen - erlauben, daß ein Script von einem anderen Vhost folgenden Code ausführen darf:


<?php
    // Inhalt von /tmp löschen
    system("rm -rf /tmp");
?>

Dies hätte in der PHP4-Standardeinstellung (aber mit safe_mode=Off) zufolge, daß alle Sessiondaten anderer Benutzer gelöscht worden wären. Gegebenfalls sollte man sich mit einer entsprechenden Anfrage an seinen Hoster wenden.

Eine andere Möglichkeit dies zu verhindern wäre es, für jeden Vhost eine andere Einstellung des session.save_path-Parameters zu wählen, wenn Sessiondaten in Files gespeichert werden.

23.8 Wie groß darf die Menge an Daten sein, die ich in einer Session speichern darf?

Antwort von Daniel T. Gorski

Diese Frage läßt sich im Grunde nur mit "maßvoll" beantworten. Der Unterschied zwischen wenigen Bytes und 5KB, wird sich nicht ungünstig auf die Performance des Servers auswirken.

Eigene Performancemessungen ergeben hier eindeutig einen Wissensvorteil. Zudem sollte man bedenken, daß größere Datenmengen besser in einer Datenbank aufgehoben sind - und man sollte seine Datenverwaltung nochmals gründlich überschlafen.

23.9 Wie kann ich mir den Inhalt der Sessiondaten anzeigen lassen?

Antwort von Daniel T. Gorski

Ähnlich wie die Ausgabe anderer PHP-Hashes (assoziativer Arrays) lassen sich Sessiondaten mittels einer kleinen Schleife ausgeben. Zuvor muß die Session mit session_start() initialisiert werden. Dies bewirkt, daß der $HTTP_SESSION_VARS-Hash gefüllt wird. Der Konfigurationsparameter track_vars der php.ini muß eingeschaltet sein.


<?php
    @session_start();
    while (list($key,$value) = each($HTTP_SESSION_VARS))
       {
           echo $key." = ".$value."<br>";
       }
?>

23.10 Wie kann ich mir den Inhalt der Cookiedaten anzeigen lassen?

Antwort von Daniel T. Gorski

Analog zu obigen Beispiel kann man über $HTTP_COOKIE_VARS auf den Inhalt eines Cookies zugreifen. Auch hier muß der Konfigurationsparameter track_vars der php.ini eingeschaltet sein.


<?php
    @session_start();
    while (list($key,$value) = each($HTTP_COOKIE_VARS))
       {
           echo $key." = ".$value."<br>";
       }
?>

23.11 Sessiondaten werden nach session_destroy() nicht gelöscht. Wie kann ich sie trotzdem löschen?

Antwort von Daniel T. Gorski


<?php
    // Wenn es doch nötig wird - brute force
    if (strtolower(session_module_name()) == "files")
        {
           @unlink(get_cfg_var("session.save_path")."/sess_".session_id());
        }
?>

23.12 Was geschieht im Filesystem des Servers wenn ich Sessions benutze?

Antwort von Daniel T. Gorski

Damit der Vorgang der Sessionspeicherung ein wenig verdeutlicht wird, untersuchen wir die Veränderungen im Filesystem des Servers. Die folgenden Beispiele laufen auf einem Linux/Unixsystem. Um sie nachvollziehen zu können, muß PHP4 mit safe_mode=Off konfiguriert werden. Zusätzlich wird davon ausgegangen, daß der Client (Browser) Cookies akzeptiert.

Als allererstes braucht man ein Script, welches den Inhalt des im session.save_path festgelegten Directories anzeigen kann:


<?php
    // Datei: list.dir.php
    // Liste das "session.save_path" Directory
    // "safe_mode" muß "Off" sein
    if (strtolower(session_module_name()) == "files")
        {
            echo "<pre>";
            system("ls -l ".get_cfg_var("session.save_path"));
            echo "</pre>";
        }
?>

Dieses Script wird unter dem Namen list.dir.php gespeichert. Hat man z.B. Shell-Zugriff auf den benötigten Teil des Servers, führt natürlich auch ein einfaches ls -l /tmp zu der von list.dir.php produzierten Ausgabe.

Das obige Script wird ausgeführt, um den Ursprungszustand des im session.save_path gespeicherten Verzeichnisses zu betrachten. Zunächst produziert das Script folgendes:

Ausgabe (mit list.dir.php):


    insgesamt 0
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock

Das Verzeichnis (in diesem Fall /tmp) ist fast leer. Die Datei mysql.sock kann für unseren Zweck vernachlässigt werden.

Als nächstes schreibt man ein Script, welches nach und nach ergänzt oder verändert wird, um die Veränderungen im Filesystem zu beobachten. Der Name diese Scriptes sei session.php. Dieses Script enthält nur die Funktion session_start(). Nach dem Start beobachten wir die Veränderungen:


<?php
    // Datei: session.php
    @session_start();
?>

Ausgabe (mit list.dir.php):


    insgesamt 0
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock
    -rw-------    1 wwwrun   www    0 Okt  1 17:58 sess_cd45a3f76f73250..

Wie man sehen kann, hat PHP eine Sessiondatei mit den Zugriffsrechten des Webservers erzeugt, dessen Prozeß es ja ist. Diese Datei ist zunächst 0 (Null) Byte groß. Diese Datei wird gleich die Sessiondaten aufnehmen.

Als nächstes wird das Script um die session_register() Funktion erweitert, die festlegt, daß die Variablen $s_userName und $s_userPermissions und ihre Inhalte gespeichert werden sollen. Bei den Parametern der session_register() Funktion handelt es sich nicht um die Variablen selbst, sondern um ihre Namen. Diese Parameter besitzen kein führendenes "$".


<?php
//  Datei: session.php
    @session_start();
    session_register("s_userName","s_userPermissions");

    $s_userName        = "dtg";
    $s_userPermissions = "keine :=(";
?>

Ausgabe (mit list.dir.php):


    insgesamt 4
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock
    -rw-------    1 wwwrun   www   55 Okt  1 17:58 sess_cd45a3f76f73250..

Die Sessiondatei ist nun auf 55 Byte angewachsen. Sie enthält folgenden String:


    s_userName|s:3:"dtg";s_userPermissions|s:9:"keine :=(";

also die serialisierten (serialize()) Namen und Inhalte der Variablen, die mit session_register() bestimmt wurden. In den Sessiondaten können verschiedene Variablentypen gespeichert werden, also auch Arrays und Objekte.

Wenn man jetzt auf eine Variable in der Sessiondatei verzichten will, kann man mittels session_unregister() PHP davon abhalten, die betreffende Variable zu speichern:


<?php
    // Datei: session.php
    @session_start();
    session_unregister("s_userPermissions");

    $s_userPermissions = "ich will nicht gespeichert werden";
?>

Ausgabe (mit list.dir.php):


    insgesamt 4
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock
    -rw-------    1 wwwrun   www   21 Okt  1 17:59 sess_cd45a3f76f73250..

Die Variable $s_userPermissions wurde verworfen, die Sessiondatei ist nur noch 21 Byte groß und enthält nur noch die Variable $s_userName:


    s_userName|s:3:"dtg";

Möchte man nun alle Sessiondaten löschen stellt PHP4 die Funktion session_destroy() zur Verfügung. Vor einem session_destroy() muß aber ein session_start() oder session_register() ausgeführt werden:


<?php
    // Datei: session.php
    @session_start();
    session_destroy();
?>

Ausgabe (mit list.dir.php):


    insgesamt 0
    srwxrwxrwx    1 root     root   0 Sep 22 21:38 mysql.sock

Die Sessiondatei wurde gelöscht und die Sessionvariablen stehen für weitere Scripte nicht mehr zur Verfügung. Wird nach diesem Zeitpunkt nochmals die Funktion session_start() bzw. session_register() im gleichen Browserfenster aufgerufen, wird die Sessiondatei erneut erzeugt, und zwar u.U. mit dem gleichen Namen d.h. mit der gleichen Session-ID(!). Der Grund dafür ist, daß der vom Browser gespeicherte Cookie - der die Session-ID enthält - erneut an den Server übermittelt wird.

23.13 Wie benutze ich die Session-Funktionen unter Windows?

Antwort von Sebastian Bergmann

Der Wert von session.save_path in der php.ini ist in seiner Standardeinstellung fr den Einsatz der session_*() Funktionen auf Win32 unbrauchbar. Abhilfe schafft hier das Anlegen eines neuen Verzeichnisses, zum Beispiel c:\winnt\temp\sessions\, dessen Ort man nun als Wert fr session.save_path in der php.ini angibt.


24. PEAR

24.1 Was ist PEAR?

Antwort von Martin Jansen

PEAR (PHP Extension and Application Repository) ist ein zentrales Archiv für Klassen und Bibliotheken in PHP mit einem hohen Wiederverwendungswert und für PHP Erweiterungen in C.

PEAR ist Malin Bakken gewidmet. Die ersten Bestandteile des PEAR wurden kurz vor ihrer Geburt geschrieben.

PEAR ist das Gegenstück zu CPAN bei Perl und CTAN bei TeX. Der Zweck von PEAR ist die Verbreitung nützlicher, geprüfter und qualitativ hochwertiger Skripte in PHP und Erweiterungen in C, die von allen PHP-Entwicklern genutzt werden können.

Gleichzeit sollen die Standards, die in PEAR definiert werden, dazu dienen, dass Entwickler Code schreiben können, der portabel ist und auf vielen anderen Systemen und Konfigurationen eingesetzt werden kann.

Wer Code zu PEAR beisteuern will, der sollte die Mailingliste pear-dev@lists.php.net abonnieren und sich mit den PEAR Coding Standards auseinandersetzen. Wenn man sich an die Coding Standards (die den Dokumentationsstil, die Einrückung der Zeilen usw. vorgeben) hält, dann sind die Chancen sehr gut, dass der Code ins PEAR aufgenommen wird.

24.2 Wo kann ich PEAR downloaden?

Antwort von Martin Jansen

Derzeit ist PEAR Bestandteil jeder PHP Distribution. Mit fortschreitender Größe des PEAR wird sich dieses Verfahren jedoch als zu resourcenintensiv erweisen.

Daher soll in absehbarer Zeit eine Installationsroutine entwickelt werden, mit der die Bestandteile des PEAR aus dem Internet heruntergeladen und installiert werden können.

Auf der Homepage von PEAR, die sich zur Zeit in der Aufbauphase befindet, werden die Elemente in absehbarer Zeit ebenfalls zum Download verfügbar sein.

Dort wird auch die Dokumentation zu den einzelnen Elementen zu finden sein.

24.3 Wie installiere ich PEAR?

Antwort von Martin Jansen

Unix:

Bei "make install" werden die Bestandteile des PEAR automatisch nach "/usr/local/lib/php" kopiert.

Danach muss man lediglich die Direktive include_path von


include_path    = ""

nach


include_path    = "./;/usr/local/lib/php"

ändern. Danach können die Skripte des PEAR auf allen Seiten eingebunden werden.

Windows:

Unter Windows gibt es derzeit keinen automatischen Installationsprozess, daher ist hier etwas Handarbeit gefragt:

24.4 Wo finde ich weitere Informationen zu PEAR?

Antwort von Martin Jansen

John Lim hat auf http://php.weblogs.com/php_pear_tutorials Links zu einer Vielzahl an Tutorials zu PEAR aufgelistet.

Darüber hinaus gibt es für PEAR einen eigenen Abschnitt im PHP Manual.


25. Open Publication License

25.1 Englische Version

Antwort von Kristian Köhntopp

REQUIREMENTS ON BOTH UNMODIFIED AND MODIFIED VERSIONS

The Open Publication works may be reproduced and distributed in whole or in part, in any medium physical or electronic, provided that the terms of this license are adhered to, and that this license or an incorporation of it by reference (with any options elected by the author(s) and/or publisher) is displayed in the reproduction.

Proper form for an incorporation by reference is as follows:

Copyright (c) 2000-2001 by Kristian Köhntopp, Tobias Ratschiller and others. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v0.4 or later (the latest version is presently available at http://www.opencontent.org/openpub/), not using any of the Open Publication License Options from section LICENSE OPTIONS.

Commercial redistribution of Open Publication-licensed material is permitted.

Any publication in standard (paper) book form shall require the citation of the original publisher and author. The publisher and author's names shall appear on all outer surfaces of the book. On all outer surfaces of the book the original publisher's name shall be as large as the title of the work and cited as possessive with respect to the title.

COPYRIGHT

The copyright to each Open Publication is owned by its author(s) or designee.

SCOPE OF LICENSE

The following license terms apply to all Open Publication works, unless otherwise explicitly stated in the document.

Mere aggregation of Open Publication works or a portion of an Open Publication work with other works or programs on the same media shall not cause this license to apply to those other works. The aggregate work shall contain a notice specifying the inclusion of the Open Publication material and appropriate copyright notice.

SEVERABILITY. If any part of this license is found to be unenforceable in any jurisdiction, the remaining portions of the license remain in force.

NO WARRANTY. Open Publication works are licensed and provided "as is" without warranty of any kind, express or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose or a warranty of non-infringement.

REQUIREMENTS ON MODIFIED WORKS

All modified versions of documents covered by this license, including translations, anthologies, compilations and partial documents, must meet the following requirements:

GOOD-PRACTICE RECOMMENDATIONS

In addition to the requirements of this license, it is requested from and strongly recommended of redistributors that:

Finally, while it is not mandatory under this license, it is considered good form to offer a free copy of any hardcopy and CD-ROM expression of an Open Publication-licensed work to its author(s).

LICENSE OPTIONS

The author(s) and/or publisher of an Open Publication-licensed document may elect certain options by appending language to the reference to or copy of the license. These options are considered part of the license instance and must be included with the license (or its incorporation by reference) in derived works.

25.2 Deutsche Version

Antwort von Kristian Köhntopp

Inoffizielle deutsche Übersetzung des englischen Originals (von Stefan Meretz).

ERFORDERNISSE FÜR UNMODIFIZIERTE UND MODIFIZIERTE VERSIONEN

Open-Publication-Arbeiten dürfen als Ganzes oder in Teilen reproduziert und verteilt werden, in beliebigen Medien, physisch oder elektronisch, vorausgesetzt, die Bedingungen dieser Lizenz gehören dazu, und diese Lizenz oder ein Verweis auf diese Lizenz (mit jeder Option, die von dem Autor / den Autoren und/oder dem Herausgeber gewählt wurde) wird in der Reproduktion angezeigt.

Eine geeignete Form einer Aufnahme durch Verweis lautet wie folgt:

Copyright (c) 2000-2001 by Kristian Köhntopp, Tobias Ratschiller und weitere Autoren. Dieses Material darf nur gemäß der Regeln und Bedingungen wie sie von der Open Publication Licence, Version v0.4, festgelegt werden, verteilt werden (die letzte Version ist gegenwärtig verfügbar unter http://www.opencontent.org/openpub/). Diese Veröffentlichung macht von keiner der im Abschnitt LIZENZ-OPTIONEN genannten Optionen Gebrauch.

Die kommerzielle Weiterverbreitung von Open Publication lizensiertem Material ist zu den aufgeführten Bedingungen ausdrücklich gestattet.

Jegliche Publikation im Standard- (Papier-) Buch-Format erfordert die Zitierung der Original-Herausgeber und Autoren. Die Namen von Herausgeber und Autor/en sollen auf allen äußeren Deckflächen des Buchs erscheinen. Auf allen äußeren Deckflächen des Buchs soll der Name des Original-Herausgebers genauso groß sein wie der Titel der Arbeit und so einnehmend genannt werden im Hinblick auf den Titel.

COPYRIGHT

Das Copyright jeder Open Publication gehört dem Autor / den Autoren oder Zeichnungsberechtigten.

GÜLTIGKEITSBEREICH DER LIZENZ

Die nachfolgenden Lizenzregeln werden auf alle Open-Publication-Arbeiten angewendet, sofern nicht explizit anders lautend im Dokument erwähnt.

Die bloße Zusammenfassung von Open-Publication-Arbeiten oder eines Teils einer Open-Publication-Arbeit mit anderen Arbeiten oder Programmen auf dem selben Medium bewirkt nicht, daß die Lizenz auch auf diese anderen Arbeiten angewendet wird. Die zusammengefaßte Arbeit soll einen Hinweis enthalten, die die Aufnahme von Open-Publication-Material und eine geeignete Copyright-Notiz angibt.

ABTRENNBARKEIT. Wenn irgendein Teil dieser Lizenz durch irgendeine Rechtsprechung außer Kraft gesetzt werden, bleiben die verbleibenden Teile der Lizenz in Kraft.

KEINE GEWÄHRLEISTUNG. Open-Publication-Arbeiten werden lizensiert und verbreitet "wie sie sind" ohne Gewährleistung jeglicher Art, explizit oder implizit, einschließlich, aber nicht begrenzt auf, der impliziten Gewährleistung des Vertriebs und der Geignetheit für einen besonderen Zweck oder eine Gewähleistung einer non-infringement.

ERFORDERNISSE FÜR MODIFIZIERTE ARBEITEN

Alle modifizierten Versionen, die durch diese Lizenz abgedeckt werden, einschließlich von Übersetzungen, Anthologien, Zusammenstellungen und Teildokumenten, müssen die folgenden Erfordernisse erfüllen:

EMPFEHLUNGEN EINER GUTEN PRAXIS

In Ergänzung zu den Erfordernissen dieser Lizenz, wird von den Weiterverteilenden erwartet und ihnen stark empfohlen:

Schließlich, obwohl nicht erforderlich unter dieser Lizenz, ist es, eine vorgeschlagene gute Form eine kostenlose Kopie jedes Hardcopy- und CD-ROM-Ursprungs einer unter Open Publication lizensierten Arbeit dem Autor / den Autoren anzubieten.

LIZENZ-OPTIONEN

Der/die Autor/en und/oder der Herausgeber eines unter Open Publication lizensierten Dokuments darf bestimmte Optionen durch Anhängen von Regelungen an den Lizenz-Verweis oder die Lizenz-Kopie wählen. Diese Optionen sind empfohlener Teil der Lizenzbestimmungen und müssen in abgeleiteten Arbeiten in die Lizenz eingefügt werden.