(Da es anscheinend immer noch Unklarheiten über den Begriff "Tutorial" gibt, hier mal ein Beispiel wie ein Tutorial aufgebaut sein sollte)
Ziel: eine einfache Datenbankklasse erstellen, über die leicht einfache Anfragen an die Datenbank gestellt werden können
Verwendete Techniken: PHP, MySQLi
Schwierigkeitsgrad: für Anfänger geeignet, Grundkenntnisse über die Funktionsweisen von PHP, OOP und SQL vorausgesetzt
Anmerkungen: keine PDO, weil in diesem Fall unnötig und für Anfänger nur bedingt geeignet. Kommentare und Fragen sind gerne gesehen, werden in den Beitrag eingebaut und danach gelöscht um den Thread übersichtlich zu halten.
Kommentare: der Code ist mit Blockkommentaren versehen, die die entsprechenden Attribute bzw. Methoden zusammenfassen. Diese Kommentare sind nach Konvention in englischer Sprache verfasst, für das Verständnis des Codes allerdings nicht notwendig, dafür gibt es das Tutorial. Da man Codes allerdings immer kommentieren sollte (vor allem wenn andere Leute ihn verwenden, aber auch für einen selbst) habe ich diese drin gelassen.
Fehlerbehandlung: die Klasse schreibt Fehler in ein error-Array der aktuellen Session, die dann an beliebiger Stelle ausgegeben werden können. Alternativ kann man die Fehler natürlich auch direkt ausgeben, dies kann aber je nach Seitenaufbau problembehaftet sein. Natürlich kann man dies auch anders lösen, z.B. mit einer weiteren Funktion error o.Ä.
Debugging: die meisten Methoden bekommen als letzten Parameter einen boolean-Wert $debug übergeben, der falls gesetzt dafür sorgt, dass Informationen zur momentanen Situation in ein debug-Array der aktuellen Session geschrieben werden, welches dann ähnlich wie das error-Array an beliebiger Stelle ausgegeben werden kann. Natürlich kann man dies auch anders lösen, z.B. mit einer weiteren Funktion debug o.Ä.
Schritt 1: Was soll die Klasse können?
Am Anfang steht immer die Frage "was will ich eigentlich erreichen?". Um das ganze einfach und kompakt zu halten (vorerst - das gute an OOP ist ja das Erweiterungen sehr einfach sind), beschränken wir uns auf die wesentlichen Funktionen: Datenbankverbindung herstellen, SELECT-, INSERT-, UPDATE- und DELETE-Anfragen.
Schritt 2: Struktur
Da wir die DB-Klasse ständig verfügbar haben wollen, die ohne jedes Mal eine Instanz z.B. als Funktionsargument zu übergeben benutzbar ist, sind die Attribute (Variablen) und Methoden (Funktionen) unserer Klasse statisch (Schlüsselwort static). Da wir die Klasse nicht instanziieren, brauchen wir keinen Konstruktor, stattdessen eine init-Methode, die die Datenbankverbindung aufbaut. Weiterhin möchten wir eine select-Methode, eine insert-Methode, eine update-Methode und eine delete-Methode, die jeweils entsprechende Queries ausführen, ohne dass wir sie jedes Mal selber schreiben müssen. Da diese Funktionen sehr einfach gestrickt sein sollen, möchten wir außerdem noch eine query-Methode für komplexere Anfragen (z.B. JOINs), die einfach eine übergebene Query ausführt und das Ergebnis ohne Aufbereitung zurückliefert. Als Klassenattribut brauchen wir vorerst nur eine noch leere Instanz der Datenbankverbindung, auf die alle Methoden zugreifen.
Schritt 3: Grundgerüst
<?php
class DB {
/**
* stores established database connection
* @var mysqli connection
*/
public static $db = null;
}
?>
Alles anzeigen
Unsere Klasse heißt ganz einfach DB (da wir statische Methoden haben ist es gut den Namen so kurz wie möglich zu halten, sonst muss man immer so viel tippen beim Aufrufen). Die Klasse hat ein Attribut $db, in welches wir später unsere MySQL-Verbindung speichern. Das Attribut ist private, da wir es lediglich in der Klasse selbst brauchen. Wir initialisieren auf null, da die Verbindung erst später in der init-Methode hergestellt wird.
Schritt 3: Hauptfunktionen
Die Hauptfunktionen unserer Klasse sind: init, select, insert, update, delete, query. Während dieses Tutorials werden wir über einige Hilfsfunktionen stolpern, diese werden dann nach allen Hauptfunktionen in Schritt 4 erklärt.
Schritt 3.1: init
/**
* init db connection
* @param string $dbHost db host
* @param string $dbUser db user
* @param string $dbPassword db password
* @param string $dbName db name
*/
public static function init($dbHost, $dbUser, $dbPassword, $dbName) {
self::$db = new mysqli($dbHost, $dbUser, $dbPassword, $dbName);
if (self::$db->connect_errno) {
$_SESSION['errors'][] = "MySQL connection failed: ". self::$db->connect_error;
}
self::$db->query("SET NAMES utf8;");
}
Alles anzeigen
Die init-Funktion bekommt vier Parameter übergeben: $dbHost (z.B. localhost), $dbUser und $dbPassword zur Authentifikation sowie den Namen der Datenbank auf die verbunden werden soll als $dbName. Mit diesen Parametern wird dann in Zeile 9 eine neue MySQLi-Verbindung geöffnet und in die $db-Variable der Klasse gespeichert. Falls es dabei einen Fehler gibt, wird dieser ins error-Array geschrieben.
Anschließend wird noch der Zeichensatz der aktuellen Verbindung auf utf-8 gesetzt, um Zeichensatzprobleme zu vermeiden.
Der Aufruf der init-Methode sollte idealerweise in einer config oder init-Datei erfolgen, auf jeden Fall aber vor der ersten Datenbankverbindung (logischerweise). Der Aufruf erfolgt wie folgt:
Schritt 3.1: select
/**
* select data from specified table
* @param string $table database table to select from
* @param array $columns colums to select, default all
* @param array $where where condition
* @param string $limit limit
* @return array fetched data
*/
public static function select($table, $columns = '*', $where = null, $limit = null, $debug = false) {
$sql = "SELECT " . self::generateColumnList($columns) . " FROM $table";
if ($where != null) {
$sql .= " WHERE ".$where;
}
if ($limit != null) {
$sql .= " LIMIT ".$limit;
}
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
$result = self::$db->query($sql);
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>select failed: ' . self::$db->error . '<br> statement was: <strong>' . $sql . '</strong></p>';
return array();
} else {
$ret = array();
while ($row = $result->fetch_assoc()) {
$ret[] = $row;
}
if (count($ret) == 1) {
return $ret[0];
} else {
return $ret;
}
}
}
Alles anzeigen
Die select Funktion bekommt eine ganze Reihe an Parametern übergeben: $table ist der Name der Tabelle, aus der selected werden soll, $columns (optional) ist entweder ein Array mit den Spalten oder ein String, falls nur eine Spalte ausgewählt werden soll, $where und $limit (beide optional) sind die entsprechenden WHERE und LIMIT Bedingungen, wenn gewünscht, zusätzlich der optionale $debug-Parameter.
Da wir in $columns entweder einen String oder ein Array haben, bedienen wir uns der generateColumnList-Methode (siehe 4.1), die aus dem übergebenen Wert einen für den SQL-Befehl verwendbaren String generiert. Falls die $where und $limit Parameter nicht null sind, wird an das SQL die entsprechende Bedingung angehängt, anschließend wird die Query ausgeführt. Falls ein Fehler auftritt wird dieser ins error-Array geschrieben und ein leeres Array zurückgegeben. Falls die Query fehlerfrei durchläuft, wird anschließend jede Zeile des Ergebnisses in das $ret-Array geschrieben, welches am Ende zurückgegeben werden soll.
Hat dieses Array, nachdem alle Ergebnisse hineingepackt wurden, nur ein Element, so wird nur dieses eine Element zurückgegeben, ansonsten das komplette Array.
Diese Funktion kann nun verschiedene Aufrufe handlen:
gibt einfach alle Einträge der user-Tabelle zurück
gibt alle Einträge der user-Tabelle zurück, bei denen die Spalte active auf 1 gesetzt ist.
gibt die ersten 20 username-Spalten der user-Tabelle zurück, bei denen die Spalte active auf 1 gesetzt ist, zusätzlich soll debugged werden.
Schritt 3.2: insert
/**
* insert an entry to the specified table
* @param string $table database table to insert into
* @param array $data data to insert
*/
public static function insert($table, $data, $debug = false) {
$keys = ""; $values = "";
foreach ($data as $key => $value) {
$key = self::escape($key);
$value = self::escape($value);
$keys .= $key . ", ";
if ($value == null) {
$values .= "null, ";
}
else {
$values .= "'" . $value . "', ";
}
}
$keys = rtrim($keys, ', ');
$values = rtrim($values, ', ');
$sql = "INSERT INTO $table (" . $keys . ") VALUES (" . $values . ")";
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
self::$db->query($sql);
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' . __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong> ' . $sql . '</strong></p>';
}
}
Alles anzeigen
Die insert-Methode bekommt zwei Parameter übergeben: $table ist der Tabellenname, $data ist ein Array mit den einzufügenden Daten. Optional kann noch der $debug Parameter übergeben werden.
Zuerst werden zwei leere Strings initialisiert, $keys und $values. Diese sollen später unsere keys und values für die Insert-Anweisung enthalten. In einer Schleife wird anschließend das $data-Array durchlaufen und die Werte an die entsprechenden Strings angehängt. Hier bedienen wir uns einer weiteren Hilfsfunktion escape (siehe 4.2), welche die übergebenen Strings für den Eintrag in die Datenbank escaped. Da wir beim Anhängen immer ein Komma mit angehängt haben aber natürlich am Ende der Strings kein Komma mehr wollen, entfernen wir anschließend mit der PHP-eigenen Funktion rtrim alle Kommas und Leerzeichen vom rechten Rand der beiden Strings.
Nun bauen wir die insert-Anweisung korrekt zusammen, speichern sie ggf. im debug-Array, führen sie aus und speichern eventuelle Fehler ab.
Der Aufruf erfolgt dann wie folgt:
Schritt 3.3: update
/**
* update an entry in specified table
* @param string $table database table to update
* @param string $id id of dataset to update
* @param array $data updated data
*/
public static function update($table, $id, $data, $debug = false) {
$sql = "UPDATE $table SET ";
foreach ($data as $key => $value) {
$key = self::escape($key);
$value = self::escape($value);
if ($value == null) {
$sql .= "$key = null, ";
} else {
$sql .= "$key = '$value', ";
}
}
$sql = rtrim($sql, ", ");
$sql .= " WHERE " . self::getPrimaryKeyColumn($table) . " = $id";
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
self::$db->query($sql);
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' . __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong> ' . $sql . '</strong></p>';
}
}
Alles anzeigen
Die update-Funktion arbeitet ähnlich wie die insert-Funktion (mit dem Unterschied, dass aufgrund der unterschiedlichen Syntax von insert- und update-Anweisungen der String direkt ohne Hilfsstrings zusammengebaut wird), bekommt aber als Parameter zusätzlich noch die id des zu updatenden Datensatzes übergeben. Dies erfordert die Existenz einer Primary Key Spalte (!!) in der Tabelle. Dies ist i.d.R. eine Spalte, die jeden Datensatz durch auto_increment mit einer ID versieht. Wir setzen eine weitere Hilfsfunktion getPrimaryKeyColumn (siehe 4.3) ein, welche uns den Namen dieser Spalte in der Tabelle $table zurückliefert, welche dann in der WHERE-Bedingung eingebaut wird, um den entsprechenden Datensatz zu updaten. Da die ID immer unique ist, kann man mit der update Funktion in dieser Form natürlich nur einzelnde Datensätze updaten. Für einfache Zwecke ist das auch erstmal ausreichend. Am Ende erfolgt die übrige Prozedur.
Der Aufruf erfolgt dann wie folgt:
Schritt 3.4: delete
/**
* delete data from specified table
* @param string $table database table to delete from
* @param string $id id of dataset to delete
*/
public static function delete($table, $id, $debug = false) {
$sql = "DELETE FROM $table WHERE " . self::getPrimaryKeyColumn($table) . " = $id";
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
self::$db->query($sql);
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' . __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong>' . $sql . '</strong></p>';
}
}
Alles anzeigen
Auch die delete-Funktion hat Ähnlichkeiten mit der update-Funktion, denn auch hier wird wieder eine Tabelle und die ID des zu löschenden Datensatzes übergeben. Daten brauchen wir nicht mehr, schließlich wollen wir ja löschen. Die Query wird wieder mit Hilfe der getPrimaryKeyColumn-Funktion aufgebaut und anschließend in gewohnter Manier ausgeführt.
Der Aufruf erfolgt dann wie folgt:
Schritt 3.5: query
/**
* querys sql on database
* @param string $sql sql to query
* @return mixed result set
*/
public static function query($sql, $debug = false) {
$result = self::$db->query($sql);
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong>' . $sql . '</strong></p>';
}
return $result;
}
Alles anzeigen
Für alle anderen SQL-Anfragen, die komplexer sind als die durch die anderen Funktionen abgedeckten Szenarien, fügen wir noch eine query-Funktion hinzu, die übergebenes SQL einfach ausführt.
Schritt 4: Hilfsfunktionen
Schritt 4.1: generateColumnList
/**
* generates list of columns from array
* @param array $columns array of columns
* @return string imploded array
*/
private static function generateColumnList($columns) {
if (is_array($columns)) {
return implode(', ', $columns);
} else {
return $columns;
}
}
Alles anzeigen
Da diese Funktion nur von innerhalb der Klasse zugänglich sein soll, ist sie private. Als Parameter kommt ein Array oder ein String an. Falls $columns ein Array ist, wird dieses per implode zu einem durch Kommas getrennten String gemacht, falls $columns ein String ist so wird dieser einfach unverändert wieder zurückgegeben. Die Funktionalität der Funktion ist so sehr einfach, allerdings könnte man z.B. sehr einfach noch eine weitere Bestandteile aufnehmen, wenn gewünscht (column as name Unterstützung o.Ä.).
Schritt 4.2: escape
/**
* escape given string
* @param string $string string to escape
* @return string escaped string
*/
public static function escape($string) {
return self::$db->real_escape_string($string);
}
Diese Funktion liefert einfach den escapeden String zurück. Auch hier könnte man noch weitere Dinge einbauen, z.B. Ersetzungen o.Ä. Da eine solche Funktion evtl. auch außerhalb der Klasse von Nutzen sein sollte, machen wir sie public.
Schritt 4.3: getPrimaryKeyColumn
/**
* get primary key column from given table
* @param string $table table to get primary key from
* @return string primary key column name
*/
public static function getPrimaryKeyColumn($table, $debug = false) {
$sql = "SHOW KEYS FROM $table WHERE key_name = 'PRIMARY'";
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
$result = self::$db->query($sql);
while ($row = $result->fetch_assoc()) {
return $row['Column_name'];
}
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' . __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong>' . $sql . '</strong></p>';
}
return false;
}
Alles anzeigen
Diese Funktion soll den Namen der PK-Spalte der übergebenen Tabelle zurückgeben. Dafür wird eine entsprechende Query zusammengebaut und anschließend die Spalte Column_name der ersten Zeile (mehr als eine sollte es eh nicht sein) des Ergebnisses zurückgegeben. Da auch diese Funktion evtl. noch anderweitig von Nutzen sein könnte, machen wir auch diese public.
Schritt 5: insertID
Ein bisher noch nicht genanntes 'Feature' unserer Klasse soll die sogenannte insertID werden. Diese ID wird von der Datenbank nach einem insert geliefert und ist die eindeutige ID des gerade eingefügten Datensatzes. Mithilfe der insertID kann man mit dem gerade eingefügten Datensatz sehr einfach weiter arbeiten.
Diese insertID wollen wir als öffentlich zugängliches Attribut unserer Klasse speichern, deshalb fügen wir nach der Deklaration des Attributes $db folgendes ein:
Zusätzlich fügen wir am Ende der insert-Funktion noch folgende Zeile ein:
dadurch wird das in der Datenbank-Instanz vorhandene Attribut insert_id in unsere Variable insertID geschrieben (die unterschiedliche Schreibweise ist hier reine persönliche Vorliebe - natürlich könnte man die Klassenvariable auch $insert_id nennen).
Schritt 6: Komplettcode
Zum Schluss der nun komplette Code nochmal im Überblick:
<?php
class DB {
/**
* stores established database connection
* @var mysqli connection
*/
public static $db = null;
/**
* id of last inserted row
* @var string
*/
public static $insertID;
/**
* initialise db connection
* @param string $dbHost db host
* @param string $dbUser db user
* @param string $dbPassword db password
* @param string $dbName db name
*/
public static function init($dbHost, $dbUser, $dbPassword, $dbName) {
self::$db = new mysqli($dbHost, $dbUser, $dbPassword, $dbName);
if (self::$db->connect_errno) {
$_SESSION['errors'][] = "MySQL connection failed: ". self::$db->connect_error;
}
self::$db->query("SET NAMES utf8;");
}
/**
* querys sql on database
* @param string $sql sql to query
* @return mixed result set
*/
public static function query($sql, $debug = false) {
$result = self::$db->query($sql);
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>'.$sql.'</strong>';
}
if (self::$db->errno) {
$_SESSION['errors'][] = "<p>insert failed: " . self::$db->error . "<br> statement was: <strong> $sql </strong></p>";
}
return $result;
}
/**
* insert an entry to the specified table
* @param string $table database table to insert into
* @param array $data data to insert
*/
public static function insert($table, $data, $debug = false) {
$keys = ""; $values = "";
foreach ($data as $key => $value) {
$key = self::escape($key);
$value = self::escape($value);
$keys .= $key . ", ";
if ($value == null) {
$values .= "null, ";
}
else {
$values .= "'" . $value . "', ";
}
}
$keys = rtrim($keys, ', ');
$values = rtrim($values, ', ');
$sql = "INSERT INTO $table (" . $keys . ") VALUES (" . $values . ")";
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
self::$db->query($sql);
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' . __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong> ' . $sql . '</strong></p>';
}
self::$insertID = self::$db->insert_id;
}
/**
* update an entry in specified table
* @param string $table database table to update
* @param string $id id of dataset to update
* @param array $data updated data
*/
public static function update($table, $id, $data, $debug = false) {
$sql = "UPDATE $table SET ";
foreach ($data as $key => $value) {
$key = self::escape($key);
$value = self::escape($value);
if ($value == null) {
$sql .= "$key = null, ";
} else {
$sql .= "$key = '$value', ";
}
}
$sql = rtrim($sql, ", ");
$sql .= " WHERE " . self::getPrimaryKeyColumn($table) . " = $id";
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
self::$db->query($sql);
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' . __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong> ' . $sql . '</strong></p>';
}
}
/**
* select data from specified table
* @param string $table database table to select from
* @param array $columns colums to select, default all
* @param array $where where condition
* @param string $limit limit
* @return array fetched data
*/
public static function select($table, $columns = '*', $where = null, $limit = null, $debug = false) {
$sql = "SELECT " . self::generateColumnList($columns) . " FROM $table";
if ($where != null) {
$sql .= " WHERE ".$where;
}
if ($limit != null) {
$sql .= " LIMIT ".$limit;
}
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
$result = self::$db->query($sql);
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>select failed: ' . self::$db->error . '<br> statement was: <strong>' . $sql . '</strong></p>';
return array();
} else {
$ret = array();
while ($row = $result->fetch_assoc()) {
$ret[] = $row;
}
if (count($ret) == 1) {
return $ret[0];
} else {
return $ret;
}
}
}
/**
* delete data from specified table
* @param string $table database table to delete from
* @param string $id id of dataset to delete
*/
public static function delete($table, $id, $debug = false) {
$sql = "DELETE FROM $table WHERE " . self::getPrimaryKeyColumn($table) . " = $id";
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
self::$db->query($sql);
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' . __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong>' . $sql . '</strong></p>';
}
}
/**
* escape given string
* @param string $string string to escape
* @return string escaped string
*/
public static function escape($string) {
return self::$db->real_escape_string($string);
}
/**
* get primary key column from given table
* @param string $table table to get primary key from
* @return string primary key column name
*/
public static function getPrimaryKeyColumn($table, $debug = false) {
$sql = "SHOW KEYS FROM $table WHERE key_name = 'PRIMARY'";
if ($debug == true) {
$_SESSION['debug'][] = __FUNCTION__ . ': $sql is <strong>' . $sql . '</strong>';
}
$result = self::$db->query($sql);
while ($row = $result->fetch_assoc()) {
return $row['Column_name'];
}
if (self::$db->errno) {
$_SESSION['errors'][] = '<p>' . __FUNCTION__ . ' failed: ' . self::$db->error . '<br> statement was: <strong>' . $sql . '</strong></p>';
}
return false;
}
/**
* generates list of columns from array
* @param array $columns array of columns
* @return string imploded array
*/
private static function generateColumnList($columns) {
if (is_array($columns)) {
return implode(', ', $columns);
} else {
return $columns;
}
}
}
?>
Alles anzeigen
Und fertig ist unsere sehr einfache Datenbankklasse - mehr Tutorials folgen