Donnerstag, 9. Februar 2017

Data Driven 2

PHP verfügt über eine ganze Reihe unterschiedlicher Datenbankinterfaces, die sich leider aufgrund der Entstehungsgeschichte der Sprache in einigen Details unterscheiden und die außerdem bequemer zu bedienen sein könnten. Mit Hilfe einer Include-Datei und der objektorientierten Eigenschaften von PHP wollen wir uns ein Datenbankinterface schaffen, daß mit unterschiedlichen Datenbanken auf die gleiche Weise kommunizieren kann.

Bibliotheken bauen mit Objekten

Wenn man anfängt, sich eine Bibliothek von PHP-Funktionen in Includedateien zuzulegen, wird man schnell auf Probleme stoßen, sobald man auch Includes anderer PHP-Anwender hinzuzieht. Früher oder später wird es unweigerlich zu Kollisionen zwischen Funktionen oder globalen Variablen gleichen Namens kommen.
Aber selbst wenn man nur selbstgeschriebene Funktionen einsetzt, kann es zu Problemen kommen. Man stelle sich zum Beispiel eine Sammlung von Funktionen vor, die mit einem Datenbank-Link arbeiten. Abbildung 1 zeigt eine Sammlung solcher Funktionen, die mit einer globalen Variable $Link-ID arbeiten.
<?php
  var $Link_ID;  // ID der aktuellen DB-Verbindung
  var $Query_ID; // ID des aktuellen Abfrageresultates
  var $Error;    // Letzte Datenbank-Fehlermeldung

  function connect() { ... }

  function query()   { ... }

  function next_record() { ... }

  function num_rows() { ... }

?>
Eine hypothetische Sammlung von Funktionen zum Zugriff auf eine Datenbank
Wenn man sich eine Include-Datei mit diesen Funktionen definiert, kann man die query()-Funktion leicht so schreiben, daß sie prüft, ob die Link_ID definiert ist und ggf. connect()aufruft, um das Link zu initialisieren. query() könnte das Handle zum aktuellen Anfrageresultat in $Query_ID hinterlegen und next_record() könnte damit arbeiten. Damit das funktioniert, müssen diese Funktionen auf eine gemeinsame Variablen zugreifen können und da PHP keine Zeiger oder Referenzen kennt, ist es notwendig, daß es sich um eine gemeinsame globale Variable handelt.
Leider macht dies Probleme, sobald man auf einer Seite zwei verschiedene Datenbank-Abfragen verwenden möchte, denn diese würden sich die globalen Variablen gegenseitig überschreiben. Hätte PHP Zeigervariablen, könnte man jedem Aufruf von connect()query() und next_record() die entsprechenden Zeiger auf die zu verwendenden Variablen mitgeben, aber dann wäre kaum etwas gewonnen: Man müßte wieder Link_IDs und Query_IDs verwalten.
PHP bietet eine andere Methode: Man kann die zu einer Datenbankabfrage gehörenden Variablen und Funktionen zu einem Paket zusammenschnüren und dem Paket einen Namen geben. Dieser Vorgang selbst belegt noch keine Namen im Namensraum. Danach kann man quasi Kopien dieses Paketes unter einem wählbaren Variablennamen in den Namensraum der Sprache einhängen, ähnlich wie man eine Platte mountet. Das schnüren eines Paketes von Variablen und Funktionen nennt man in PHP das Definieren einer Klasse und das mounten dieses Paketes im Namensraum der Sprache ist das Erschaffen eines Objektes dieser Klasse mit new. In PHP sieht das so aus wie in Abbildung 2 gezeigt.
<?php
class DB_MiniSQL {
  var $Link_ID;  // ID der aktuellen DB-Verbindung
  var $Query_ID; // ID des aktuellen Abfrageresultates
  var $Error;    // Letzte Datenbank-Fehlermeldung

  function connect() { ... }

  function query()   { ... }

  function next_record() { ... }

  function num_rows() { ... }
}

