Ziel:
eine einfache Datenbankklasse erstellen, über die so einfach wie möglich Anfragen (Queries) an den Datenbankserver geschickt werden können.
Dabei sollen Platzhalter verwendet werden können, um SQL-Injections vermeiden zu können.
Verwendete Techniken:
PHP, PDO (siehe: http://php.net/manual/de/book.pdo.php)
Schwierigkeitsgrad:
für Anfänger geeignet, Grundkenntnisse über die Funktionsweisen von PHP, OOP und SQL vorausgesetzt
Fehlerbehandlung:
die Klasse wirft Exceptions, welche mit einem try/catch -Block abgefangen werden sollten.
Was soll die Klasse können?
Einfaches Verbinden zum Datenbankserver und Ausführen der Sql-Abfragen.
- Erweiterung "Persistente Verbindung" und Erweiterung "Initial Commands"
(wird evtl erweitert)
Struktur
Da wir die DB-Klasse ständig verfügbar haben wollen,
ohne jedes Mal eine Instanz z.B. als Funktionsargument übergeben zu müssen,
sind die Attribute (Variablen) und Methoden (Funktionen) unserer Klasse statisch (Schlüsselwort static).
Die Klasse und ihre Methoden (zur Übersicht erstmal ohne Inhalt):
class Sql
{
protected static // protected, und somit von außen nicht manipulierbar
$dbh; // später eine instanz (object) von der in PHP vordefinierten klasse PDO
public static function connect()
{
// hier soll die verbindung hergestellt werden
}
public static function close()
{
// hier soll die verbindung geschlossen werden (bei bedarf)
}
public static function exe()
{
// hier sollen sql queries ausgeführt werden
}
public function lastInsertId()
{
// hier wollen wir die lastInsertId aubrufen (zB nützlich nach einem INSERT INTO)
// siehe: http://php.net/manual/de/pdo.lastinsertid.php
}
}
Alles anzeigen
Die methode ::connect()
public static function connect($host, $username, $password, $database=null)
{
try{
// wenn $database übergeben wurde,
// dann wird beim herstellen der verbindung auch gleich die datenbank ausgewählt
// $database = (wenn) ? dann : sonst;
$database = ($database) ? ';dbname=' . $database : '';
// neue instanz der in PHP vordefinierten klasse PDO
// parameter werden übergeben:
// parameter 1: host und optional dbname
// zB "mysql:host=localhost;dbname=meine_datenbank"
// parameter 2: benutzername
// parameter 3: passwort
self::$dbh = new PDO('mysql:host=' . $host . $database, $username, $password);
// beim erstellen ist nichts "schief gelaufen".
// denn bei einem fehler hätte die in PHP vordefinierte klasse PDO eine exception geworfen
// und das script würde im catch(){} block weiter laufen.
// die verbindung sollte hergestellt worden sein
return;
}catch(PDOException $e){
// beim erstellen der neuen instanz der in PHP vordefinierten klasse PDO ist "was schief gelaufen"
// der fehler steckt im object $e
// die fehlermeldung bekommt man mit $e->getMessage(), den fehlercode mit $e->getCode()
// (zusätzlich gibt es noch $e->getFile(), $e->getLine(), und einen backtrace)
// wir haben hier den fehler (die Ausnahme (Exception)) "gefangen" (catch) -
// und werfen die fehlermeldung nach "draussen" weiter
throw new Exception($e->getMessage());
echo "hallo"; // das hier wird nie ausgegeben,
// denn entweder haben wir bei erfolg durch return -
// oder throw new Exception($e->getMessage()); diese methode verlassen
}
}
Alles anzeigen
Die methode ::close()
public static function close()
{
self::$dbh = null;
// wir setzen $dbh auf den wert NULL,
// wodurch der __destruct() der in PHP vordefinierten klasse PDO ausgeführt wird
}
Die methode ::exe()
public static function exe($sql, $para=null)
{
// parameter:
// $sql: (string) irgend eine sql-abfrage (keine multi-queries! wird von PDO nicht unterstützt)
// $para: (array) enthält die in der sql-query verwendeten platzhalter und deren werte
// $para Bsp: array( 'name' => 'hans', 'limit' => 1)
// hier erstellen wir eine kopie von $para. die copy wird später für die fehlerbehandlung genutzt
$para_copy = $para;
// die sql query "vorbereiten"
$stmt = self::$dbh->prepare($sql);
// die vorbereitete query "steck" nun als object in der variable $stmt (für statement)
// für genaueres siehe: http://php.net/manual/de/pdo.prepare.php
// $bind_para: momentan nur um platzhalter bei einem LIMIT nutzen zu können
// platzhalter wie zB LIMIT :x, :y müssen "gebunden" werden,
// der grund kurz gefasst:
// die sql-anweisung LIMIT erwartet integer (ganzzahlen).
// ohne bindParam() wird aber ein string in die query (an dessen platzhalter) eingefügt.
// wir würden also einen fehler bekommen, da LIMIT "1", "10" nicht valid ist.
// für genaueres siehe: http://php.net/manual/de/pdostatement.bindparam.php
//
// $bind_para = (wenn) ? dann : sonst;
$bind_para = ($para !== null
/* suche nach "... LIMIT :placeholder ...", oder "... limit :placeholder ..." */
and (strpos($sql, ' LIMIT :') !== false or strpos($sql, ' limit :') !== false)
) ? true : false;
// wenn array $para übergeben wurde und wir $bind_para auf true gesetzt haben
if($bind_para and is_array($para)){
// für jedes in $para als $key => &$val
// (& bewirkt, dass wir $val als referenz übernehmen)
// genaueres zum thema PDO:: konstanten siehe: http://php.net/manual/de/pdo.constants.php
foreach($para as $key => &$val){
if(is_string($val)){
$stmt->bindParam($key, $val, PDO::PARAM_STR);
}
elseif(is_bool($val)){
$stmt->bindParam($key, $val, PDO::PARAM_BOOL);
}
elseif(is_null($val)){
$stmt->bindParam($key, $val, PDO::PARAM_NULL);
}
elseif(is_numeric($val)){
$stmt->bindParam($key, $val, PDO::PARAM_INT);
}
else{
// ERGÄNZUNG 18.03.2015
// es gibt kein PDO::PARAM_FLOAT, also werden floats als string eingefügt (zb "0.1123")
// (string) vor der varibale $val bewirkt, dass der wert in $val in einen string gewandelt wird
$stmt->bindParam($key, (string)$val, PDO::PARAM_STR);
}
}
// $para wird nicht mehr gebraucht
$para = null;
}
// ausführen der sql-anweisung
// $para ist entweder ein array, oder NULL.
// übergeben von NULL zu ->execute() kommt keinem mitgegebenen parameter gleich
if(!$stmt->execute($para)){
// beim ausführen ist ein fehler aufgetreten.
// der fehler steckt in $stmt->errorInfo()
// $stmt->errorInfo() gibt ein array zurück: (siehe: http://php.net/manual/de/pdo.errorinfo.php)
/*
0 SQLSTATE error code (a five characters alphanumeric identifier defined in the ANSI SQL standard).
1 Driver-specific error code.
2 Driver-specific error message.
*/
// also setzen wir uns einen string aus diesem array zusammen:
$err_info = $stmt->errorInfo();
$sql_state = $err_info[0];
$ecode = $err_info[1];
$emsg = $err_info[2];
// um mit der meldung am ende etwas anfangen zu können,
// bezeichen wir diese werte in dem zu erstellenden string:
$sql_state = '(SQLSTATE: ' . $sql_state . ')';
$ecode = '(eCode: ' . $ecode . ')';
$emsg = 'eMessage: ' . $emsg;
// alles zusammen gefügt:
$error = $sql_state . ' ' . $emsg . ' ' . $ecode;
// und um später auch zu wissen bei welcher query dieser fehler auftrat setzen wir -
// die $sql und $para_copy (hier kommt die kopie von oben zum einsatz) hinten dran:
// überflüssige leerzeichen auf $sql entfernen
$sql = preg_replace('/\s+/', ' ', $sql);
// aus dem array $para_copy einen string erstellen:
$para_sring = '';
if($para_copy){
foreach($para_copy as $k => $v){
$para_sring .= ($para_sring === '') ? '' : '; ';
$para_sring .= ((strpos($k, ':') !== false) ? '' : ':') . $k . ' => ' . $v;
}
}
$error .= 'query: ' . $sql . ' para: ' . $para_sring;
// jetzt werfen wir den fehler nach "draußen"
// ! achtung: in den parametern $para könnten benutzerdaten wie zB passwörter enthalten sein!
// daher sollte der hier erstellte und nach draußßen geworfene error-string nicht ausgegeben werden!
throw new Exception($error);
}
// beim ausführen (->execute) trat kein fehler auf.
// jetzt holen wir uns alle datensätze (zeilen) die wir bekommen können.
// dabei setzen wit $result ersteinmal auf NULL.
// wenn nichts "zu holen" gab (wir waren nicht einmal in der while-schleife),
// dann gibt es kein ergebnis (also NULL)
$result = null;
while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
// beim ersten durchlauf setzen wir $result als array
if($result === null){
$result = array();
}
// sammeln der daten im array $result
$result[] = $row;
}
// hier setzen wir $stmt auf null (nicht unbedingt notwendig)
$stmt = null;
// fertig: ergebnis ($result) zurückgeben
return $result;
}
Alles anzeigen
Die methode ::lastInsertId()
public static function lastInsertId()
{
// hier gibts nicht viel zu tun.
// die in PHP vordefinierte klasse PDO stellt uns diese methode zur verfügung:
return self::$dbh->lastInsertId();
}
Das wars. Hier die ganze Klasse nocheinmal ohne Kommentare:
class Sql
{
protected static
$dbh;
public static function connect($host, $username, $password, $database=null)
{
try{
$database = ($database) ? ';dbname=' . $database : '';
self::$dbh = new PDO('mysql:host=' . $host . $database, $username, $password);
return;
}catch(PDOException $e){
throw new Exception($e->getMessage());
}
}
public static function close()
{
self::$dbh = null;
}
public static function exe($sql, $para=null)
{
$para_copy = $para;
$stmt = self::$dbh->prepare($sql);
$bind_para = ($para !== null
and (strpos($sql, ' LIMIT :') !== false or strpos($sql, ' limit :') !== false)
) ? true : false;
if($bind_para and is_array($para)){
foreach($para as $key => &$val){
if(is_string($val)){
$stmt->bindParam($key, $val, PDO::PARAM_STR);
}
elseif(is_bool($val)){
$stmt->bindParam($key, $val, PDO::PARAM_BOOL);
}
elseif(is_null($val)){
$stmt->bindParam($key, $val, PDO::PARAM_NULL);
}
elseif(is_numeric($val)){
$stmt->bindParam($key, $val, PDO::PARAM_INT);
}
else{ // PDO::PARAM_FLOAT does not exist. handle float as string
$stmt->bindParam($key, (string)$val, PDO::PARAM_STR);
}
}
$para = null;
}
if(!$stmt->execute($para)){
$err_info = $stmt->errorInfo();
$sql_state = $err_info[0];
$ecode = $err_info[1];
$emsg = $err_info[2];
$sql_state = '(SQLSTATE: ' . $sql_state . ')';
$ecode = '(eCode: ' . $ecode . ')';
$emsg = 'eMessage: ' . $emsg;
$error = $sql_state . ' ' . $emsg . ' ' . $ecode;
$sql = preg_replace('/\s+/', ' ', $sql);
$para_sring = '';
if($para_copy){
foreach($para_copy as $k => $v){
$para_sring .= ($para_sring === '') ? '' : '; ';
$para_sring .= ((strpos($k, ':') !== false) ? '' : ':') . $k . ' => ' . $v;
}
}
$error .= 'query: ' . $sql . ' para: ' . $para_sring;
throw new Exception($error);
}
$result = null;
while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
if($result === null){
$result = array();
}
$result[] = $row;
}
$stmt = null;
return $result;
}
public static function lastInsertId()
{
return self::$dbh->lastInsertId();
}
}
Alles anzeigen
Beispiele:
einen Fehler beim Verbindungsaufbau provozieren und fangen (catch-block)
try{
Sql::connect('localhost', 'root', 'einFalschesPasswort', 'datenbank_name');
}catch(Exception $e){
echo $e->getMessage();
}
Ausgabe:
einen Fehler bei Ausführen einer Query provozieren und fangen (catch-block)
(Vorraussetzung natürlich eine gültige Verbindung)
try{
Sql::connect('localhost', 'benutzername', 'password');
$sql = "SELECT irgendwas falsches;";
$rows = Sql::exe($sql);
var_dump($rows);
}catch(Exception $e){
echo $e->getMessage();
}
Ausgabe:
(SQLSTATE: 42S22) eMessage: Unknown column 'irgendwas' in 'field list' (eCode: 1054)query: SELECT irgendwas falsches; para:
Bsp für eine Query ohne Parameter:
try{
Sql::connect('localhost', 'benutzername', 'password');
$sql = "SELECT `table_schema` AS `Datenbankname` FROM `information_schema`.`tables` GROUP BY `table_schema`;";
$rows = Sql::exe($sql);
echo "<pre>";
var_dump($rows);
}catch(Exception $e){
echo $e->getMessage();
}
Ausgabe (Bsp):
array(7) {
[0] => array(1) {
["Datenbankname"] => string(5) "cdcol"
}
[1] => array(1) {
["Datenbankname"] => string(18) "information_schema"
}
[2] => array(1) {
["Datenbankname"] => string(5) "mysql"
}
[3] => array(1) {
["Datenbankname"] => string(18) "performance_schema"
}
[4] => array(1) {
["Datenbankname"] => string(10) "phpmyadmin"
}
[5] => array(1) {
["Datenbankname"] => string(4) "test"
}
[6] => array(1) {
["Datenbankname"] => string(7) "webauth"
}
}
Alles anzeigen
Bsp für eine Query mit Parameter:
try{
Sql::connect('localhost', 'benutzername', 'password');
$sql = "
SELECT `table_name`, `table_rows`
FROM `information_schema`.`tables`
WHERE `table_schema` = :table_schema
LIMIT :start_offset, :limit_rows
";
// wir haben 3 platzhalter in der query verwendet.
// also müssen wir auch 3 parameter an die methode ::exe() übergeben
$para = array(
'table_schema' => 'mysql',
'start_offset' => 0,
'limit_rows' => 3,
);
$rows = Sql::exe($sql, $para); // dieses mal geben wir $para mit
echo "<pre>";
var_dump($rows);
}catch(Exception $e){
echo $e->getMessage();
}
Alles anzeigen
Ausgabe (Bsp):
array(3) {
[0] => array(2) {
["table_name"] => string(12) "columns_priv"
["table_rows"] => string(2) "29"
}
[1] => array(2) {
["table_name"] => string(2) "db"
["table_rows"]=> string(1) "3"
}
[2] => array(2) {
["table_name"] => string(5) "event"
["table_rows"]=> string(1) "0"
}
}
Alles anzeigen
Und zuletzt nich ein unkommentiertes Beispiel:
try{
Sql::connect('localhost', 'benutzername', 'password');
}catch(Exception $e){
die('Verbindung konnte nicht hergestellt werden. Fehler: ' . $e->getMessage());
}
// ...
$sql = "
SELECT
`name`, `ort`, `plz`
FROM
`benutzer`
WHERE
`name` = :name,
AND `dieses` = :jenes
LIMIT 1
";
$para = array(
'name' => 'hans',
'jenes' => 'irgendwas',
);
try{
$rows = Sql::exe($sql, $para);
}catch(Exception $e){
die('Fehler bei .... Fehler: ' . $e->getMessage());
}
$benutzer = $rows[0];
$benutzer_name = $benutzer['name'];
// ...
if($benutzer_name == 'hans'){
$sql = "
INSERT INTO `benutzer_spezial`
SET `name` = :name
";
$para = array(
'name' => $benutzer_name,
);
try{
$rows = Sql::exe($sql, $para);
}catch(Exception $e){
die('Fehler bei .... Fehler: ' . $e->getMessage());
}
}
Alles anzeigen