$db1 = new DB_MiniSQL;
$db2 = new DB_MiniSQL;
?>
Definition einer Klasse DB_MiniSQL mit den Eigenschaften aus Abbildung 1, dann Schaffen von zwei Instanzen $db1 und $db2
Anders als in Abbildung 1 verbraucht die Deklaration einer Klasse keine Namen im globalen Namensraum - hinter der schliessenden Klammer der class-Anweisung ist also gar nichts passiert, außer daß wir PHP erklärt haben, was alles zu einem DB_MiniSQL gehört. PHP hat jetzt also einen Bauplan für DB_MiniSQL-Objekte.
Mit Hilfe der new-Anweisung sagen wir PHP, daß es ein DB_MiniSQL erschaffen und unter dem Namen $db1 in den Namensraum der Sprache legen soll. Ein zweites Objekt der gleichen Klasse soll unter dem Namen $db2 angelegt werden. Anders als bei der Variante mit den globalen Variablen kommt es hier nicht zur Kollision zwischen z.B. den beiden Link_IDs, da sich die beiden existierenden Link_IDs quasi in ihren "Pfadnamen" unterscheiden: Sie werden als $db1->Link_ID und $db2->Link_ID angesprochen.
Auch bei den Funktionen müssen wir angeben, welche der beiden Datenbankverbindungen wir verwenden wollen: $db1->query() sendet eine Anfrage über das eine Link, $db2->query() über das andere Link.
Dadurch, daß wir als Entwickler einer Bibliothek Funktionssammlungen in Include-Dateien in einer Klasse kapseln, überlassen wir es also dem Anwender unserer Bibliothek, wieviele Anwendungen unserer Bibliothek er gleichzeitig in Betrieb hat und unter welchem Namenspräfix er unsere Funktionen in seinen Scripten einblendet. Als Anwender einer solchen Objektbibliothek müssen wir uns nur daran gewöhnen, mit new ein Namenspräfix für die verwendeten Funktionen zu wählen (etwa: $db1 = new DB_MiniSQL) und dann statt eines Funktionsnamens query() den Namen mit diesem Präfix zusammen zu verwenden: $db1->query() bzw. $db2->query().
Um innerhalb der Funktion query() auf Variablen wie $Link_ID zuzugreifen, müßte man nun aber den eigenen Namen kennen. Schließlich müßte query() entscheiden, ob es auf $db1->Link_ID oder $db2->Link_IDoder mit einem ganz anderen Namen arbeiten soll. Das wäre allerdings übermäßig umständlich und man hat dies nicht so gelöst. Stattdessen kann innerhalb eines Objektes über  den festen Prefix $this auf die eigenen Variablen und Funktionen zugegriffen werden, unabhängig davon, unter welchem Namen sie außerhalb des Objektes sichtbar sind. Eine Funktion query() in einem Datenbank-Objekt kann also ihre eigene Link_ID als $this->Link_ID ansprechen und ihre eigene Funktion connect() als $this->connect() aufrufen.

Eine Datenbanklasse als Beispiel

Wir wollen dies am Beispiel einer Klasse DB_Sql zum Zugriff auf eine MySQL-Datenbank demonstrieren [1]. Unsere Klasse soll Variablen $Host$Database$User und $Password enthalten, die definieren, mit welcher Datenbank auf welchem Server kommuniziert werden soll und welcher Benutzer mit welchem Passwort sich dort anmelden soll. Das Resultat dieser Anmeldung wird eine $Link_ID sein, die ebenfalls in der Datenbank gespeichert werden muß.
Anfragen an die Datenbank werden entweder ein Resultat $Query_ID erzeugen, oder Fehlermeldungen, deren Text $Error und Fehlernummer $Errno ebenfalls verfügbar gemacht werden müssen. Wenn das Resultat der Datebankabfrage durchgelesen wird, wird man eine aktuelle Zeile in einem Array $Record zwischenspeichern wollen und man wird eine Zeilennummer $Row mitführen wollen. Unsere Klasse muß also intern schon einmal die in Abbildung 3 gezeigten Variablen anlegen - die Funktionen, die diese Variablen mit sinnvollen Werten belegen, stehen noch aus.
class DB_Sql {
  var $Host     = ""; // Host, auf dem MySQL läuft
  var $Database = ""; // zu verwendetende Database
  var $User     = ""; // User und Paßwort für Login
  var $Password = "";

  var $Link_ID  = 0;  // Resultat des mysql_connect()
  var $Query_ID = 0;  // Resultat des mysql_query()
  var $Record   = array();  // aktuelles mysql_fetch_array()-Ergebnis
  var $Row;           // Aktuelle Ergebniszeile

  var $Errno    = 0;  // Fehlerstatus der Anfrage...
  var $Error    = "";

  // Hier Funktionen einfügen
}
Definition und Erläuterung der in der Klasse DB_Sql verwendeten Variablen.
Damit man mit dieser Klasse etwas sinnvolles tun kann, wird man zunächst einmal eine Datenbankverbindung herstellen müssen. Dies kann schief gehen: Der Datenbankserver könnte nicht erreichbar sein, die gewünschte Datenbank könnte nicht vorhanden sein oder Username und Paßwort sind falsch. Die Klasse muß dann einen Fehler signalisieren und das Programm anhalten. Wir definieren eine Funktion halt(), der eine Fehlermeldung mitgegeben werden kann und die diese Meldung dann ausgibt und das Programm anhält und eine Funktion connect(), die versucht, die Link_ID zu initialisieren. Das Resultat ist in Abbildung 4 zu sehen.
  function halt($msg) {
    printf("</td></tr></table>");
    printf("<b>Database error:</b> %s<br>\n", $msg);
    printf("<b>MySQL Error</b>: %s (%s)<br>\n",
      $this->Errno,
      $this->Error);
    die("Session halted.");
  }

  function connect() {
    if ( 0 == $this->Link_ID ) {
      $this->Link_ID=mysql_connect($this->Host, $this->User, $this->Password);
      if (!$this->Link_ID) {
        $this->halt("Link-ID == false, connect failed");
      }
      if (!mysql_query(sprintf("use %s",$this->Database),$this->Link_ID)) {
        $this->halt("cannot use database ".$this->Database);
      }
    }
  }
Die Funktionen halt() und connect() sind in die Klasse DB_Sql einzufügen.
Die Funktion halt() gibt zunächst einmal eine Reihe von schließenden Tags aus, die helfen sollen, falls der Datenbankfehler in einer HTML-Tabelle auftritt. In diesem Fall würden viele Browser (Netscape zum Beispiel!) den Text nicht darstellen, weil die offene Tabelle bzw. Tabellenzelle nicht geschlossen würde. Nur im HTML-Quelltext der Seite wäre die Fehlermeldung sichtbar. Danach wird die Fehlermeldung ausgegeben und die Werte der Variablen $Errno und $Error des Objektes noch zusätzlich angezeigt. Die die()-Anweisung läßt den Interpreter dann anhalten, damit kein weiterer Schaden angerichtet wird.
Die Funktion connect() testet, ob bereits eine Link_ID existiert. Wenn dies nicht der Fall ist, wird unter Verwendung der Werte aus $Host$User und $Password versucht, eine Datenbankverbindung aufzubauen. Falls dies nicht gelingt, wird mit einem Fehler gestoppt, andernfalls wird versucht, die in $Database angegebene Datenbank mit einem use-Kommando zu aktivieren.

Klassen erweitern

Unsere Klasse ist nun schon benutzbar, wenn auch nicht in sinnvoller Weise. Man kann eine Datenbankverbindung aufbauen, aber ohne query()- und next_record()-Funktionen leider keine Werte abfragen. Wir wollen die Klasse dennoch schon einmal einsetzen, um uns mit ihrem Gebrauch vertraut zu machen. Abbildung 5 zeigt eine Methode, mit der man die Klasse verwenden könnte.
<?php
  // Definiert die DB_Sql-Klasse
  require("db_mysql.inc");

  // $db ist unser Datenbankobjekt
  $db = new DB_Sql;

  // Belegen der Connectparameter
  $db->Host     = "localhost";
  $db->User     = "kris";
  $db->Password = "";
  $db->Database = "wahl";

  // Verbinden mit der Datenbank.
  $db->connect();

 ?>
Eine (nicht empfohlene) Möglichkeit, DB_Sql anzuwenden.
Diese Methode ist jedoch nicht sehr empfehlenswert: Nach der Erzeugung des Objektes $db müssen seine Connectparameter einzeln und für jede Seite neu belegt werden, auf der DB_Sql verwendet wird. Es wäre schöner, wenn wir eine neue Datenbank-Klasse definieren könnten, die genauso ist wie DB_Sql, nur mit anderen Connect-Parametern. Tatsächlich können wird das: Wir können Klassen erweitern und dabei vorhandene Klassendefinitionen verwenden. Abbildung 6 zeigt die Definition einer Klasse DB_Wahl, die denselben Connect durchführt wie in Abbildung 5 gezeigt. Abbildung 6b zeigt das zu Abbildung 5äquivalente  Programm unter Verwendung von DB_Wahl.

// DB_Wahl ist genau wie DB_Sql, nur anders... :-)

class DB_Wahl extends DB_Sql {
  var $Host     = "localhost";
  var $User     = "kris";
  var $Password = "";
  var $Database = "wahl";
}


// Enthält DB_Sql.
require("db_mysql.inc");
// Enthält DB_Wahl
require("local.inc");

// Erstelle ein Datenbankobjekt
$db = new DB_Wahl;

// Verbinde mit der Datenbank
$db->connect(); 

Erstellung und Anwendung einer neuen Klasse DB_Wahl aus DB_Sql.
DB_Wahl ist nicht leer, sondern verfügt über genau dieselben Variablen und Funktionen wie DB_Sql, auch wenn diese in der Definition von DB_Wahl nicht mit aufgeführt sind. Erkennbar ist dies an der Deklaration der Klasse: DB_Wahl extends DB_Sql, d.h. zunächst einmal ist DB_Wahl ganz genauso wie DB_Sql definiert. In der weiteren DB_Wahl-Definition werden nun aber die Startwerte einiger Variablen überschrieben, nämlich mit den Connectparametern für die Wahl-Datenbank.
Wenn jetzt DB_Wahl verwendet wird, wie in Abbildung 6b gezeigt wird, wird eine Datenbankverbindung mit diesen Parametern aufgebaut. Anders als in Abbildung 5 müssen diese Parameter aber nicht mehr in jeder Datei neu aufgeführt werden, sondern können zentral an einer Stelle (der Definition von DB_Wahl in local.inc) gepflegt werden - besonders bei großen Projekten sehr empfehlenswert.

Abfragen und Abfrageresultate

  function query($Query_String) {
    $this->connect();

#   printf("Debug: query = %s<br>\n", $Query_String);

    $this->Query_ID = mysql_query($Query_String,$this->Link_ID);
    $this->Row   = 0;
    $this->Errno = mysql_errno();
    $this->Error = mysql_error();
    if (!$this->Query_ID) {
      $this->halt("Invalid SQL: ".$Query_String);
    }

    return $this->Query_ID;
  }

  function next_record() {
    $this->Record = mysql_fetch_array($this->Query_ID);
    $this->Row   += 1;
    $this->Errno = mysql_errno();
    $this->Error = mysql_error();

    $stat = is_array($this->Record);
    if (!$stat) {
      mysql_free_result($this->Query_ID);
      $this->Query_ID = 0;
    }
    return $stat;
  }

  function seek($pos) {
    $status = mysql_data_seek($this->Query_ID, $pos);
    if ($status)
      $this->Row = $pos;
    return;
  }
Die Funktionen query()next_record() und seek() sind der Definition von DB_Sql hinzuzufügen.

Abbildung 7 zeigt drei Funktionen, mit denen man DB_Sql dann endlich einer sinnvollen Verwendung zufügen kann: Nun ist es nämlich endlich möglich, tatsächlich Anfragen an eine Datenbank zu senden und Resultate abzuholen. Die Funktion query() ruft zu diesem Zweck erst einmal connect() auf. connect() wird eine Datenbankverbindung herstellen und die passende Datenbank auswählen, wenn dies noch nicht geschehen ist. Auf diese Weise sparen wir uns den manuellen Aufruf von connect() vor der ersten Anfrage auf einer Seite und können stattdessen einfach query() verwenden, ohne weiter nachdenken zu müssen.
Die auskommentierte Anweisung enthält Code, der die aktuelle Query ausdruckt. Wenn wir eine Anwendung debuggen müssen, kann es nützlich sein, alle SQL-Anfragen zu Gesicht zu bekommen. Durch Entfernen des Kommentarzeichens ist das leicht möglich.
Beim Absetzen einer neuen Query wird eine neue Query_ID erzeugt und die aktuelle Ergebniszeile auf Null gesetzt. Danach muß abgefragt werden, ob die Query legal war, d.h. ob sie gültiges SQL enthielt: Errno und Error werden aktualisiert. Wenn dabei ein Fehler auftrat, wird das Programm mit einer Fehlermeldung angehalten. Andernfalls wird die Query_ID an den Abfrager zurückgegeben.
Die next_record()-Funktion kann dann eingesetzt werden, um die Ergebnisse der Anfrage abzuholen. Sie liest jeweils eine Zeile des Ergebnisses, führt den Zeilenzähler mit und prüft auf das Auftreten von Fehlern. Wird das Ende des Anfrageergebnisses erreicht, d.h. ist $this->Record kein Array mehr, sondern leer, wird das Anfrageergebnis freigegeben, um Speicher zu sparen. next_record() liefert "true", solange noch Ergebnisse vorliegen und kann daher in einer while()-Schleife eingesetzt werden.
Mit Hilfe der seek()-Funktion ist es möglich, innerhalb einer Ergebnistabelle zu springen und so ein Anfrageergebnis mehrfach zu lesen oder Zeilen am Anfang der Ergebnistabelle zu überspringen. Abbildung 8 zeigt eine mögliche Anwendung von query() und next_record(), um Daten in einer Tabelle abzufragen.
<?php
  require("db_mysql.inc"); // DB_Sql
  require("local.inc");    // DB_Wahl

  $db = new DB_Wahl;
  $query = "select kennung, grafik, link, beschreibung from inserenten";
  $db->query($query);
 ?>

<table border=1 cellpadding=4 align=center bgcolor="#eeeeee">
  <tr>
    <th>Kennung</th>
    <th>Grafik</th>
    <th>Link</th>
    <th>Beschreibung</th>
  </tr>

  <?php while($db->next_record()): ?>
  <tr>
    <td><?php print $db->Record["kennung"] ?></td>
    <td><?php print $db->Record["grafik"] ?></td>
    <td><?php print $db->Record["link"] ?></td>
    <td><?php print $db->Record["beschreibung"] ?></td>
  </tr>
<?php endwhile ?>
</table>
Abfrage der Tabelle inserenten in der Datenbank wahl.
CREATE TABLE inserenten (
  id int(11) DEFAULT '0' NOT NULL auto_increment,
  kennung varchar(127) DEFAULT '' NOT NULL,
  grafik varchar(127) DEFAULT '' NOT NULL,
  link varchar(127) DEFAULT '' NOT NULL,
  beschreibung varchar(127) DEFAULT '' NOT NULL,
  PRIMARY KEY (id),
  KEY kennung (kennung),
);

CREATE TABLE bannerwechsel (
  pos int(11) DEFAULT '0' NOT NULL,
);
Definition der Tabelle inserten
Ein Webserver (z.B. das für diesen Kurs erstellte www.wahl.de [5]) enthalte Bannerwerbung mit Verweisen auf die Webserver von Inserenten. Die Werbebanner liegen als Gif-Bilder vor, deren Pfadnamen bekannt sind. In der Datenbank ist in einer Tabelle inserenten hinterlegt, welche Kunden Bannerwerbung auf diesem Server geschaltet haben und bei jedem Aufruf der Webseite sollen umlaufend Werbebanner ausgesucht und angezeigt werden. Die Inserententabelle enthält zu jedem Werbebanner eine Kennung, den Pfadnamen der Gif-Datei und den Link zum Server des Kunden sowie den zu verwendenden Alt-Text.
Abbildung 8 zeigt, wie mit Hilfe der DB_Wahl-Klasse diese Tabelle durchgelesen werden kann. Ausgegeben wird eine HTML-Tabelle aller Kundenkennungen mit den zugehörigen Bannerinformationen. Abbildung 8b zeigt die Definition der Tabelle inserenten sowie der Tabelle bannerwechsel. Die bannerwechsel-Tabelle enthält nur eine Zeile mit nur einer Spalte (also nur eine Zahl), nämlich die aktuelle Bannernummer. Das Banneranzeigeprogramm verwendet diese Information, um die Bannerrotation zu steuern.
Der Bannerrotator (Abbildung 9) besteht aus einer Funktion banner_rotate(), die nichts weiter macht, als den Zähler in der Bannerwechsel-Tabelle weiterzuzählen und auszulesen, um dann einen Image-Tag für das passende Banner zu generieren. Das dabei auftretende Locking ist MySQL-spezifisch (MySQL kennt keine richtigen Transaktionen).
Die Funktion ist ziemlich linear: Sie sperrt die bannerwechsel-Tabelle und bewegt den Zähler um eine Position weiter. Danach liest sie den aktuellen Zählerstand aus und entsperrt die Tabelle wieder. Mit dem Zählerstand, der modulo der Anzahl der Inserenten begradigt wird, wird aus der Tabelle inserenten der passende Inserent herausgesucht und ein Image-Tag erzeugt, der in ein Link verpackt ist. Dabei darf nicht direkt in die Präsentation des Inserenten gesprungen werden, sondern es muß ein Verweis auf ein weiteres lokales Programm erzeugt werden, das Klicks auf das Banner in der Datenbank registriert und dann erst mittels eines Redirect zur Präsentation des Inserenten verzweigt. Nur auf diese Weise kann dem Inserenten auch die Wirksamkeit seiner Werbung bewiesen werden.
<?php
function banner_rotate() {
  global $db; // Setzt voraus, daß ein globales Datenbank-Objekt existiert.

  $max_inserent = 4; // Konfigurier mich!

  $db->query("lock tables bannerwechsel");              // Lock setzen
  $db->query("update bannerwechsel set pos = pos + 1"); // Banner wechseln
  $db->query("select pos from bannerwechsel");          // Zähler auslesen
  $db->next_record();
  $pos = $db->Record["pos"];
  $db->query("unlock tables");                          // Lock entfernen

  // Inserent zum aktuellen Zählerstand (mod $max_inserent) suchen
  $query = sprintf("select * from inserenten where id = '%s'",
    $pos % $max_inserent);
  $db->query($query);
  $db->next_record();

  // Link und Image ausgeben
  printf("<a href=\"jump.php3?kennung=%s\">
          <img src=\"%s\" alt=\"%s\" width=468 height=60 border=0>
          </a>",
    $this->Record["kennung"],
    $this->Record["grafik"],
    $this->Record["beschreibung"]);
}

?>
Funktion banner_rotate() zum Wechseln von Werbebannern.

Das hier nicht gezeigte jump.php3-Script bekommt als Parameter die Kennung des geklickten Banners als GET-Parameter übergeben. Im Programm steht dieser als $kennung zur Verfügung. Mittels dieser Information kann das Programm die Information link aus der Tabelle inserenten extrahieren und einen Location-Header() dorthin generieren. Danach kann es in einer weiteren Tabelle banner Informationen über den Browser des Anklickers verewigen (etwa den Hersteller des Browsers, das Betriebssystem, auf dem er läuft, die Sprache und Version des Browsers sowie die $REMOTE_ADDR desjenigen, der geklickt hat. Diese Informationen können dann mit PHP abgefragt und aufbereitet werden.
<?php
  function num_rows() {
    return mysql_num_rows($this->Query_ID);
  }

  function num_fields() {
    return mysql_num_fields($this->Query_ID);
  }

  function f($Name) {
    return $this->Record[$Name];
  }

  function p($Name) {
    print $this->Record[$Name];
  }
?>
Weitere Funktionen der Klasse DB_Sql
Um die Klasse DB_Sql abzurunden [2], werden in Abbildung 10 noch einige Funktionen definiert, die den Zugriff auf die Resultate einer Query vereinfachen: Die Funktionen num_rows() und num_fields() geben die Höhe und Breite der Ergebnistabelle aus. Die Funktionen f() und p() können verwendet werden, um auf einzelne Felder der aktuellen Ergebniszeile zuzugreifen.

Grafik mit PHP

Das Script jump.php3, dessen Code wird nicht gezeigt haben, weil es zu trivial ist, erzeugt für jeden Klick auf ein Werbebanner einen Location-Header, der auf die Präsentation verweist und trägt dann die geklickte Kennung in einer Tabelle banner ein. Abbildung 11b zeigt die Struktur dieser Tabelle. Neben der tatsächlich angeklickten Kennung werden außerdem noch der Zeitpunkt des Klicks, die IP-Adresse des Abrufers sowie der Typ der von ihm verwendeten Browsersoftware abgespeichert.
Die Tabelle banner enthält eine Zeile für jeden Klick auf ein Werbebanner.
CREATE TABLE banner (
  id int(11) DEFAULT '0' NOT NULL auto_increment,
  datum timestamp(14),
  kennung varchar(127) DEFAULT '' NOT NULL,
  browser_kennung varchar(127) DEFAULT '' NOT NULL,
  remote_addr varchar(127) DEFAULT '' NOT NULL,
  PRIMARY KEY (id),
  KEY datum (datum),
  KEY kennung (kennung),
  KEY remote_addr (remote_addr)
);
Auswertung der banner-Tabelle: Welche Werbung wurde wie oft angeklickt?
+-------+---------+
| klick | kennung |
+-------+---------+
|   412 | blank   |
|   327 | kaufen  |
|   240 | film    |
|   105 | franken |
+-------+---------+
4 rows in set (0.13 sec)
Aus diesen Informationen kann man mit einer einfachen Query feststellen, welche Anzeige wie oft angeklickt worden ist. Ein SQL-Select, daß nach den Kennungen gruppiert, ermöglicht es in einem Statement festzustellen, welche Werbung wie oft angeklickt wurde:
select count(kennung) as klick, kennung 
       from banner 
       group by kennung 
       order by klick desc
Diese Anweisung erzeugt eine Ergebnistabelle mit zwei Spalten und so vielen Zeilen, wie angeklickte Kennungen existieren. Die erste Spalte, klick, enthält die Anzahl der Klicks für jede Anzeige. Die zweite Spalte enthält die Kennung der Anzeige. Die Ausgabe erfolgt so, daß die Werbung mit den meisten Klicks als erstes ausgegeben wird (Hitliste). Das Resultat sehe so aus, wie in Abbildung 11c gezeigt.
PHP erlaubt es, aus diesen Daten dynamisch ein GIF zu erzeugen, mit dem diese Zahlen in einem aussagekräftigen Balkendiagramm dargestellt werden [3]. Dazu muß PHP mit der GD Bibliothek von Thomas Boutell gelinkt werden [4] - nur dann stehen die entsprechenden Image-Befehle zur Verfügung, die in diesem Anwendungsbeispiel benutzt werden.
PHP-Scripte, die GIF-Bilder generieren wollen, können nur dieses eine GIF erzeugen. Sie können kein HTML generieren und sie können auch nicht mehr als Bild ausgeben. Daher kann es unter Umständen notwendig sein, den entsprechenden Code auf mehr als eine Datei zu verteilen. Ein PHP-Programm, das ein GIF generiert, beginnt immer mit einem Aufruf von 'header("image/gif")' und einem '$im = ImageCreate(breite, höhe);'. Die Header-Anweisung teilt dem PHP-Interpreter und dem Webserver mit, daß wir kein HTML erzeugen, sondern eine Bildatei. Die ImageCreate-Anweisung reserviert einen Zeichenpuffer, in dem wir dann malen können. Das Resultat ist ein Imagehandle, das wir bei allen weiteren Zeichenaufrufen mit angeben müssen. Am Ende unseres PHP-Scriptes müssen wir immer'ImageGif($im)' aufrufen: Das fertige GIF wird dann ausgegeben.
Solche PHP-Programme sind jedoch sehr schwer zu debuggen, weil Fehlermeldungen nun einmal "im Grafikmodus" sehr schwer zu lesen sind. Es ist daher sinnvoll, zum Debuggen die Header-Anweisung und den ImageGif()-Aufruf auszukommentieren und erst zu aktivieren, wenn das fertige Script läuft.
Um in einem Bild zeichnen zu können, muß man sich Farben zu definieren. GIF-Bilder sind Palettenbilder, d.h. man kann sich bis zu 256 verschiedene Stifte definieren, die eine beliebige Farbe haben können. Das Bild wird dann nicht mit den Farben gemalt, sondern mit den Stiften, die man sich definiert hat. Einen weißen Stift definiert man sich beispielsweise mit der Anweisung '$white = ImageColorAllocate($im, 255,255,255)': Anzugeben sind neben dem Imagehandle die RGB-Werte der gewünschten Farbe, die Funktion liefert dafür eine Stiftnummer.
Der mit ImageCreate() reservierte Bildspeicher ist nicht initialisiert: Im Puffer kann noch allerlei Speicherdreck stehen, der in irgendwelchen Farben dargestellt wird. Es ist sehr empfehlenswert, das Bild zunächst einmal mit der gewünschten Grundfarbe zu initialisieren. Dies kann zum Beispiel mittels 'ImageFilledRectangle($im, 0,0, breite,höhe, $white)' geschehen. Dabei ist zu beachten, daß alle Zeichenanweisungen mit einem Koordinatensystem arbeiten, bei dem der Punkt in der linken oberen Ecke des Bildes die Koordinaten 0,0 hat.
Die GD-Bibliothek kann in Bildern auch Texte setzen. Falls man sie standalone übersetzt hat, stehen dazu allerdings nur die sehr eingeschränkten eingebauten Zeichensätze zur Verfügung. Bindet man jedoch auch noch FreeType mit ein, kann man statt der beschränkten ImageString()-Funktion das flexiblere ImageTTFText() verwenden, das beliebige TTF-Zeichensätze verarbeiten kann.
Abbildung 11 zeigt ein PHP-Programm, das die in Abbildung 11b definierte banner-Tabelle auswertet und ein Balkendiagramm über die Klicks pro Anzeige generiert.
Generierung eines Balkendiagrammes (GIF) aus den Klickdaten der banner-Tabelle unter Verwendung der GD-Bibliothek
<?php

  $db = new DB_Wahl;
  $query = "select count(kennung) as klick, kennung 
              from banner 
          group by kennung 
          order by klick desc";
  $db->query($query);
  $db->num_rows();

  // Array $kennung erzeugen, Maximalwert bestimmen
  $max = 0;
  while($db->next_record()) {
    $kennung[$db->f("kennung")] = $db->f("klick");
    $max = $db->f("klick")>$max?$db->f("klick"):$max; 
  }

  // Bildpuffer und Farben allozieren
  $im    = ImageCreate(200, 220);
  $white = ImageColorAllocate($im, 255, 255, 255);
  $black = ImageColorAllocate($im, 0, 0, 0);
  for ($i=0; $i<$num; $i++) {
    $col[$i] = ImageColorAllocate($im, 
                 255-250/$num*$i, 250/$num*$i, 250/$num*$i);
  }

  // Hintergrundfarbe setzen
  ImageFilledRectangle($im, 0,0, $siz_x, $siz_y, $white);

  $i = 0;
  reset($kennung);
  while(list($k, $v) = each($kennung)) {
    // Säulenhöhe skalieren
    $h = $v/$max * (200);

    // Säule malen und schwarz umrahmen
    ImageFilledRectangle($im, $i*50+5, 205-$h, $i*50+45, 205, $col[$i]);
    ImageRectangle      ($im, $i*50+5, 205-$h, $i*50+45, 205, $black);

    // Bildunterschrift: Kennung, in der Säule: Höhe
    ImageString         ($im, 2, $i*50+5,  207, $k, $black);
    ImageString         ($im, 2, $i*50+10, 207-$h, round($v), $black);
    $i++;
  }

  ImageGif($im);
?>
In einer anderen Tabelle, zugriffe, sind überhaupt alle Zugriffe auf die Hauptseite dieses Webservers registriert worden. Durch eine leichte Abwandlung des SQL-Selects kann man aus dieser Tabelle mit dem o.a. Programm eine Zugriffstatistik nach Tagen erhalten:
  select dayofmonth(datum) as kennung, 
         count(dayofmonth(datum)) as klick
    from zugriffe 
group by kennung 
order by datum

In der Grafik läßt sich sehr schön die stark lokalisierte Aktivität des Webservers am Wahlsonntag und dem darauf folgenden Montag erkennen. Noch feiner aufgelöst ergibt sich eine stundenweise Zugriffskurve des Wahltages und des folgenden Montages:
  select date_format(datum, 'dH') as kennung,
         count(datum) as klick
    from zugriffe
   where date_format(datum, 'dH') between '2700' and '2900'
group by kennung
   order by datum


[1]
Der vollständige Code zur Klasse DB_Sql ist Bestandteil der Bibliothek PHPLIB ( http://phplib.shonline.de) und kann von der PHPLIB-Site geladen werden. PHPLIB enthält außerdem Versionen von DB_Sql für Postgres, ODBC und Oracle.
[2]
Der vollständige Code zur Klasse DB_Sql aus [1] enthält noch einige Funktionen mehr, die für das Beispiel hier aber nicht von Bedeutung sind.
[3]
PHPLIB Version-6 enthält eine Klasse, die außerdem Liniendiagramme erzeugen kann.
[4]
libgd, http://www.boutell.com/
[5]
http://www.wahl.de, ein Praktikumsprojekt im Rahmen eines Industriepraktikums bei SH Online.

Keine Kommentare:

Kommentar veröffentlichen