Datenbankintegration (Webdevelopment): Unterschied zwischen den Versionen
K |
|||
(5 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
Zeile 1: | Zeile 1: | ||
= Kapitel X: Datenbankintegration mit PHP und MySQL = | = Kapitel X: Datenbankintegration mit PHP und MySQL = | ||
== | == Einleitung: Warum Datenbanken in PHP-Webanwendungen? == | ||
Moderne Webanwendungen sind selten statisch. Sie leben von dynamischen Inhalten, Benutzerinteraktionen und der Fähigkeit, Informationen über Sitzungen hinweg zu speichern und wieder abzurufen. Ob es sich um ein Content-Management-System (CMS), einen Online-Shop, ein soziales Netzwerk oder eine einfache Benutzerverwaltung handelt – die Persistenz von Daten ist entscheidend. Hier kommen Datenbanken ins Spiel. | Moderne Webanwendungen sind selten statisch. Sie leben von dynamischen Inhalten, Benutzerinteraktionen und der Fähigkeit, Informationen über Sitzungen hinweg zu speichern und wieder abzurufen. Ob es sich um ein Content-Management-System (CMS), einen Online-Shop, ein soziales Netzwerk oder eine einfache Benutzerverwaltung handelt – die Persistenz von Daten ist entscheidend. Hier kommen Datenbanken ins Spiel. | ||
Zeile 9: | Zeile 9: | ||
Eine der populärsten Datenbanklösungen im Webentwicklungsbereich, insbesondere in Kombination mit PHP, ist MySQL. Als relationales Datenbankmanagementsystem (RDBMS) bietet MySQL eine strukturierte Methode zur Datenspeicherung in Tabellen, die miteinander in Beziehung stehen können. Es ist ein Kernbestandteil vieler Webserver-Setups wie LAMP (Linux, Apache, MySQL, PHP) oder LEMP (Linux, Nginx, MySQL, PHP). | Eine der populärsten Datenbanklösungen im Webentwicklungsbereich, insbesondere in Kombination mit PHP, ist MySQL. Als relationales Datenbankmanagementsystem (RDBMS) bietet MySQL eine strukturierte Methode zur Datenspeicherung in Tabellen, die miteinander in Beziehung stehen können. Es ist ein Kernbestandteil vieler Webserver-Setups wie LAMP (Linux, Apache, MySQL, PHP) oder LEMP (Linux, Nginx, MySQL, PHP). | ||
Um die Brücke zwischen der PHP-Anwendung und der MySQL-Datenbank zu schlagen, stellt PHP spezielle Erweiterungen (Extensions) bereit. Diese Erweiterungen fungieren als Schnittstelle oder API (Application Programming Interface), die es PHP-Skripten ermöglichen, eine Verbindung zur Datenbank herzustellen, SQL-Befehle | Um die Brücke zwischen der PHP-Anwendung und der MySQL-Datenbank zu schlagen, stellt PHP spezielle Erweiterungen (Extensions) bereit. Diese Erweiterungen fungieren als Schnittstelle oder API (Application Programming Interface), die es PHP-Skripten ermöglichen, eine Verbindung zur Datenbank herzustellen, SQL-Befehle zu senden und die von der Datenbank zurückgegebenen Ergebnisse zu empfangen und zu verarbeiten. | ||
Dieses Kapitel führt | Dieses Kapitel führt in die Grundlagen der Datenbankintegration mit PHP und MySQL ein. Es enthält die verschiedenen verfügbaren PHP-Erweiterungen, wobei der Schwerpunkt auf den modernen und sicheren Ansätzen liegt: PDO (PHP Data Objects) und MySQLi (MySQL Improved). Es wird gezeigt, wie man Verbindungen aufbaut, grundlegende Datenbankoperationen – bekannt als CRUD (Create, Read, Update, Delete) – durchführt und wie man Daten sicher abfragt und manipuliert. Ein besonderes Augenmerk liegt dabei auf der Sicherheit, insbesondere auf der Vermeidung von SQL-Injection-Angriffen durch den Einsatz von Prepared Statements. Zum Abschluss erfolgt ein Überblick über nützliche PHP-Array-Funktionen zur Verarbeitung der abgerufenen Datenbankergebnisse. | ||
== | == Überblick über PHP-Datenbankerweiterungen == | ||
Die Art und Weise, wie PHP mit Datenbanken interagiert, hat sich im Laufe der Zeit weiterentwickelt. Ursprünglich gab es für jede populäre Datenbank eine eigene spezifische Erweiterung. Für MySQL waren dies die <code>mysql_*</code> Funktionen. Diese sind jedoch inzwischen veraltet und wurden durch modernere, flexiblere und sicherere Alternativen ersetzt. Heute stehen Entwicklern hauptsächlich zwei empfohlene Erweiterungen für die Arbeit mit MySQL zur Verfügung: PDO und MySQLi. | Die Art und Weise, wie PHP mit Datenbanken interagiert, hat sich im Laufe der Zeit weiterentwickelt. Ursprünglich gab es für jede populäre Datenbank eine eigene spezifische Erweiterung. Für MySQL waren dies die <code>mysql_*</code> Funktionen. Diese sind jedoch inzwischen veraltet und wurden durch modernere, flexiblere und sicherere Alternativen ersetzt. Heute stehen Entwicklern hauptsächlich zwei empfohlene Erweiterungen für die Arbeit mit MySQL zur Verfügung: PDO und MySQLi. | ||
* | * '''PDO (PHP Data Objects):''' Eine allgemeine Datenzugriffs-Abstraktionsschicht, die mit verschiedenen Datenbanksystemen verwendet werden kann. | ||
* | * '''MySQLi (MySQL Improved):''' Eine Erweiterung, die speziell für die Arbeit mit MySQL entwickelt wurde und dessen spezifische Funktionen unterstützt. | ||
* | * '''<code>mysql_*</code> Funktionen:''' Die ursprüngliche MySQL-Erweiterung, die seit PHP 7.0 entfernt wurde und nicht mehr verwendet werden sollte. | ||
Im Folgenden werden diese drei Optionen detaillierter vorgestellt. | Im Folgenden werden diese drei Optionen detaillierter vorgestellt. | ||
=== | === PDO (PHP Data Objects) === | ||
Die PDO-Erweiterung definiert eine leichtgewichtige und konsistente Schnittstelle für den Datenbankzugriff in PHP. Sie fungiert als ''Datenzugriffs-Abstraktionsschicht''. Das bedeutet, dass Entwickler unabhängig von der verwendeten Datenbank dieselben PDO-Funktionen nutzen können, um Abfragen auszuführen und Daten abzurufen.[1, 2] Es ist jedoch wichtig zu verstehen, dass PDO ''keine'' Datenbankabstraktion im Sinne einer SQL-Dialekt-Übersetzung oder der Emulation fehlender Datenbankfeatures bietet. Um eine Verbindung zu einer spezifischen Datenbank wie MySQL herzustellen, benötigt PDO einen entsprechenden datenbankspezifischen Treiber, z.B. <code>pdo_mysql</code>, der in der PHP-Konfiguration aktiviert sein muss.[1] | Die PDO-Erweiterung definiert eine leichtgewichtige und konsistente Schnittstelle für den Datenbankzugriff in PHP. Sie fungiert als ''Datenzugriffs-Abstraktionsschicht''. Das bedeutet, dass Entwickler unabhängig von der verwendeten Datenbank dieselben PDO-Funktionen nutzen können, um Abfragen auszuführen und Daten abzurufen.[1, 2] Es ist jedoch wichtig zu verstehen, dass PDO ''keine'' Datenbankabstraktion im Sinne einer SQL-Dialekt-Übersetzung oder der Emulation fehlender Datenbankfeatures bietet. Um eine Verbindung zu einer spezifischen Datenbank wie MySQL herzustellen, benötigt PDO einen entsprechenden datenbankspezifischen Treiber, z.B. <code>pdo_mysql</code>, der in der PHP-Konfiguration aktiviert sein muss.[1] | ||
'''Vorteile von PDO:''' | |||
* | *'''Datenbankunabhängigkeit:''' Der größte Vorteil von PDO ist die Möglichkeit, den Code für Datenbankinteraktionen weitgehend unverändert zu lassen, selbst wenn das zugrunde liegende Datenbanksystem gewechselt wird (z.B. von MySQL zu PostgreSQL).[1, 2] Dies erhöht die Portabilität und Flexibilität von PHP-Anwendungen erheblich.[2, 3] | ||
* | * '''Sicherheit durch Prepared Statements:''' PDO bietet eine robuste und konsistente Implementierung von Prepared Statements. Diese Technik ist der Goldstandard zur Verhinderung von SQL-Injection-Angriffen, da sie die SQL-Befehlsstruktur strikt von den übergebenen Daten trennt.[1, 2, 4, 5, 6] | ||
* | * '''Benutzerfreundlichkeit und Konsistenz:''' PDO stellt eine einheitliche API mit nützlichen Hilfsfunktionen bereit, wie z.B. vielfältige "Fetch Modes", um die Struktur der abgerufenen Daten zu steuern.[2] Die konsistente Funktionsweise über verschiedene Datenbanktreiber hinweg vereinfacht die Entwicklung. | ||
* | * '''Moderne Fehlerbehandlung:''' PDO verwendet standardmäßig Exceptions (<code>PDOException</code>) zur Signalisierung von Fehlern.[1, 2, 7] Dies ermöglicht eine strukturierte und moderne Fehlerbehandlung mittels <code>try...catch</code>-Blöcken, was als Best Practice gilt.[1, 2] | ||
* | '''Verbindungsaufbau mit PDO (Beispiel MySQL):''' | ||
* | * '''DSN (Data Source Name):''' Die Verbindungsparameter werden in einer Zeichenkette, dem DSN, übergeben. Für MySQL hat der DSN typischerweise das Format <code>mysql:host=hostname;dbname=datenbankname;charset=zeichensatz;unix_socket=pfad_zum_socket</code>. Die Angabe des Zeichensatzes (z.B. <code>utf8mb4</code>) ist wichtig für die korrekte Datenübertragung.[1, 7] Beispiel: <code>mysql:host=localhost;dbname=test;charset=utf8mb4</code>. | ||
* '''PDO-Konstruktor:''' Eine Verbindung wird durch Instanziierung der PDO-Klasse hergestellt: <code>$pdo = new PDO($dsn, $username, $password, $options);</code>.[1, 7, 8] Die Parameter sind der DSN, der Datenbankbenutzername, das Passwort und ein optionales Array mit Treiberoptionen. | |||
* | |||
* | '''Verbindungsoptionen (<code>$options</code>):''' Über dieses Array können wichtige Verhaltensweisen von PDO gesteuert werden: | ||
* | * <code>PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION</code>: Dies ist die empfohlene Einstellung für die Fehlerbehandlung. PDO wirft bei Fehlern eine <code>PDOException</code>, die abgefangen werden kann.[1, 2, 4] | ||
* | * <code>PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC</code>: Legt fest, dass Daten standardmäßig als assoziatives Array (<code>spaltenname => wert</code>) zurückgegeben werden, was oft die Weiterverarbeitung erleichtert.[1, 9] | ||
* <code>PDO::ATTR_EMULATE_PREPARES => false</code>: Deaktiviert die Emulation von Prepared Statements durch PDO und zwingt die Verwendung nativer Prepared Statements des Datenbanktreibers. Dies gilt als die sicherste und oft performanteste Methode.[1, 10, 9, 11] Standardmäßig ist die Emulation aktiviert (<code>true</code>). Obwohl PDO auch im emulierten Modus durch korrektes Escaping Schutz vor SQL Injection bietet [12, 5, 13], eliminiert die Deaktivierung potenzielle, wenn auch seltene, Sicherheitsrisiken, die in bestimmten Konstellationen (z.B. bei Verwendung exotischer Zeichensätze wie GBK [11]) auftreten könnten. Die explizite Deaktivierung ist daher eine wichtige Best Practice. | |||
* <code>PDO::ATTR_PERSISTENT => true</code>: Aktiviert persistente Datenbankverbindungen. Diese werden nicht am Ende des Skripts geschlossen, sondern in einem Pool gehalten und für nachfolgende Anfragen wiederverwendet. Dies kann die Performance von Webanwendungen verbessern, da der Overhead des Verbindungsaufbaus entfällt. Die Verwendung erfordert jedoch sorgfältige Überlegung, da der Zustand der Verbindung zwischen Anfragen bestehen bleiben kann.[7] | |||
'''Fehlerbehandlung:''' Der Verbindungsaufbau sollte immer in einem <code>try...catch</code>-Block erfolgen, um <code>PDOException</code> abzufangen und angemessen darauf zu reagieren (z.B. Fehlermeldung loggen, alternative Aktion ausführen).[1, 7] Es ist kritisch, detaillierte Fehlermeldungen in Produktionsumgebungen nicht an den Benutzer auszugeben, da sie sensible Informationen enthalten könnten. Die PHP-Einstellung <code>display_errors</code> sollte daher auf <code>0</code> gesetzt sein.[7] | |||
'''Verbindung schließen:''' Eine PDO-Verbindung bleibt für die Lebensdauer des PDO-Objekts aktiv. Um sie explizit zu schließen, müssen alle Referenzen auf das PDO-Objekt und alle davon abgeleiteten <code>PDOStatement</code>-Objekte entfernt werden, typischerweise durch Zuweisung von <code>null</code>.[7] PHP schließt die Verbindung automatisch am Ende des Skriptlaufs, wenn dies nicht manuell geschieht. | |||
'''Code-Beispiel (PDO-Verbindung zu MySQL):''' | |||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
<?php$host = 'localhost'; | <?php | ||
$host = 'localhost'; | |||
$dbname = 'meine_datenbank'; | $dbname = 'meine_datenbank'; | ||
$username = 'mein_benutzer'; | $username = 'mein_benutzer'; | ||
Zeile 53: | Zeile 59: | ||
try { | try { | ||
$pdo = new PDO($dsn, $username, $password, $options); | |||
// Erfolgreich verbunden, $pdo-Objekt kann nun verwendet werden. | |||
// echo "Verbindung erfolgreich hergestellt!"; | |||
} catch (\PDOException $e) { | } catch (\PDOException $e) { | ||
// Fehler beim Verbindungsaufbau | |||
// In Produktion: Fehler loggen, statt ausgeben | |||
error_log("Datenbankverbindungsfehler: ". $e--->getMessage()); | |||
die("Es gab ein Problem mit der Datenbankverbindung. Bitte versuchen Sie es später erneut."); | |||
} | } | ||
Zeile 67: | Zeile 73: | ||
// Verbindung explizit schließen (optional, da PHP dies am Skriptende tut) | // Verbindung explizit schließen (optional, da PHP dies am Skriptende tut) | ||
// $pdo = null; | // $pdo = null; | ||
? | ?> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Dieses Beispiel zeigt den empfohlenen Weg, eine PDO-Verbindung zu MySQL herzustellen, inklusive wichtiger Optionen für Fehlerbehandlung und Sicherheit sowie einem <code>try...catch</code>-Block zum Abfangen von Verbindungsfehlern. | Dieses Beispiel zeigt den empfohlenen Weg, eine PDO-Verbindung zu MySQL herzustellen, inklusive wichtiger Optionen für Fehlerbehandlung und Sicherheit sowie einem <code>try...catch</code>-Block zum Abfangen von Verbindungsfehlern. | ||
=== | === MySQLi (MySQL Improved) === | ||
Die MySQLi-Erweiterung ist, wie der Name schon sagt, speziell für die Interaktion mit MySQL-Datenbanken konzipiert und bietet Unterstützung für Funktionen, die ab MySQL Version 4.1 eingeführt wurden.[14, 15] Sie ermöglicht den Zugriff auf MySQL-spezifische Features, die über PDO möglicherweise nicht direkt verfügbar sind.[16, 15] | Die MySQLi-Erweiterung ist, wie der Name schon sagt, speziell für die Interaktion mit MySQL-Datenbanken konzipiert und bietet Unterstützung für Funktionen, die ab MySQL Version 4.1 eingeführt wurden.[14, 15] Sie ermöglicht den Zugriff auf MySQL-spezifische Features, die über PDO möglicherweise nicht direkt verfügbar sind.[16, 15] | ||
* | * '''Duale Schnittstelle:''' Ein charakteristisches Merkmal von MySQLi ist die Unterstützung von zwei Programmierstilen [17]: | ||
** | ** '''Prozedural:''' Dieser Stil ähnelt den alten <code>mysql_*</code> Funktionen. Die Funktionsnamen beginnen typischerweise mit <code>mysqli_</code> (z.B. <code>mysqli_connect()</code>, <code>mysqli_query()</code>). Der Datenbankverbindungs-Handle (eine Ressource oder ein Objekt) muss dabei explizit als erster Parameter an die meisten Funktionen übergeben werden.[17] Dieser Stil wird oft von Entwicklern bevorzugt, die von der alten <code>mysql</code>-Erweiterung migrieren.[17] | ||
** | ** '''Objektorientiert (OO):''' Hier wird ein Objekt der <code>mysqli</code>-Klasse erstellt, und Datenbankoperationen werden als Methoden dieses Objekts aufgerufen (z.B. <code>$mysqli->query()</code>, <code>$mysqli->prepare()</code>).[17] Dieser Ansatz passt oft besser zu modernen PHP-Entwicklungspraktiken und vermeidet die Notwendigkeit, den Verbindungshandle ständig zu übergeben. Die offizielle PHP-Dokumentation für MySQLi ist primär im objektorientierten Stil verfasst.[17] Es gibt keine nennenswerten Performance-Unterschiede zwischen den beiden Stilen; die Wahl ist meist eine Frage der persönlichen Präferenz oder Projektkonventionen.[17] | ||
* | '''Verbindungsaufbau mit MySQLi:''' | ||
* | * '''Prozedural:''' Die Funktion <code>mysqli_connect()</code> wird verwendet: <code>$link = mysqli_connect($host, $user, $pass, $db, $port, $socket);</code>. Sie gibt bei Erfolg ein <code>mysqli</code>-Objekt (oder <code>false</code> bei Fehlern vor PHP 8.1) zurück.[17, 18] Fehler können mit <code>mysqli_connect_errno()</code> und <code>mysqli_connect_error()</code> geprüft werden.[18] | ||
* '''Objektorientiert:''' Ein neues <code>mysqli</code>-Objekt wird instanziiert: <code>$mysqli = new mysqli($host, $user, $pass, $db, $port, $socket);</code>. Wichtig: Der Konstruktor gibt ''immer'' ein Objekt zurück, auch wenn die Verbindung fehlschlägt. Der Verbindungsstatus muss explizit über die Eigenschaften <code>$mysqli->connect_errno</code> und <code>$mysqli->connect_error</code> geprüft werden.[17, 18] | |||
* | |||
'''Fortgeschrittene Verbindung:''' Für feinere Kontrolle über Verbindungsoptionen (z.B. Setzen von Timeouts oder Initialisierungsbefehlen ''vor'' dem Verbindungsaufbau) können <code>mysqli_init()</code>, <code>mysqli_options()</code> und <code>mysqli_real_connect()</code> verwendet werden.[18, 19] | |||
* '''Fehlerbehandlung:''' Neben der manuellen Prüfung von Fehlercodes und -meldungen ist die empfohlene Methode, MySQLi so zu konfigurieren, dass es bei Fehlern Exceptions wirft. Dies geschieht mit <code>mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);</code>. Danach können Datenbankfehler elegant mit <code>try...catch</code>-Blöcken behandelt werden, ähnlich wie bei PDO.[18, 20, 21, 22, 23, 24, 25, 26] | |||
'''Verbindung schließen:''' Prozedural mit <code>mysqli_close($link)</code>, objektorientiert mit <code>$mysqli->close()</code>.[17, 18, 19] | |||
'''Persistente Verbindungen:''' MySQLi unterstützt ebenfalls persistente Verbindungen, die über PHP-Konfigurationseinstellungen (<code>mysqli.allow_persistent</code>, <code>mysqli.max_persistent</code>) gesteuert werden.[27] Standardmäßig setzt MySQLi den Zustand einer wiederverwendeten persistenten Verbindung zurück (via <code>mysqli::change_user()</code>), was zwar für Konsistenz sorgt, aber auch Performance kosten kann.[27] | |||
'''Code-Beispiele (MySQLi-Verbindungen):''' | |||
'''Objektorientiert (mit Exception-Handling):''' | |||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
<?php$host = 'localhost'; | <?php | ||
$host = 'localhost'; | |||
$user = 'mein_benutzer'; | $user = 'mein_benutzer'; | ||
$pass = 'mein_passwort'; | $pass = 'mein_passwort'; | ||
Zeile 97: | Zeile 110: | ||
try { | try { | ||
$mysqli = new mysqli($host, $user, $pass, $db); | |||
// Zeichensatz setzen (wichtig!) | |||
$mysqli--->set_charset('utf8mb4'); | |||
// Erfolgreich verbunden | |||
// echo "Verbindung erfolgreich (OO)! Host-Info: ". $mysqli->host_info; | |||
} catch (mysqli_sql_exception $e) { | } catch (mysqli_sql_exception $e) { | ||
// Fehler beim Verbindungsaufbau oder set_charset | |||
error_log("MySQLi Verbindungsfehler: ". $e->getMessage()); | |||
die("Es gab ein Problem mit der Datenbankverbindung."); | |||
} | } | ||
Zeile 111: | Zeile 124: | ||
// Verbindung schließen | // Verbindung schließen | ||
// $mysqli- | // $mysqli->close(); | ||
? | ?> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
** | ** '''Prozedural (mit Exception-Handling):''' | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
<?php$host = 'localhost'; | <!--?php$host = 'localhost'; | ||
$user = 'mein_benutzer'; | $user = 'mein_benutzer'; | ||
$pass = 'mein_passwort'; | $pass = 'mein_passwort'; | ||
Zeile 133: | Zeile 146: | ||
} catch (mysqli_sql_exception $e) { | } catch (mysqli_sql_exception $e) { | ||
// Fehler beim Verbindungsaufbau oder set_charset | // Fehler beim Verbindungsaufbau oder set_charset | ||
error_log("MySQLi Verbindungsfehler: ". $e->getMessage()); | error_log("MySQLi Verbindungsfehler: ". $e--->getMessage()); | ||
die("Es gab ein Problem mit der Datenbankverbindung."); | |||
} | } | ||
Zeile 141: | Zeile 154: | ||
// Verbindung schließen | // Verbindung schließen | ||
// mysqli_close($link); | // mysqli_close($link); | ||
? | ?> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
'''Bedeutung von <code>mysqlnd</code>:''' Die Benutzerfreundlichkeit, insbesondere bei der Arbeit mit Prepared Statements, wird stark durch den verwendeten MySQL-Treiber beeinflusst. Der '''MySQL Native Driver (<code>mysqlnd</code>)''', der seit PHP 5.4 standardmäßig enthalten ist [16], ist hier entscheidend. <code>mysqlnd</code> stellt die Methode <code>get_result()</code> (bzw. die Funktion <code>mysqli_stmt_get_result()</code>) zur Verfügung.[25, 15] Diese Methode erlaubt es, nach dem Ausführen eines Prepared Statements ein Standard-<code>mysqli_result</code>-Objekt zu erhalten, von dem dann wie gewohnt mit <code>fetch_assoc()</code>, <code>fetch_all()</code> etc. die Ergebnisse abgerufen werden können.[25] Ohne <code>mysqlnd</code> (bei Verwendung der älteren <code>libmysqlclient</code>-Bibliothek) ist das Abrufen von Ergebnissen aus Prepared Statements deutlich umständlicher und erfordert die manuelle Bindung jeder einzelnen Ergebnisspalte an PHP-Variablen mittels <code>bind_result()</code> und anschließendes <code>fetch()</code>.[25] Da <code>mysqlnd</code> heute der Standard ist, konzentrieren sich moderne Tutorials und Beispiele meist auf die <code>get_result()</code>-Methode, aber es ist wichtig, diesen Hintergrund zu kennen, falls man auf ältere Systeme oder Konfigurationen trifft. | |||
=== | === Veraltete <code>mysql_*</code> Funktionen === | ||
Die <code>mysql_*</code> Funktionsfamilie (<code>mysql_connect</code>, <code>mysql_query</code>, <code>mysql_fetch_assoc</code> etc.) war die ursprüngliche Methode, um in PHP mit MySQL-Datenbanken zu interagieren. Diese Erweiterung ist jedoch '''stark veraltet''' und sollte '''unter keinen Umständen mehr für neue Projekte verwendet werden'''. | Die <code>mysql_*</code> Funktionsfamilie (<code>mysql_connect</code>, <code>mysql_query</code>, <code>mysql_fetch_assoc</code> etc.) war die ursprüngliche Methode, um in PHP mit MySQL-Datenbanken zu interagieren. Diese Erweiterung ist jedoch '''stark veraltet''' und sollte '''unter keinen Umständen mehr für neue Projekte verwendet werden'''. | ||
* | * '''Status:''' Die <code>mysql_*</code> Erweiterung wurde in '''PHP 5.5''' offiziell als "deprecated" (veraltet) markiert, was bedeutet, dass ihre Verwendung ab dieser Version Warnungen (<code>E_DEPRECATED</code>) erzeugte.[28, 29] Mit der Veröffentlichung von '''PHP 7.0''' wurde die Erweiterung '''vollständig entfernt'''.[28, 29] PHP-Code, der diese Funktionen nutzt, führt auf PHP 7.0 oder neuer zu fatalen Fehlern und funktioniert nicht mehr.[28, 29] | ||
* | * '''Gründe für die Entfernung:''' Es gab zwingende Gründe für diesen Schritt: | ||
** | ** '''Gravierende Sicherheitsrisiken:''' Der Hauptgrund war die Anfälligkeit für SQL-Injection-Angriffe. Die <code>mysql_*</code> Funktionen boten keinen eingebauten Mechanismus wie Prepared Statements. Entwickler mussten Benutzereingaben manuell mit <code>mysql_real_escape_string()</code> "escapen", eine Methode, die leicht vergessen oder falsch angewendet werden konnte, was zu weit verbreiteten Sicherheitslücken führte.[29, 30] Das Fehlen von Prepared Statements in dieser alten Erweiterung ist direkt ursächlich für die hohe Zahl an SQL-Injection-Schwachstellen in älteren PHP-Anwendungen.[29] | ||
** | ** '''Fehlende Unterstützung moderner MySQL-Features:''' Die Erweiterung wurde ursprünglich für MySQL Version 3.23 entwickelt und seitdem kaum weiterentwickelt.[29] Sie unterstützte viele wichtige Funktionen moderner MySQL-Versionen nicht, darunter Prepared Statements, Stored Procedures, Transaktionen, SSL-Verschlüsselung, Kompression, Multi-Statements und vollständige Zeichensatzunterstützung.[29] | ||
** | ** '''Schlechte Wartbarkeit:''' Der Code der Erweiterung war veraltet und wurde immer schwieriger zu warten und an neue Versionen der MySQL-Client-Bibliotheken anzupassen.[29] | ||
** | ** '''Förderung unsicherer Praktiken:''' Da viele alte Tutorials und Codebeispiele die <code>mysql_*</code> Funktionen unsicher verwendeten, trug die fortgesetzte Verfügbarkeit zur Verbreitung schlechter und unsicherer Programmierpraktiken bei.[29] | ||
* | * '''Verwendung heute:''' Die klare Antwort lautet: '''Nein'''. Diese Funktionen existieren in modernen PHP-Versionen nicht mehr.[31, 30] Jede Anwendung, die sie noch verwendet, ist nicht nur unsicher, sondern auch inkompatibel mit aktueller PHP-Software. Eine Migration zu PDO oder MySQLi ist unumgänglich, um Sicherheit und Kompatibilität zu gewährleisten.[28, 29] Die erzwungene Migration durch die Entfernung in PHP 7.0 unterstreicht, wie wichtig es ist, Deprecation-Warnungen ernst zu nehmen und Code proaktiv zu aktualisieren, um zukünftige Probleme zu vermeiden.[29, 32] | ||
=== Tabelle 1: Vergleich der PHP-Datenbankerweiterungen für MySQL === | === Tabelle 1: Vergleich der PHP-Datenbankerweiterungen für MySQL === | ||
Zeile 162: | Zeile 175: | ||
{| class="wikitable" | {| class="wikitable" | ||
! Merkmal !! PDO (PHP Data Objects) !! MySQLi (MySQL Improved) !! <code>mysql_*</code> (Veraltet) | ! Merkmal !! PDO (PHP Data Objects) !! MySQLi (MySQL Improved) !! <code>mysql_*</code> (Veraltet) | ||
|- | |-<br> | ||
| aktiv="" empfohlen="" | | | aktiv="" empfohlen="" | | ||
<br> | <br> | ||
Zeile 168: | Zeile 181: | ||
<br> | <br> | ||
| Entfernt (seit PHP 7.0) [29] | | Entfernt (seit PHP 7.0) [29] | ||
|- | |-<br> | ||
| objektorientiert="" oo="" | | | objektorientiert="" oo="" | | ||
<br> | <br> | ||
Zeile 174: | Zeile 187: | ||
<br> | <br> | ||
| Prozedural | | Prozedural | ||
|- | |-<br> | ||
| multi-datenbank="" mysql="" pgsql="" sqlite="" | | | multi-datenbank="" mysql="" pgsql="" sqlite="" | | ||
<br> | <br> | ||
Zeile 180: | Zeile 193: | ||
<br> | <br> | ||
| Nur MySQL | | Nur MySQL | ||
|- | |-<br> | ||
| ja="" nativ="" empfohlen="" emulation="" m="" glich="" 1="" 11="" | | | ja="" nativ="" empfohlen="" emulation="" m="" glich="" 1="" 11="" | | ||
<br> | <br> | ||
Zeile 190: | Zeile 203: | ||
<br> | <br> | ||
| Sehr gut (durch Prepared Statements) [25] || Schlecht (manuelles Escaping nötig) [29] | | Sehr gut (durch Prepared Statements) [25] || Schlecht (manuelles Escaping nötig) [29] | ||
|- | |-<br> | ||
| gute="" unterst="" tzung="" via="" treiber="" | | | gute="" unterst="" tzung="" via="" treiber="" | | ||
<br> | <br> | ||
| Sehr gute Unterstützung (spezifisch) [15] || Veraltet, viele Features fehlen [29] | | Sehr gute Unterstützung (spezifisch) [15] || Veraltet, viele Features fehlen [29] | ||
|- | |-<br> | ||
| hoch="" 2="" | | | hoch="" 2="" | | ||
<br> | <br> | ||
Zeile 200: | Zeile 213: | ||
<br> | <br> | ||
| Niedrig | | Niedrig | ||
|- | |-<br> | ||
| exceptions="" mwt-preservehtml="" pdoexception="" code="" | | | exceptions="" mwt-preservehtml="" pdoexception="" code="" | | ||
<br> | <br> | ||
Zeile 206: | Zeile 219: | ||
<br> | <br> | ||
| Fehlercodes / <code>mysql_error()</code> | | Fehlercodes / <code>mysql_error()</code> | ||
|- | |-<br><br><br> | ||
| '''Nicht verwenden!''' [29] | | '''Nicht verwenden!''' [29] | ||
|} | |} | ||
== | == Grundlegende Datenbankoperationen (CRUD) == | ||
Nachdem wir die verschiedenen Erweiterungen kennengelernt haben, wenden wir uns nun den grundlegenden Operationen zu, die man typischerweise mit einer Datenbank durchführt. Diese werden oft mit dem Akronym CRUD zusammengefasst: | Nachdem wir die verschiedenen Erweiterungen kennengelernt haben, wenden wir uns nun den grundlegenden Operationen zu, die man typischerweise mit einer Datenbank durchführt. Diese werden oft mit dem Akronym CRUD zusammengefasst: | ||
* | * '''C'''reate: Neue Datensätze erstellen (SQL: <code>INSERT</code>). | ||
* | * '''R'''ead: Vorhandene Datensätze lesen/abfragen (SQL: <code>SELECT</code>). | ||
* | * '''U'''pdate: Bestehende Datensätze ändern (SQL: <code>UPDATE</code>). | ||
* | * '''D'''elete: Datensätze löschen (SQL: <code>DELETE</code>). | ||
In den folgenden Abschnitten werden wir sehen, wie diese Operationen mit den modernen Erweiterungen PDO und MySQLi umgesetzt werden, wobei wir durchgehend '''Prepared Statements''' verwenden, um die Sicherheit zu gewährleisten. | In den folgenden Abschnitten werden wir sehen, wie diese Operationen mit den modernen Erweiterungen PDO und MySQLi umgesetzt werden, wobei wir durchgehend '''Prepared Statements''' verwenden, um die Sicherheit zu gewährleisten. | ||
=== | === Daten abfragen (SELECT) === | ||
Das Abfragen von Daten ist eine der häufigsten Datenbankoperationen. | Das Abfragen von Daten ist eine der häufigsten Datenbankoperationen. | ||
* | * '''PDO:''' | ||
** | ** '''Ohne Parameter:''' Wenn die Abfrage keine variablen Teile enthält (z.B. keine <code>WHERE</code>-Klausel mit Benutzereingaben), kann die <code>query()</code>-Methode verwendet werden. Sie führt die SQL-Abfrage direkt aus und gibt ein <code>PDOStatement</code>-Objekt zurück, das zur Ergebnisauswertung dient.[2, 33] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// Beispiel: Alle Benutzer auswählen | // Beispiel: Alle Benutzer auswählen | ||
$stmt = $pdo- | $stmt = $pdo->query("SELECT id, username FROM users"); | ||
// $stmt kann nun zum Fetchen der Ergebnisse verwendet werden | // $stmt kann nun zum Fetchen der Ergebnisse verwendet werden | ||
</syntaxhighlight> | </syntaxhighlight> | ||
** | ** '''Mit Parametern (Prepared Statement):''' Dies ist der Standardfall, wenn Benutzereingaben oder andere Variablen die Abfrage beeinflussen (z.B. in <code>WHERE</code>- oder <code>LIMIT</code>-Klauseln). | ||
* | * '''Vorbereiten (<code>prepare</code>):''' Die SQL-Abfrage wird mit Platzhaltern (<code>?</code> für positionale oder <code>:name</code> für benannte Platzhalter) vorbereitet.[34, 33, 5] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// Beispiel: Benutzer mit bestimmter ID auswählen (positionaler Platzhalter) | // Beispiel: Benutzer mit bestimmter ID auswählen (positionaler Platzhalter) | ||
$sql = "SELECT id, username, email FROM users WHERE id =?"; | $sql = "SELECT id, username, email FROM users WHERE id =?"; | ||
$stmt = $pdo- | $stmt = $pdo->prepare($sql); | ||
// Beispiel: Benutzer mit bestimmtem Status auswählen (benannter Platzhalter) | // Beispiel: Benutzer mit bestimmtem Status auswählen (benannter Platzhalter) | ||
$sql_named = "SELECT id, username FROM users WHERE status = :status"; | $sql_named = "SELECT id, username FROM users WHERE status = :status"; | ||
$stmt_named = $pdo- | $stmt_named = $pdo->prepare($sql_named); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
*# | *# '''Ausführen (<code>execute</code>):''' Die vorbereitete Anweisung wird mit den tatsächlichen Werten ausgeführt. Die Werte werden als Array an die <code>execute()</code>-Methode übergeben. Bei positionalen Platzhaltern entspricht die Reihenfolge im Array der Reihenfolge der <code>?</code> in der SQL-Abfrage. Bei benannten Platzhaltern wird ein assoziatives Array verwendet, dessen Schlüssel den Platzhalternamen entsprechen (mit oder ohne führenden Doppelpunkt).[34, 33, 5] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// Ausführen für positionalen Platzhalter | // Ausführen für positionalen Platzhalter | ||
$user_id = 123; | $user_id = 123; | ||
$stmt- | $stmt->execute([$user_id]); | ||
// Ausführen für benannten Platzhalter | // Ausführen für benannten Platzhalter | ||
$user_status = 'active'; | $user_status = 'active'; | ||
$stmt_named- | $stmt_named->execute([':status' => $user_status]); | ||
// oder: $stmt_named- | // oder: $stmt_named->execute(['status' => $user_status]); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
*# | *# '''Alternative Bindung:''' Optional können Parameter auch explizit vor dem <code>execute()</code>-Aufruf mit <code>bindValue()</code> (bindet einen Wert) oder <code>bindParam()</code> (bindet eine Variable als Referenz) gebunden werden. Dies gibt mehr Kontrolle über den Datentyp (z.B. <code>PDO::PARAM_INT</code>, <code>PDO::PARAM_STR</code>).[5, 13] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
$stmt- | $stmt->bindValue(1, $user_id, PDO::PARAM_INT); // Position 1, Wert von $user_id als Integer | ||
$stmt- | $stmt->execute(); | ||
$stmt_named- | $stmt_named->bindParam(':status', $user_status, PDO::PARAM_STR); // Platzhalter :status, Variable $user_status als String | ||
$stmt_named- | $stmt_named->execute(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
** | ** '''MySQLi:''' | ||
*** | *** '''Ohne Parameter:''' Ähnlich wie bei PDO kann bei Abfragen ohne variable Teile die <code>query()</code>-Methode (OO-Stil: <code>$mysqli->query()</code>) oder die <code>mysqli_query()</code>-Funktion (prozeduraler Stil: <code>mysqli_query($link, "...")</code>) verwendet werden. Sie geben ein <code>mysqli_result</code>-Objekt zurück.[20, 25] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// OO-Stil | // OO-Stil | ||
$result = $mysqli- | $result = $mysqli->query("SELECT id, username FROM users"); | ||
// Prozedural | // Prozedural | ||
$result = mysqli_query($link, "SELECT id, username FROM users"); | $result = mysqli_query($link, "SELECT id, username FROM users"); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
*** | *** '''Mit Parametern (Prepared Statement):''' | ||
** | ** '''Vorbereiten (<code>prepare</code>):''' Die SQL-Abfrage wird mit <code>?</code>-Platzhaltern vorbereitet. Im OO-Stil <code>$stmt = $mysqli->prepare("...")</code>, prozedural <code>$stmt = mysqli_prepare($link, "...")</code>.[25, 35] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// OO-Stil | // OO-Stil | ||
$sql = "SELECT id, username, email FROM users WHERE id =?"; | $sql = "SELECT id, username, email FROM users WHERE id =?"; | ||
$stmt = $mysqli- | $stmt = $mysqli->prepare($sql); | ||
// Prozedural | // Prozedural | ||
$sql_proc = "SELECT id, username, email FROM users WHERE id =?"; | $sql_proc = "SELECT id, username, email FROM users WHERE id =?"; | ||
$stmt_proc = mysqli_prepare($link, $sql_proc); | $stmt_proc = mysqli_prepare($link, $sql_proc); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
**# | **# '''Parameter binden (<code>bind_param</code>):''' Die Variablen werden an die Platzhalter gebunden. Dies ist ein wesentlicher Unterschied zu PDOs <code>execute</code>-Array-Bindung. <code>bind_param()</code> erfordert einen String, der die Datentypen der zu bindenden Variablen angibt (z.B. "i" für Integer, "s" für String, "d" für Double/Float, "b" für Blob), gefolgt von den Variablen selbst.[25, 35] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
$user_id = 123; | $user_id = 123; | ||
// OO-Stil | // OO-Stil | ||
$stmt- | $stmt->bind_param("i", $user_id); // "i" für Integer | ||
// Prozedural | // Prozedural | ||
mysqli_stmt_bind_param($stmt_proc, "i", $user_id); | mysqli_stmt_bind_param($stmt_proc, "i", $user_id); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
**# | **# '''Ausführen (<code>execute</code>):''' Die vorbereitete Anweisung wird ausgeführt. OO-Stil: <code>$stmt->execute()</code>, prozedural: <code>mysqli_stmt_execute($stmt)</code>.[25, 36, 35] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// OO-Stil | // OO-Stil | ||
$stmt- | $stmt->execute(); | ||
// Prozedural | // Prozedural | ||
mysqli_stmt_execute($stmt_proc); | mysqli_stmt_execute($stmt_proc); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
**# | **# '''Ergebnis holen:''' Um die Ergebnisse eines Prepared Statements in MySQLi zu erhalten, ist die Methode <code>get_result()</code> (OO) bzw. <code>mysqli_stmt_get_result()</code> (prozedural) am bequemsten. Sie gibt ein Standard-<code>mysqli_result</code>-Objekt zurück, das dann mit den üblichen Fetch-Methoden verarbeitet werden kann. '''Wichtig:''' Diese Methode erfordert den <code>mysqlnd</code>-Treiber.[25, 15] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// OO-Stil | // OO-Stil | ||
$result = $stmt- | $result = $stmt->get_result(); | ||
// Prozedural | // Prozedural | ||
$result_proc = mysqli_stmt_get_result($stmt_proc); | $result_proc = mysqli_stmt_get_result($stmt_proc); | ||
Zeile 305: | Zeile 318: | ||
Die Alternative ohne <code>mysqlnd</code> ist <code>bind_result()</code>, bei der jede Ergebnisspalte an eine PHP-Variable gebunden wird, und <code>fetch()</code> iterativ aufgerufen wird, um die Variablen zu füllen.[25] Dies ist deutlich umständlicher. | Die Alternative ohne <code>mysqlnd</code> ist <code>bind_result()</code>, bei der jede Ergebnisspalte an eine PHP-Variable gebunden wird, und <code>fetch()</code> iterativ aufgerufen wird, um die Variablen zu füllen.[25] Dies ist deutlich umständlicher. | ||
=== | === Ergebnisse abrufen und verarbeiten === | ||
Nachdem eine <code>SELECT</code>-Abfrage erfolgreich ausgeführt wurde, müssen die zurückgegebenen Daten aus dem Ergebnisobjekt (<code>PDOStatement</code> oder <code>mysqli_result</code>) abgerufen ("gefetched") werden. | Nachdem eine <code>SELECT</code>-Abfrage erfolgreich ausgeführt wurde, müssen die zurückgegebenen Daten aus dem Ergebnisobjekt (<code>PDOStatement</code> oder <code>mysqli_result</code>) abgerufen ("gefetched") werden. | ||
*** | *** '''Zeilenweise Iteration (<code>fetch</code> im Loop):''' | ||
Dies ist der klassische Weg, um Ergebnisse zu verarbeiten, insbesondere wenn jede Zeile einzeln behandelt werden muss oder wenn die Ergebnismenge sehr groß ist. | Dies ist der klassische Weg, um Ergebnisse zu verarbeiten, insbesondere wenn jede Zeile einzeln behandelt werden muss oder wenn die Ergebnismenge sehr groß ist. | ||
**** | **** '''PDO:''' Die <code>fetch()</code>-Methode des <code>PDOStatement</code>-Objekts wird wiederholt in einer <code>while</code>-Schleife aufgerufen. <code>fetch()</code> gibt bei jedem Aufruf die nächste Zeile zurück (im Format des eingestellten Fetch-Modus, z.B. <code>PDO::FETCH_ASSOC</code>) und <code>false</code> (oder <code>null</code> je nach Modus/Version), wenn keine weiteren Zeilen vorhanden sind.[2, 33] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
$stmt = $pdo- | $stmt = $pdo->prepare("SELECT name, email FROM users WHERE status =?"); | ||
$stmt- | $stmt->execute(['active']); | ||
while ($row = $stmt- | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { | ||
echo "Name: ". htmlspecialchars($row['name']). ", E-Mail: ". htmlspecialchars($row['email']). "<br>"; | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
**** | **** '''MySQLi:''' Die <code>fetch_assoc()</code>-Methode (oder <code>fetch_row()</code>, <code>fetch_object()</code>, etc.) des <code>mysqli_result</code>-Objekts wird in einer <code>while</code>-Schleife verwendet. Sie gibt die nächste Zeile als Array (oder Objekt) zurück oder <code>null</code>, wenn das Ende erreicht ist.[25, 15] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// OO-Stil (nach $result = $stmt- | // OO-Stil (nach $result = $stmt->get_result();) | ||
while ($row = $result- | while ($row = $result->fetch_assoc()) { | ||
echo "Name: ". htmlspecialchars($row['name']). ", E-Mail: ". htmlspecialchars($row['email']). "<br>"; | |||
} | } | ||
// Prozedural (nach $result = mysqli_stmt_get_result($stmt);) | // Prozedural (nach $result = mysqli_stmt_get_result($stmt);) | ||
while ($row = mysqli_fetch_assoc($result)) { | while ($row = mysqli_fetch_assoc($result)) { | ||
echo "Name: ". htmlspecialchars($row['name']). ", E-Mail: ". htmlspecialchars($row['email']). "<br>"; | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Dieser Ansatz ist '''speichereffizient''', da immer nur eine Zeile gleichzeitig im PHP-Speicher gehalten wird. Dies ist besonders wichtig bei Abfragen, die potenziell Tausende oder Millionen von Zeilen zurückgeben könnten.[37] | Dieser Ansatz ist '''speichereffizient''', da immer nur eine Zeile gleichzeitig im PHP-Speicher gehalten wird. Dies ist besonders wichtig bei Abfragen, die potenziell Tausende oder Millionen von Zeilen zurückgeben könnten.[37] | ||
*** | *** '''Alle Ergebnisse auf einmal abrufen (<code>fetchAll</code> / <code>fetch_all</code>):''' | ||
Für kleinere bis mittlere Ergebnismengen ist es oft bequemer, alle Zeilen auf einmal in ein PHP-Array zu laden. | Für kleinere bis mittlere Ergebnismengen ist es oft bequemer, alle Zeilen auf einmal in ein PHP-Array zu laden. | ||
**** | **** '''PDO:''' Die <code>fetchAll()</code>-Methode des <code>PDOStatement</code>-Objekts gibt ein Array zurück, das alle Ergebniszeilen enthält.[33, 38] Der Fetch-Modus (z.B. <code>PDO::FETCH_ASSOC</code>) kann als Argument übergeben werden. | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
$stmt = $pdo- | $stmt = $pdo->prepare("SELECT name, email FROM users WHERE status =?"); | ||
$stmt- | $stmt->execute(['active']); | ||
$results = $stmt- | $results = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||
// $results ist nun ein Array von assoziativen Arrays | // $results ist nun ein Array von assoziativen Arrays | ||
</syntaxhighlight> | </syntaxhighlight> | ||
**** | **** '''MySQLi:''' Die <code>fetch_all()</code>-Methode des <code>mysqli_result</code>-Objekts (verfügbar mit <code>mysqlnd</code>) tut dasselbe. Der Modus (<code>MYSQLI_ASSOC</code>, <code>MYSQLI_NUM</code>, <code>MYSQLI_BOTH</code>) wird als Argument übergeben.[25, 39] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// OO-Stil (nach $result = $stmt- | // OO-Stil (nach $result = $stmt->get_result();) | ||
$results = $result- | $results = $result->fetch_all(MYSQLI_ASSOC); | ||
// Prozedural (nach $result = mysqli_stmt_get_result($stmt);) | // Prozedural (nach $result = mysqli_stmt_get_result($stmt);) | ||
Zeile 352: | Zeile 365: | ||
Der '''Vorteil''' dieser Methode liegt in der einfacheren Weiterverarbeitung des Ergebnis-Arrays mit Standard-PHP-Array-Funktionen wie <code>foreach</code> oder <code>count</code>.[40] Der '''Nachteil''' ist der potenziell hohe Speicherverbrauch, da alle Daten gleichzeitig in den PHP-Speicher geladen werden. Bei sehr großen Ergebnismengen kann dies zu Speicherlimit-Fehlern führen.[37] | Der '''Vorteil''' dieser Methode liegt in der einfacheren Weiterverarbeitung des Ergebnis-Arrays mit Standard-PHP-Array-Funktionen wie <code>foreach</code> oder <code>count</code>.[40] Der '''Nachteil''' ist der potenziell hohe Speicherverbrauch, da alle Daten gleichzeitig in den PHP-Speicher geladen werden. Bei sehr großen Ergebnismengen kann dies zu Speicherlimit-Fehlern führen.[37] | ||
*** | *** '''Fetch-Modi / Fetch-Funktionen (Struktur der Ergebniszeile):''' | ||
Beide Erweiterungen bieten verschiedene Möglichkeiten, die Struktur der abgerufenen Datenzeilen zu bestimmen: | Beide Erweiterungen bieten verschiedene Möglichkeiten, die Struktur der abgerufenen Datenzeilen zu bestimmen: | ||
**** | **** '''PDO Fetch-Modi (Auswahl):''' | ||
***** | ***** <code>PDO::FETCH_ASSOC</code>: Gibt ein assoziatives Array zurück (<code>['spaltenname' => 'wert',... ]</code>).[9, 33, 41, 42] Sehr gebräuchlich. | ||
***** | ***** <code>PDO::FETCH_NUM</code>: Gibt ein numerisch indiziertes Array zurück (<code>[0 => 'wert1', 1 => 'wert2',...]</code>).[9, 41, 42] | ||
***** | ***** <code>PDO::FETCH_BOTH</code> (Standard): Gibt ein Array zurück, das sowohl assoziative als auch numerische Indizes enthält.[41, 42] Verbraucht mehr Speicher. | ||
***** | ***** <code>PDO::FETCH_OBJ</code>: Gibt ein anonymes <code>stdClass</code>-Objekt zurück, bei dem die Spaltennamen Eigenschaften sind (<code>$row->spaltenname</code>).[9, 41, 42, 43] | ||
***** | ***** <code>PDO::FETCH_CLASS</code>: Erstellt eine Instanz einer angegebenen Klasse und weist die Spaltenwerte den Klasseneigenschaften zu.[38, 41, 42, 43] | ||
***** | ***** <code>PDO::FETCH_COLUMN</code>: Ruft nur den Wert einer einzelnen Spalte aus der nächsten Zeile ab.[38] | ||
***** | ***** <code>PDO::FETCH_LAZY</code>: Eine Kombination aus <code>FETCH_BOTH</code> und <code>FETCH_OBJ</code>, die Werte erst bei Zugriff lädt.[9, 41, 42] | ||
**** | **** '''MySQLi Fetch-Funktionen (Auswahl):''' | ||
***** | ***** <code>fetch_assoc()</code> / <code>mysqli_fetch_assoc()</code>: Gibt ein assoziatives Array zurück.[17, 25, 39, 15] Sehr gebräuchlich. | ||
***** | ***** <code>fetch_row()</code> / <code>mysqli_fetch_row()</code>: Gibt ein numerisch indiziertes Array zurück.[39, 35] | ||
***** | ***** <code>fetch_array()</code> / <code>mysqli_fetch_array()</code>: Gibt je nach übergebenem Modus (<code>MYSQLI_ASSOC</code>, <code>MYSQLI_NUM</code>, <code>MYSQLI_BOTH</code>) ein assoziatives, numerisches oder beides enthaltendes Array zurück.[39, 35] | ||
***** | ***** <code>fetch_object()</code> / <code>mysqli_fetch_object()</code>: Gibt ein <code>stdClass</code>-Objekt zurück oder eine Instanz einer angegebenen Klasse.[21, 15, 35] | ||
***** | ***** <code>fetch_column()</code> / <code>mysqli_fetch_column()</code>: Ruft den Wert einer einzelnen Spalte ab (erst ab PHP 8.1 verfügbar).[24] | ||
***** | ***** <code>fetch_all()</code> / <code>mysqli_fetch_all()</code>: Ruft alle Zeilen auf einmal ab, Modus (<code>MYSQLI_ASSOC</code>, <code>MYSQLI_NUM</code>, <code>MYSQLI_BOTH</code>) wählbar.[21, 25, 39] | ||
=== Tabelle 2: Ausgewählte PDO Fetch-Modi === | === Tabelle 2: Ausgewählte PDO Fetch-Modi === | ||
Zeile 376: | Zeile 389: | ||
! Konstante !! Rückgabeformat !! Beschreibung | ! Konstante !! Rückgabeformat !! Beschreibung | ||
|- | |- | ||
| | | <code>PDO::FETCH_ASSOC</code> | ||
| assoziatives="" array="" mwt-preservehtml="" | [" col'=">" 'val']<="" code>')="" ><br> | | assoziatives="" array="" mwt-preservehtml="" | [" col'=">" 'val']<="" code>')="" ><br> | ||
| Gängigste Form, Spaltennamen als Schlüssel. | | Gängigste Form, Spaltennamen als Schlüssel. | ||
Zeile 398: | Zeile 411: | ||
| Spaltennamen werden zu Objekteigenschaften. | | Spaltennamen werden zu Objekteigenschaften. | ||
|- | |- | ||
| | | <code>PDO::FETCH_CLASS</code> || objekt="" einer="" spezifischen="" klasse="" | | ||
<br> | <br> | ||
| Mappt Spalten auf Eigenschaften einer vordefinierten Klasse. | | Mappt Spalten auf Eigenschaften einer vordefinierten Klasse. | ||
|- | |- | ||
| | | <code>PDO::FETCH_COLUMN</code> || einzelner="" wert="" | | ||
<br> | <br> | ||
| Gibt nur den Wert der ersten (oder angegebenen) Spalte der nächsten Zeile zurück. | | Gibt nur den Wert der ersten (oder angegebenen) Spalte der nächsten Zeile zurück. | ||
Zeile 430: | Zeile 443: | ||
| Flexibel je nach Modus (<code>MYSQLI_ASSOC</code>, <code>MYSQLI_NUM</code>, <code>MYSQLI_BOTH</code>). | | Flexibel je nach Modus (<code>MYSQLI_ASSOC</code>, <code>MYSQLI_NUM</code>, <code>MYSQLI_BOTH</code>). | ||
|- | |- | ||
| | | <code>fetch_object()</code> / <code>mysqli_fetch_object()</code> | ||
| mwt-preservehtml="" | stdClass" -objekt="" oder="" spezifische="" klasse="" ><br> | | mwt-preservehtml="" | stdClass" -objekt="" oder="" spezifische="" klasse="" ><br> | ||
| Spaltennamen werden zu Objekteigenschaften. | | Spaltennamen werden zu Objekteigenschaften. | ||
Zeile 440: | Zeile 453: | ||
| Holt alle Ergebniszeilen auf einmal (benötigt <code>mysqlnd</code>). | | Holt alle Ergebniszeilen auf einmal (benötigt <code>mysqlnd</code>). | ||
|- | |- | ||
| | | <code>fetch_column()</code> / <code>mysqli_fetch_column()</code> || einzelner="" wert="" | | ||
<br> | <br> | ||
| Gibt nur den Wert der ersten (oder angegebenen) Spalte zurück (PHP 8.1+). | | Gibt nur den Wert der ersten (oder angegebenen) Spalte zurück (PHP 8.1+). | ||
|} | |} | ||
*** | *** '''Zugriff auf Spaltenwerte:''' | ||
Der Zugriff auf die Daten innerhalb einer abgerufenen Zeile hängt vom gewählten Fetch-Modus ab: | Der Zugriff auf die Daten innerhalb einer abgerufenen Zeile hängt vom gewählten Fetch-Modus ab: | ||
**** | **** Assoziatives Array: <code>$row['spaltenname']</code> | ||
**** | **** Numerisches Array: <code>$row</code>, <code>$row[1]</code>,... | ||
**** | **** Objekt: <code>$row->spaltenname</code> | ||
*** | *** '''Iteration mit <code>foreach</code>:''' | ||
Nachdem Ergebnisse mit <code>fetchAll()</code> oder <code>fetch_all()</code> in ein Array geladen wurden, ist <code>foreach</code> die natürliche Wahl zur Iteration [37, 40, 44]: | Nachdem Ergebnisse mit <code>fetchAll()</code> oder <code>fetch_all()</code> in ein Array geladen wurden, ist <code>foreach</code> die natürliche Wahl zur Iteration [37, 40, 44]: | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
$results = $stmt- | $results = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||
foreach ($results as $row) { | foreach ($results as $row) { | ||
echo "Name: ". htmlspecialchars($row['name']). "<br>"; | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Zeile 461: | Zeile 474: | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// Nur PHP 8+ mit PDO | // Nur PHP 8+ mit PDO | ||
$stmt = $pdo- | $stmt = $pdo->query("SELECT name, email FROM users"); | ||
foreach ($stmt as $row) { // PDOStatement ist direkt traversierbar | foreach ($stmt as $row) { // PDOStatement ist direkt traversierbar | ||
echo "Name: ". htmlspecialchars($row['name']). "<br>"; | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Diese Methode ist besonders elegant und effizient für große Ergebnismengen in modernen PHP-Versionen. | Diese Methode ist besonders elegant und effizient für große Ergebnismengen in modernen PHP-Versionen. | ||
=== | === Daten manipulieren (INSERT, UPDATE, DELETE) === | ||
Das Einfügen, Aktualisieren und Löschen von Daten sind kritische Operationen, da sie den Datenbestand verändern. Hier ist die Verwendung von '''Prepared Statements zwingend erforderlich''', um die Integrität und Sicherheit der Datenbank zu gewährleisten, insbesondere wenn Benutzereingaben beteiligt sind. | Das Einfügen, Aktualisieren und Löschen von Daten sind kritische Operationen, da sie den Datenbestand verändern. Hier ist die Verwendung von '''Prepared Statements zwingend erforderlich''', um die Integrität und Sicherheit der Datenbank zu gewährleisten, insbesondere wenn Benutzereingaben beteiligt sind. | ||
*** | *** '''PDO:''' | ||
**** | **** '''Vorbereiten (<code>prepare</code>):''' Die SQL-Anweisung (<code>INSERT</code>, <code>UPDATE</code> oder <code>DELETE</code>) wird mit Platzhaltern (<code>?</code> oder <code>:name</code>) für die einzufügenden/zu aktualisierenden Werte sowie für die Kriterien in <code>WHERE</code>-Klauseln (bei <code>UPDATE</code> und <code>DELETE</code>) vorbereitet.[5, 46, 47, 48] | ||
**** | **** '''Ausführen (<code>execute</code>):''' Die Anweisung wird mit einem Array ausgeführt, das die Werte für die Platzhalter enthält.[5, 46, 47, 48] | ||
**** | **** '''Ergebnis prüfen:''' | ||
***** | ***** ''Anzahl betroffener Zeilen:'' <code>$stmt->rowCount()</code> gibt die Anzahl der durch <code>UPDATE</code> oder <code>DELETE</code> betroffenen Zeilen zurück. Bei <code>INSERT</code> ist der Rückgabewert oft nicht zuverlässig oder standardisiert.[8, 9, 5, 49] Das Verhalten kann je nach Datenbanktreiber und Konfiguration (z.B. <code>PDO::MYSQL_ATTR_FOUND_ROWS</code>) variieren.[9] | ||
***** | ***** ''ID des letzten eingefügten Datensatzes:'' Bei <code>INSERT</code>-Anweisungen in Tabellen mit einer <code>AUTO_INCREMENT</code>-Spalte liefert <code>$pdo->lastInsertId()</code> die ID des neu eingefügten Datensatzes zurück.[8, 33, 48] | ||
**** | **** '''Code-Beispiele (PDO):''' | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
<?php// --- INSERT --- | <!--?php// --- INSERT --- | ||
$name = $_POST['name']; | $name = $_POST['name']; | ||
$email = $_POST['email']; | $email = $_POST['email']; | ||
Zeile 486: | Zeile 499: | ||
$sql_insert = "INSERT INTO users (username, email, status) VALUES (:name, :email, :status)"; | $sql_insert = "INSERT INTO users (username, email, status) VALUES (:name, :email, :status)"; | ||
$stmt_insert = $pdo->prepare($sql_insert); | $stmt_insert = $pdo--->prepare($sql_insert); | ||
$stmt_insert- | $stmt_insert->execute([':name' => $name, ':email' => $email, ':status' => $status]); | ||
$lastId = $pdo- | $lastId = $pdo->lastInsertId(); | ||
echo "Neuer Benutzer mit ID $lastId eingefügt.<br>"; | echo "Neuer Benutzer mit ID $lastId eingefügt.<br>"; | ||
Zeile 496: | Zeile 509: | ||
$sql_update = "UPDATE users SET status = :status WHERE id = :id"; | $sql_update = "UPDATE users SET status = :status WHERE id = :id"; | ||
$stmt_update = $pdo- | $stmt_update = $pdo->prepare($sql_update); | ||
$stmt_update- | $stmt_update->execute([':status' => $new_status, ':id' => $user_id_to_update]); | ||
$affectedRows = $stmt_update- | $affectedRows = $stmt_update->rowCount(); | ||
echo "$affectedRows Benutzerstatus aktualisiert.<br>"; | echo "$affectedRows Benutzerstatus aktualisiert.<br>"; | ||
Zeile 505: | Zeile 518: | ||
$sql_delete = "DELETE FROM users WHERE id = :id"; | $sql_delete = "DELETE FROM users WHERE id = :id"; | ||
$stmt_delete = $pdo- | $stmt_delete = $pdo->prepare($sql_delete); | ||
$stmt_delete- | $stmt_delete->execute([':id' => $user_id_to_delete]); | ||
$affectedRowsDel = $stmt_delete- | $affectedRowsDel = $stmt_delete->rowCount(); | ||
echo "$affectedRowsDel Benutzer gelöscht.<br>"; | echo "$affectedRowsDel Benutzer gelöscht.<br>"; | ||
? | ?> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
*** | *** '''MySQLi:''' | ||
**** | **** '''Vorbereiten (<code>prepare</code>):''' Die SQL-Anweisung wird mit <code>?</code>-Platzhaltern vorbereitet (OO: <code>$mysqli->prepare()</code>, Proc: <code>mysqli_prepare()</code>).[25, 50, 15, 35] | ||
**** | **** '''Parameter binden (<code>bind_param</code>):''' Variablen werden explizit mit Typenangabe gebunden (OO: <code>$stmt->bind_param()</code>, Proc: <code>mysqli_stmt_bind_param()</code>).[25, 50, 15, 35] Die Notwendigkeit, den Datentyp für jede Variable anzugeben, ist ein wesentlicher Unterschied zur einfacheren Array-Übergabe bei PDOs <code>execute()</code>. | ||
**** | **** '''Ausführen (<code>execute</code>):''' Die Anweisung wird ausgeführt (OO: <code>$stmt->execute()</code>, Proc: <code>mysqli_stmt_execute()</code>).[25, 36, 50, 15, 35] | ||
**** | **** '''Ergebnis prüfen:''' | ||
***** | ***** ''Anzahl betroffener Zeilen:'' Über die Eigenschaft <code>$mysqli->affected_rows</code> oder <code>$stmt->affected_rows</code> (OO) bzw. die Funktionen <code>mysqli_affected_rows($link)</code> oder <code>mysqli_stmt_affected_rows($stmt)</code> (Proc).[18, 26, 15, 35] | ||
***** | ***** ''ID des letzten eingefügten Datensatzes:'' Über <code>$mysqli->insert_id</code> oder <code>$stmt->insert_id</code> (OO) bzw. <code>mysqli_insert_id($link)</code> oder <code>mysqli_stmt_insert_id($stmt)</code> (Proc).[26, 15] Die Verwendung von <code>$mysqli->insert_id</code> wird oft als zuverlässiger angesehen, da sie auch nach dem Schließen des Statements funktioniert.[15] | ||
**** | **** '''Code-Beispiele (MySQLi OO):''' | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
<?php// --- INSERT --- | <!--?php// --- INSERT --- | ||
$name = $_POST['name']; | $name = $_POST['name']; | ||
$email = $_POST['email']; | $email = $_POST['email']; | ||
Zeile 527: | Zeile 540: | ||
$sql_insert = "INSERT INTO users (username, email, status) VALUES (?,?,?)"; | $sql_insert = "INSERT INTO users (username, email, status) VALUES (?,?,?)"; | ||
$stmt_insert = $mysqli->prepare($sql_insert); | $stmt_insert = $mysqli--->prepare($sql_insert); | ||
// Typen: s=string, s=string, s=string | // Typen: s=string, s=string, s=string | ||
$stmt_insert- | $stmt_insert->bind_param("sss", $name, $email, $status); | ||
$stmt_insert- | $stmt_insert->execute(); | ||
$lastId = $mysqli- | $lastId = $mysqli->insert_id; | ||
echo "Neuer Benutzer mit ID $lastId eingefügt (MySQLi OO).<br>"; | echo "Neuer Benutzer mit ID $lastId eingefügt (MySQLi OO).<br>"; | ||
$stmt_insert- | $stmt_insert->close(); // Statement schließen | ||
// --- UPDATE --- | // --- UPDATE --- | ||
Zeile 540: | Zeile 553: | ||
$sql_update = "UPDATE users SET status =? WHERE id =?"; | $sql_update = "UPDATE users SET status =? WHERE id =?"; | ||
$stmt_update = $mysqli- | $stmt_update = $mysqli->prepare($sql_update); | ||
// Typen: s=string, i=integer | // Typen: s=string, i=integer | ||
$stmt_update- | $stmt_update->bind_param("si", $new_status, $user_id_to_update); | ||
$stmt_update- | $stmt_update->execute(); | ||
$affectedRows = $mysqli- | $affectedRows = $mysqli->affected_rows; // Oder $stmt_update->affected_rows vor close() | ||
echo "$affectedRows Benutzerstatus aktualisiert (MySQLi OO).<br>"; | echo "$affectedRows Benutzerstatus aktualisiert (MySQLi OO).<br>"; | ||
$stmt_update- | $stmt_update->close(); | ||
// --- DELETE --- | // --- DELETE --- | ||
Zeile 552: | Zeile 565: | ||
$sql_delete = "DELETE FROM users WHERE id =?"; | $sql_delete = "DELETE FROM users WHERE id =?"; | ||
$stmt_delete = $mysqli- | $stmt_delete = $mysqli->prepare($sql_delete); | ||
// Typ: i=integer | // Typ: i=integer | ||
$stmt_delete- | $stmt_delete->bind_param("i", $user_id_to_delete); | ||
$stmt_delete- | $stmt_delete->execute(); | ||
$affectedRowsDel = $mysqli- | $affectedRowsDel = $mysqli->affected_rows; | ||
echo "$affectedRowsDel Benutzer gelöscht (MySQLi OO).<br>"; | echo "$affectedRowsDel Benutzer gelöscht (MySQLi OO).<br>"; | ||
$stmt_delete- | $stmt_delete->close(); | ||
? | ?> | ||
</syntaxhighlight>''(Anmerkung: Prozedurale Beispiele folgen demselben Muster unter Verwendung der <code>mysqli_*</code>-Funktionen wie <code>mysqli_prepare</code>, <code>mysqli_stmt_bind_param</code>, <code>mysqli_stmt_execute</code>, <code>mysqli_insert_id</code>, <code>mysqli_affected_rows</code> und <code>mysqli_stmt_close</code>)''. | </syntaxhighlight>''(Anmerkung: Prozedurale Beispiele folgen demselben Muster unter Verwendung der <code>mysqli_*</code>-Funktionen wie <code>mysqli_prepare</code>, <code>mysqli_stmt_bind_param</code>, <code>mysqli_stmt_execute</code>, <code>mysqli_insert_id</code>, <code>mysqli_affected_rows</code> und <code>mysqli_stmt_close</code>)''. | ||
== | == Sicherheit: SQL Injection verstehen und verhindern == | ||
Eines der größten Sicherheitsrisiken bei der Interaktion mit Datenbanken über Webanwendungen ist die SQL Injection (SQLi). Das Verständnis dieser Bedrohung und ihrer Abwehr ist für jeden PHP-Entwickler unerlässlich. | Eines der größten Sicherheitsrisiken bei der Interaktion mit Datenbanken über Webanwendungen ist die SQL Injection (SQLi). Das Verständnis dieser Bedrohung und ihrer Abwehr ist für jeden PHP-Entwickler unerlässlich. | ||
=== | === SQL Injection === | ||
SQL Injection ist eine Angriffstechnik, bei der ein Angreifer versucht, bösartigen oder manipulierten SQL-Code über Benutzereingabefelder einer Webanwendung (z.B. Formulare, URL-Parameter, HTTP-Header, Cookies) in die Datenbankabfragen einzuschleusen.[51, 52, 53, 54, 55, 56, 57] | SQL Injection ist eine Angriffstechnik, bei der ein Angreifer versucht, bösartigen oder manipulierten SQL-Code über Benutzereingabefelder einer Webanwendung (z.B. Formulare, URL-Parameter, HTTP-Header, Cookies) in die Datenbankabfragen einzuschleusen.[51, 52, 53, 54, 55, 56, 57] | ||
Zeile 578: | Zeile 591: | ||
Da <code>'1'='1'</code> immer wahr ist, würde diese Abfrage (je nach genauer Struktur und Datenbank) möglicherweise alle Benutzer zurückgeben oder den Login ohne korrektes Passwort ermöglichen.[5, 15, 55] | Da <code>'1'='1'</code> immer wahr ist, würde diese Abfrage (je nach genauer Struktur und Datenbank) möglicherweise alle Benutzer zurückgeben oder den Login ohne korrektes Passwort ermöglichen.[5, 15, 55] | ||
=== | === Auswirkungen und Gefahren von SQL Injection === | ||
Die potenziellen Folgen einer erfolgreichen SQL-Injection-Attacke sind gravierend und vielfältig: | Die potenziellen Folgen einer erfolgreichen SQL-Injection-Attacke sind gravierend und vielfältig: | ||
*** | *** '''Datendiebstahl:''' Angreifer können auf sensible Daten zugreifen und diese auslesen, darunter Benutzerdaten, Passwörter (oft als Hashes, die aber geknackt werden können), Kreditkarteninformationen, Geschäftsgeheimnisse und andere vertrauliche Informationen.[51, 53, 56] | ||
*** | *** '''Datenmanipulation/-zerstörung:''' Angreifer können bestehende Daten in der Datenbank verändern (z.B. Preise in einem Shop, Benutzerrollen) oder komplette Datensätze oder sogar Tabellen löschen (<code>DELETE</code>, <code>DROP TABLE</code>).[51, 53, 56] | ||
*** | *** '''Umgehung der Authentifizierung/Identitätsdiebstahl:''' Wie im Beispiel oben gezeigt, können Angreifer Login-Mechanismen umgehen und sich unautorisiert Zugang verschaffen, möglicherweise sogar mit administrativen Rechten.[51, 56] | ||
*** | *** '''Denial of Service (DoS):''' Durch das Löschen wichtiger Daten oder das Ausführen ressourcenintensiver Abfragen kann die Verfügbarkeit der Anwendung oder der Datenbank beeinträchtigt werden.[51, 56] | ||
*** | *** '''Kompromittierung des Servers:''' In manchen Fällen können Angreifer über SQL Injection Betriebssystembefehle auf dem Datenbankserver ausführen oder vollen administrativen Zugriff auf die Datenbank erlangen, was weitreichende Konsequenzen haben kann.[51, 55, 56] | ||
*** | *** '''Reputationsschaden und rechtliche Folgen:''' Erfolgreiche Angriffe, insbesondere wenn sie zu Datenlecks führen, können das Vertrauen der Benutzer schwer beschädigen und zu empfindlichen Strafen gemäß Datenschutzgesetzen führen.[53] | ||
=== | === Prävention durch Prepared Statements (Primäre Methode) === | ||
Die mit Abstand | Die mit Abstand wichtigste und effektivste Methode zur Verhinderung von SQL Injection ist die konsequente Verwendung von '''Prepared Statements''' (parametrisierten Abfragen).[4, 25, 5, 58, 35] | ||
Das Grundprinzip von Prepared Statements ist die '''strikte Trennung von SQL-Befehlsstruktur und den Daten (Parametern)'''.[4, 25, 5, 58, 35] Der Ablauf ist typischerweise zweistufig: | Das Grundprinzip von Prepared Statements ist die '''strikte Trennung von SQL-Befehlsstruktur und den Daten (Parametern)'''.[4, 25, 5, 58, 35] Der Ablauf ist typischerweise zweistufig: | ||
**# | **# '''Prepare (Vorbereiten):''' Die SQL-Anweisung wird mit Platzhaltern anstelle der tatsächlichen Werte an den Datenbankserver gesendet. Der Server parst die Anweisung, prüft die Syntax und bereitet einen Ausführungsplan vor, ohne die endgültigen Daten zu kennen. | ||
**# | **# '''Execute (Ausführen) mit Parameterbindung:''' Die tatsächlichen Werte (Parameter) werden separat an den Server gesendet und an die vorbereiteten Platzhalter gebunden. Der Server führt nun die vorkompilierte Anweisung mit den sicher eingefügten Werten aus. | ||
Der entscheidende Punkt ist, dass die Datenbank die separat gesendeten Parameter '''immer als Datenwerte''' behandelt, niemals als Teil des SQL-Befehls.[5, 15, 58] Selbst wenn ein Parameter bösartigen SQL-Code enthält (z.B. <code>' OR '1'='1</code>), wird dieser nicht ausgeführt, sondern als einfacher String-Wert behandelt, der z.B. in eine Spalte eingefügt oder in einer <code>WHERE</code>-Klausel verglichen wird. Diese Trennung macht Prepared Statements immun gegen SQL Injection. | Der entscheidende Punkt ist, dass die Datenbank die separat gesendeten Parameter '''immer als Datenwerte''' behandelt, niemals als Teil des SQL-Befehls.[5, 15, 58] Selbst wenn ein Parameter bösartigen SQL-Code enthält (z.B. <code>' OR '1'='1</code>), wird dieser nicht ausgeführt, sondern als einfacher String-Wert behandelt, der z.B. in eine Spalte eingefügt oder in einer <code>WHERE</code>-Klausel verglichen wird. Diese Trennung macht Prepared Statements immun gegen SQL Injection. | ||
*** | *** '''Platzhalter:''' | ||
**** | **** '''PDO:''' Unterstützt sowohl positionale Platzhalter (<code>?</code>) als auch benannte Platzhalter (<code>:name</code>, <code>:email</code>, etc.).[5, 13] Benannte Platzhalter machen den Code oft lesbarer, besonders bei vielen Parametern. | ||
**** | **** '''MySQLi:''' Unterstützt ausschließlich positionale Platzhalter (<code>?</code>).[25] | ||
*** | *** '''Parameterbindung:''' | ||
**** | **** '''PDO:''' Bietet flexible Bindungsoptionen: | ||
***** | ***** ''Implizit über <code>execute()</code>-Array:'' Die einfachste und häufigste Methode. Ein Array mit den Werten wird an <code>execute()</code> übergeben. PDO kümmert sich um das korrekte Escaping und die Typbehandlung (oft werden Werte sicher als Strings behandelt).[4, 12, 5, 13] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
$stmt- | $stmt->execute([$wert1, $wert2]); // für? Platzhalter | ||
$stmt- | $stmt->execute([':name' => $wert1, ':id' => $wert2]); // für :name Platzhalter | ||
</syntaxhighlight> | </syntaxhighlight> | ||
***** | ***** ''Explizit mit <code>bindValue()</code> oder <code>bindParam()</code>:'' Erlaubt die explizite Angabe des Datentyps (z.B. <code>PDO::PARAM_INT</code>, <code>PDO::PARAM_STR</code>) für jeden Parameter, was in manchen Fällen für Klarheit oder spezifische Datenbankoptimierungen sorgen kann.[5, 13] <code>bindParam</code> bindet eine Variable (ihr Wert wird erst bei <code>execute()</code> gelesen), <code>bindValue</code> bindet den aktuellen Wert einer Variablen oder einen Literalwert. | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
$stmt- | $stmt->bindValue(':id', $user_id, PDO::PARAM_INT); | ||
$stmt- | $stmt->bindParam(':name', $user_name, PDO::PARAM_STR); | ||
$stmt- | $stmt->execute(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
**** | **** '''MySQLi:''' Erfordert immer die explizite Bindung mittels <code>bind_param()</code>. | ||
***** | ***** <code>bind_param()</code> (OO) / <code>mysqli_stmt_bind_param()</code> (Proc): Nimmt als ersten Parameter einen String entgegen, der die Datentypen aller nachfolgend übergebenen Variablen definiert.[25, 50, 15, 58] | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// OO-Stil | // OO-Stil | ||
$stmt- | $stmt->bind_param("ssi", $name, $email, $id); // String, String, Integer | ||
// Prozedural | // Prozedural | ||
mysqli_stmt_bind_param($stmt, "ssi", $name, $email, $id); | mysqli_stmt_bind_param($stmt, "ssi", $name, $email, $id); | ||
Zeile 652: | Zeile 665: | ||
|} | |} | ||
*** | *** '''Detaillierte Code-Beispiele zur Prävention:''' | ||
Die Beispiele für INSERT, UPDATE und DELETE in Abschnitt 3.3 demonstrieren bereits die korrekte Anwendung von Prepared Statements mit PDO und MySQLi zur Verhinderung von SQL Injection. Es ist entscheidend, dieses Muster konsequent anzuwenden, wann immer externe Daten in SQL-Abfragen einfließen. | Die Beispiele für INSERT, UPDATE und DELETE in Abschnitt 3.3 demonstrieren bereits die korrekte Anwendung von Prepared Statements mit PDO und MySQLi zur Verhinderung von SQL Injection. Es ist entscheidend, dieses Muster konsequent anzuwenden, wann immer externe Daten in SQL-Abfragen einfließen. | ||
Zeile 659: | Zeile 672: | ||
Obwohl Prepared Statements der wichtigste Schutz gegen SQL Injection sind, sollte Sicherheit immer als mehrschichtiges Konzept ("Defense-in-Depth") betrachtet werden.[5] Zusätzliche Maßnahmen erhöhen die Robustheit der Anwendung: | Obwohl Prepared Statements der wichtigste Schutz gegen SQL Injection sind, sollte Sicherheit immer als mehrschichtiges Konzept ("Defense-in-Depth") betrachtet werden.[5] Zusätzliche Maßnahmen erhöhen die Robustheit der Anwendung: | ||
*** | *** '''Eingabevalidierung und -bereinigung:''' Überprüfen Sie ''alle'' Benutzereingaben serverseitig, bevor sie überhaupt in die Nähe der Datenbank kommen.[4, 59, 20, 5] Stellen Sie sicher, dass die Daten dem erwarteten Typ (Zahl, String, E-Mail-Format etc.) und Format entsprechen. Verwenden Sie dafür PHP-Filterfunktionen wie <code>filter_var()</code> oder reguläre Ausdrücke.[4, 5] Dies ist eine wichtige zweite Verteidigungslinie, die ungültige oder unerwartete Daten frühzeitig abfängt. Es ersetzt jedoch ''nicht'' die Notwendigkeit von Prepared Statements. | ||
*** | *** '''Prinzip der geringsten Rechte (Least Privilege):''' Konfigurieren Sie den Datenbankbenutzer, den Ihre PHP-Anwendung verwendet, mit den absolut minimal notwendigen Berechtigungen.[20, 5] Wenn die Anwendung nur Daten lesen und schreiben muss, geben Sie ihr keine Rechte zum Ändern der Tabellenstruktur (<code>ALTER TABLE</code>) oder zum Löschen von Tabellen (<code>DROP TABLE</code>). Beschränken Sie den Zugriff auf die benötigten Datenbanken und Tabellen. Verwenden Sie niemals den <code>root</code>- oder Admin-Benutzer der Datenbank für die Anwendung. Dies begrenzt den potenziellen Schaden, falls doch eine Schwachstelle ausgenutzt wird. | ||
*** | *** '''Sichere Fehlerbehandlung:''' Konfigurieren Sie PHP und Ihre Datenbankerweiterung so, dass detaillierte Fehlermeldungen (insbesondere solche, die SQL-Code oder Datenbankstrukturinformationen enthalten) niemals dem Endbenutzer angezeigt werden.[20, 5] Solche Informationen sind für Angreifer wertvoll. Loggen Sie Fehler stattdessen in eine sichere Datei auf dem Server, die nur für Administratoren zugänglich ist. Verwenden Sie <code>PDO::ERRMODE_EXCEPTION</code> oder <code>mysqli_report</code> für strukturierte Fehlerbehandlung. | ||
*** | *** '''Output Escaping:''' Obwohl nicht direkt zur Verhinderung von SQL Injection, ist es wichtig, Daten, die aus der Datenbank gelesen und im HTML-Kontext ausgegeben werden, korrekt zu escapen (z.B. mit <code>htmlspecialchars()</code>), um Cross-Site Scripting (XSS)-Angriffe zu verhindern. | ||
Die Kombination dieser Maßnahmen – mit Prepared Statements als Kernstück – bietet einen robusten Schutz gegen SQL Injection und andere verwandte Angriffe. | Die Kombination dieser Maßnahmen – mit Prepared Statements als Kernstück – bietet einen robusten Schutz gegen SQL Injection und andere verwandte Angriffe. | ||
== | == Nützliche PHP Array-Funktionen für Datenbankergebnisse == | ||
Wenn Daten aus der Datenbank abgerufen werden, insbesondere mit Methoden wie <code>fetchAll()</code> (PDO) oder <code>fetch_all()</code> (MySQLi), liegen die Ergebnisse typischerweise als PHP-Array vor (meist ein Array von assoziativen Arrays, wobei jedes innere Array eine Ergebniszeile darstellt). PHP bietet eine Reihe nützlicher Funktionen zur Verarbeitung solcher Arrays. | Wenn Daten aus der Datenbank abgerufen werden, insbesondere mit Methoden wie <code>fetchAll()</code> (PDO) oder <code>fetch_all()</code> (MySQLi), liegen die Ergebnisse typischerweise als PHP-Array vor (meist ein Array von assoziativen Arrays, wobei jedes innere Array eine Ergebniszeile darstellt). PHP bietet eine Reihe nützlicher Funktionen zur Verarbeitung solcher Arrays. | ||
*** | *** '''Iteration mit <code>foreach</code>:''' | ||
Dies ist die Standardmethode, um die einzelnen Zeilen und Spalten eines Ergebnis-Arrays zu durchlaufen.[37, 40, 44] | Dies ist die Standardmethode, um die einzelnen Zeilen und Spalten eines Ergebnis-Arrays zu durchlaufen.[37, 40, 44] | ||
**** | **** '''Syntax:''' | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// Iteration über die Zeilen (jedes $row ist ein assoziatives Array) | // Iteration über die Zeilen (jedes $row ist ein assoziatives Array) | ||
foreach ($results as $row) { | foreach ($results as $row) { | ||
echo "ID: ". $row['id']. ", Name: ". htmlspecialchars($row['name']). "<br>"; | |||
} | } | ||
// Iteration über Zeilen mit Zugriff auf den numerischen Index der Zeile | // Iteration über Zeilen mit Zugriff auf den numerischen Index der Zeile | ||
foreach ($results as $index = | foreach ($results as $index => $row) { | ||
echo "Zeile ". $index. ": ID = ". $row['id']. "<br>"; | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
**** | **** '''Code-Beispiel:''' | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
<?php// Annahme: $results enthält das Ergebnis von $stmt->fetchAll(PDO::FETCH_ASSOC); | <!--?php// Annahme: $results enthält das Ergebnis von $stmt--->fetchAll(PDO::FETCH_ASSOC); | ||
$results = ['id' = | $results = ['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'], | ||
; | |||
echo "< | echo "</syntaxhighlight>"; | ||
foreach ($results as $row) { | foreach ($results as $row) { | ||
echo " | |||
*Benutzer #". $row['id']. ": ". htmlspecialchars($row['name']); | |||
echo " (". htmlspecialchars($row['email']). ") | |||
"; | |||
} | } | ||
echo " | echo ""; | ||
? | ?> | ||
*** | *** '''Zählen von Ergebnissen mit <code>count()</code>:''' | ||
Die Funktion <code>count()</code> gibt die Anzahl der Elemente in einem Array zurück. Wenn sie auf ein mit <code>fetchAll()</code> oder <code>fetch_all()</code> erzeugtes Ergebnis-Array angewendet wird, liefert sie die Anzahl der abgerufenen Zeilen.[40, 60, 61] | Die Funktion <code>count()</code> gibt die Anzahl der Elemente in einem Array zurück. Wenn sie auf ein mit <code>fetchAll()</code> oder <code>fetch_all()</code> erzeugtes Ergebnis-Array angewendet wird, liefert sie die Anzahl der abgerufenen Zeilen.[40, 60, 61] | ||
**** | **** '''Syntax:''' <code>$anzahlZeilen = count($results);</code>.[60] | ||
**** | **** '''Wichtiger Hinweis:''' <code>count()</code> funktioniert nur zuverlässig für die Gesamtzahl der Zeilen, wenn ''alle'' Zeilen zuvor in ein Array geladen wurden (also nach <code>fetchAll</code>/<code>fetch_all</code>). Wenn Sie Ergebnisse zeilenweise mit <code>fetch()</code> in einer <code>while</code>-Schleife verarbeiten, müssen Sie einen manuellen Zähler innerhalb der Schleife inkrementieren, um die Anzahl der verarbeiteten Zeilen zu ermitteln.[40] Alternativ können <code>PDOStatement::rowCount()</code> [8, 9] oder <code>mysqli_result::$num_rows</code> / <code>mysqli_stmt::$num_rows</code> [26, 15] verwendet werden, um die Anzahl der von einer <code>SELECT</code>-Abfrage zurückgegebenen Zeilen zu erhalten, ''bevor'' die Daten gefetched werden (das Verhalten von <code>rowCount</code> kann jedoch bei anderen Anweisungstypen wie <code>UPDATE</code> variieren). | ||
**** | **** '''Code-Beispiel:''' | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
<?php | <!--?php | ||
// Annahme: $results von fetchAll() | // Annahme: $results von fetchAll() | ||
$results = [ /*... wie oben... */ ]; | $results = [ /*... wie oben... */ ]; | ||
$anzahl = count($results); | $anzahl = count($results); | ||
echo "Anzahl der Benutzer (via fetchAll/count): $anzahl<br>"; | echo "Anzahl der Benutzer (via fetchAll/count): $anzahl<br-->"; | ||
// Vergleich: Zählen bei zeilenweiser Verarbeitung | // Vergleich: Zählen bei zeilenweiser Verarbeitung | ||
$stmt = $pdo- | $stmt = $pdo->query("SELECT id FROM users"); // Annahme: PDO-Statement | ||
$manueller_zaehler = 0; | $manueller_zaehler = 0; | ||
while ($row = $stmt- | while ($row = $stmt->fetch()) { | ||
$manueller_zaehler++; | |||
} | } | ||
echo "Anzahl der Benutzer (via while/fetch): $manueller_zaehler<br>"; | echo "Anzahl der Benutzer (via while/fetch): $manueller_zaehler<br>"; | ||
// Alternative: rowCount() nach SELECT (PDO) | // Alternative: rowCount() nach SELECT (PDO) | ||
$stmt_count = $pdo- | $stmt_count = $pdo->query("SELECT id FROM users"); | ||
$rowCountResult = $stmt_count- | $rowCountResult = $stmt_count->rowCount(); // Kann bei manchen DBs/Treibern funktionieren | ||
echo "Anzahl der Benutzer (via rowCount nach SELECT): $rowCountResult<br>"; | echo "Anzahl der Benutzer (via rowCount nach SELECT): $rowCountResult<br>"; | ||
? | ?> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
*** | *** '''(Optional) Weitere nützliche Array-Funktionen:''' | ||
**** | **** <code>array_column(array $input, mixed $column_key [, mixed $index_key = null ])</code>: Extrahiert alle Werte aus einer einzelnen Spalte des Ergebnis-Arrays und gibt sie als neues, eindimensionales Array zurück.[62] Sehr nützlich, um z.B. eine Liste aller Benutzer-IDs oder E-Mail-Adressen zu erhalten. | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php"> | ||
// Annahme: $results enthält das Array von assoziativen Arrays | // Annahme: $results enthält das Array von assoziativen Arrays | ||
Zeile 733: | Zeile 748: | ||
print_r($alle_emails); | print_r($alle_emails); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
**** | **** <code>array_map(callable $callback, array $array1 [, array...$arrays ])</code>: Wendet eine benutzerdefinierte Funktion (<code>callback</code>) auf jedes Element eines Arrays an und gibt ein neues Array mit den Ergebnissen zurück.[63] Nützlich zur Transformation von Daten (z.B. Formatierung, Bereinigung). | ||
**** | **** <code>array_filter(array $array [, callable $callback [, int $flag = 0 ]])</code>: Filtert Elemente eines Arrays basierend auf einer Callback-Funktion.[61] Nützlich, um nur bestimmte Zeilen aus dem Ergebnis-Array zu behalten (obwohl dies oft effizienter direkt in der SQL-Abfrage geschieht). | ||
**** | **** <code>array_count_values(array $array)</code>: Zählt, wie oft jeder eindeutige Wert in einem eindimensionalen Array vorkommt.[63] Kann nützlich sein, wenn man z.B. mit <code>array_column</code> eine Spalte extrahiert hat und die Häufigkeit bestimmter Werte (z.B. Status) wissen möchte. | ||
Diese Funktionen, insbesondere <code>foreach</code> und <code>count</code>, sind grundlegende Werkzeuge bei der Verarbeitung von Datenbankergebnissen in PHP. | Diese Funktionen, insbesondere <code>foreach</code> und <code>count</code>, sind grundlegende Werkzeuge bei der Verarbeitung von Datenbankergebnissen in PHP. | ||
== | == Zusammenfassung & Best Practices == | ||
Die Integration von Datenbanken ist ein zentraler Aspekt der PHP-Webentwicklung. Dieses Kapitel hat die Grundlagen für die Interaktion mit MySQL-Datenbanken gelegt. Hier sind die wichtigsten Punkte und Best Practices zusammengefasst: | Die Integration von Datenbanken ist ein zentraler Aspekt der PHP-Webentwicklung. Dieses Kapitel hat die Grundlagen für die Interaktion mit MySQL-Datenbanken gelegt. Hier sind die wichtigsten Punkte und Best Practices zusammengefasst: | ||
'''Wahl der Erweiterung:''' Verwenden Sie ausschließlich die modernen Erweiterungen '''PDO''' oder '''MySQLi'''.[2, 29, 5, 15] | |||
* | * '''PDO''' wird generell für neue Projekte empfohlen, da es Datenbankunabhängigkeit bietet und eine konsistentere, oft als einfacher empfundene API (insbesondere bei der Parameterbindung) hat.[16, 3, 64] | ||
* | * '''MySQLi''' ist eine gute Wahl, wenn ausschließlich mit MySQL gearbeitet wird und spezifische MySQL-Funktionen benötigt werden oder wenn eine prozedurale Schnittstelle bevorzugt wird.[15] | ||
'''Prepared Statements:''' Dies ist die wichtigste Sicherheitsmaßnahme. Verwenden Sie Prepared Statements immer dann, wenn externe Daten (Benutzereingaben, URL-Parameter etc.) Teil einer SQL-Abfrage werden (insbesondere bei <code>WHERE</code>, <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>).[2, 25, 5] Sie verhindern SQL Injection zuverlässig durch die Trennung von Code und Daten. | |||
'''Sichere Fehlerbehandlung:''' Konfigurieren Sie PDO (<code>PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION</code>) oder MySQLi (<code>mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)</code>) so, dass Fehler als Exceptions behandelt werden.[1, 18] Fangen Sie diese Exceptions ab und loggen Sie detaillierte Fehlermeldungen serverseitig, anstatt sie dem Benutzer anzuzeigen.[20, 5] | |||
'''Input Validation:''' Validieren Sie alle Benutzereingaben serverseitig auf Typ, Format und erlaubte Werte, bevor Sie sie an die Datenbank übergeben. Dies ist eine wichtige zusätzliche Schutzschicht.[4, 5] | |||
'''Prinzip der geringsten Rechte (Least Privilege):''' Der Datenbankbenutzer, den Ihre PHP-Anwendung verwendet, sollte nur die minimal notwendigen Berechtigungen für seine Aufgaben besitzen.[5] | |||
'''Sichere Zugangsdaten:''' Speichern Sie Datenbank-Credentials (Hostname, Benutzername, Passwort) niemals direkt im PHP-Code oder in Dateien innerhalb des Web-Roots.[20, 65] Verwenden Sie stattdessen Konfigurationsdateien außerhalb des öffentlich zugänglichen Bereichs oder Umgebungsvariablen. | |||
'''Code-Struktur:''' Kapseln Sie Datenbankverbindungslogik und wiederkehrende Abfragen in Funktionen oder Klassen, um die Wartbarkeit und Wiederverwendbarkeit zu verbessern.[20] | |||
'''Effizienz:''' Laden Sie große Ergebnismengen nicht komplett in den Speicher (<code>fetchAll</code>/<code>fetch_all</code>). Verwenden Sie stattdessen zeilenweise Verarbeitung (<code>while</code>/<code>fetch</code>) oder Iteratoren (PDO mit PHP 8+).[37, 40] Nutzen Sie persistente Verbindungen nur nach sorgfältiger Abwägung der Vor- und Nachteile.[7, 27] | |||
'''Defense-in-Depth:''' Betrachten Sie Sicherheit als Gesamtkonzept. Die Kombination aus der richtigen API-Wahl, konsequenten Prepared Statements, Eingabevalidierung, minimalen Rechten, sicherer Fehlerbehandlung, sicherer Speicherung von Zugangsdaten und regelmäßigen Software-Updates bildet eine robuste Verteidigung. |
Aktuelle Version vom 1. Mai 2025, 15:18 Uhr
Kapitel X: Datenbankintegration mit PHP und MySQL
Einleitung: Warum Datenbanken in PHP-Webanwendungen?
Moderne Webanwendungen sind selten statisch. Sie leben von dynamischen Inhalten, Benutzerinteraktionen und der Fähigkeit, Informationen über Sitzungen hinweg zu speichern und wieder abzurufen. Ob es sich um ein Content-Management-System (CMS), einen Online-Shop, ein soziales Netzwerk oder eine einfache Benutzerverwaltung handelt – die Persistenz von Daten ist entscheidend. Hier kommen Datenbanken ins Spiel.
PHP allein, als serverseitige Skriptsprache, kann Daten nur für die Dauer der Ausführung eines Skripts im Arbeitsspeicher halten. Sobald das Skript beendet ist, gehen diese Informationen verloren. Um Daten dauerhaft zu speichern, zu organisieren, zu ändern und effizient abzurufen, benötigen PHP-Anwendungen ein externes Datenbanksystem.
Eine der populärsten Datenbanklösungen im Webentwicklungsbereich, insbesondere in Kombination mit PHP, ist MySQL. Als relationales Datenbankmanagementsystem (RDBMS) bietet MySQL eine strukturierte Methode zur Datenspeicherung in Tabellen, die miteinander in Beziehung stehen können. Es ist ein Kernbestandteil vieler Webserver-Setups wie LAMP (Linux, Apache, MySQL, PHP) oder LEMP (Linux, Nginx, MySQL, PHP).
Um die Brücke zwischen der PHP-Anwendung und der MySQL-Datenbank zu schlagen, stellt PHP spezielle Erweiterungen (Extensions) bereit. Diese Erweiterungen fungieren als Schnittstelle oder API (Application Programming Interface), die es PHP-Skripten ermöglichen, eine Verbindung zur Datenbank herzustellen, SQL-Befehle zu senden und die von der Datenbank zurückgegebenen Ergebnisse zu empfangen und zu verarbeiten.
Dieses Kapitel führt in die Grundlagen der Datenbankintegration mit PHP und MySQL ein. Es enthält die verschiedenen verfügbaren PHP-Erweiterungen, wobei der Schwerpunkt auf den modernen und sicheren Ansätzen liegt: PDO (PHP Data Objects) und MySQLi (MySQL Improved). Es wird gezeigt, wie man Verbindungen aufbaut, grundlegende Datenbankoperationen – bekannt als CRUD (Create, Read, Update, Delete) – durchführt und wie man Daten sicher abfragt und manipuliert. Ein besonderes Augenmerk liegt dabei auf der Sicherheit, insbesondere auf der Vermeidung von SQL-Injection-Angriffen durch den Einsatz von Prepared Statements. Zum Abschluss erfolgt ein Überblick über nützliche PHP-Array-Funktionen zur Verarbeitung der abgerufenen Datenbankergebnisse.
Überblick über PHP-Datenbankerweiterungen
Die Art und Weise, wie PHP mit Datenbanken interagiert, hat sich im Laufe der Zeit weiterentwickelt. Ursprünglich gab es für jede populäre Datenbank eine eigene spezifische Erweiterung. Für MySQL waren dies die mysql_*
Funktionen. Diese sind jedoch inzwischen veraltet und wurden durch modernere, flexiblere und sicherere Alternativen ersetzt. Heute stehen Entwicklern hauptsächlich zwei empfohlene Erweiterungen für die Arbeit mit MySQL zur Verfügung: PDO und MySQLi.
- PDO (PHP Data Objects): Eine allgemeine Datenzugriffs-Abstraktionsschicht, die mit verschiedenen Datenbanksystemen verwendet werden kann.
- MySQLi (MySQL Improved): Eine Erweiterung, die speziell für die Arbeit mit MySQL entwickelt wurde und dessen spezifische Funktionen unterstützt.
mysql_*
Funktionen: Die ursprüngliche MySQL-Erweiterung, die seit PHP 7.0 entfernt wurde und nicht mehr verwendet werden sollte.
Im Folgenden werden diese drei Optionen detaillierter vorgestellt.
PDO (PHP Data Objects)
Die PDO-Erweiterung definiert eine leichtgewichtige und konsistente Schnittstelle für den Datenbankzugriff in PHP. Sie fungiert als Datenzugriffs-Abstraktionsschicht. Das bedeutet, dass Entwickler unabhängig von der verwendeten Datenbank dieselben PDO-Funktionen nutzen können, um Abfragen auszuführen und Daten abzurufen.[1, 2] Es ist jedoch wichtig zu verstehen, dass PDO keine Datenbankabstraktion im Sinne einer SQL-Dialekt-Übersetzung oder der Emulation fehlender Datenbankfeatures bietet. Um eine Verbindung zu einer spezifischen Datenbank wie MySQL herzustellen, benötigt PDO einen entsprechenden datenbankspezifischen Treiber, z.B. pdo_mysql
, der in der PHP-Konfiguration aktiviert sein muss.[1]
Vorteile von PDO:
- Datenbankunabhängigkeit: Der größte Vorteil von PDO ist die Möglichkeit, den Code für Datenbankinteraktionen weitgehend unverändert zu lassen, selbst wenn das zugrunde liegende Datenbanksystem gewechselt wird (z.B. von MySQL zu PostgreSQL).[1, 2] Dies erhöht die Portabilität und Flexibilität von PHP-Anwendungen erheblich.[2, 3]
- Sicherheit durch Prepared Statements: PDO bietet eine robuste und konsistente Implementierung von Prepared Statements. Diese Technik ist der Goldstandard zur Verhinderung von SQL-Injection-Angriffen, da sie die SQL-Befehlsstruktur strikt von den übergebenen Daten trennt.[1, 2, 4, 5, 6]
- Benutzerfreundlichkeit und Konsistenz: PDO stellt eine einheitliche API mit nützlichen Hilfsfunktionen bereit, wie z.B. vielfältige "Fetch Modes", um die Struktur der abgerufenen Daten zu steuern.[2] Die konsistente Funktionsweise über verschiedene Datenbanktreiber hinweg vereinfacht die Entwicklung.
- Moderne Fehlerbehandlung: PDO verwendet standardmäßig Exceptions (
PDOException
) zur Signalisierung von Fehlern.[1, 2, 7] Dies ermöglicht eine strukturierte und moderne Fehlerbehandlung mittelstry...catch
-Blöcken, was als Best Practice gilt.[1, 2]
Verbindungsaufbau mit PDO (Beispiel MySQL):
- DSN (Data Source Name): Die Verbindungsparameter werden in einer Zeichenkette, dem DSN, übergeben. Für MySQL hat der DSN typischerweise das Format
mysql:host=hostname;dbname=datenbankname;charset=zeichensatz;unix_socket=pfad_zum_socket
. Die Angabe des Zeichensatzes (z.B.utf8mb4
) ist wichtig für die korrekte Datenübertragung.[1, 7] Beispiel:mysql:host=localhost;dbname=test;charset=utf8mb4
. - PDO-Konstruktor: Eine Verbindung wird durch Instanziierung der PDO-Klasse hergestellt:
$pdo = new PDO($dsn, $username, $password, $options);
.[1, 7, 8] Die Parameter sind der DSN, der Datenbankbenutzername, das Passwort und ein optionales Array mit Treiberoptionen.
Verbindungsoptionen ($options
): Über dieses Array können wichtige Verhaltensweisen von PDO gesteuert werden:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
: Dies ist die empfohlene Einstellung für die Fehlerbehandlung. PDO wirft bei Fehlern einePDOException
, die abgefangen werden kann.[1, 2, 4]PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
: Legt fest, dass Daten standardmäßig als assoziatives Array (spaltenname => wert
) zurückgegeben werden, was oft die Weiterverarbeitung erleichtert.[1, 9]PDO::ATTR_EMULATE_PREPARES => false
: Deaktiviert die Emulation von Prepared Statements durch PDO und zwingt die Verwendung nativer Prepared Statements des Datenbanktreibers. Dies gilt als die sicherste und oft performanteste Methode.[1, 10, 9, 11] Standardmäßig ist die Emulation aktiviert (true
). Obwohl PDO auch im emulierten Modus durch korrektes Escaping Schutz vor SQL Injection bietet [12, 5, 13], eliminiert die Deaktivierung potenzielle, wenn auch seltene, Sicherheitsrisiken, die in bestimmten Konstellationen (z.B. bei Verwendung exotischer Zeichensätze wie GBK [11]) auftreten könnten. Die explizite Deaktivierung ist daher eine wichtige Best Practice.PDO::ATTR_PERSISTENT => true
: Aktiviert persistente Datenbankverbindungen. Diese werden nicht am Ende des Skripts geschlossen, sondern in einem Pool gehalten und für nachfolgende Anfragen wiederverwendet. Dies kann die Performance von Webanwendungen verbessern, da der Overhead des Verbindungsaufbaus entfällt. Die Verwendung erfordert jedoch sorgfältige Überlegung, da der Zustand der Verbindung zwischen Anfragen bestehen bleiben kann.[7]
Fehlerbehandlung: Der Verbindungsaufbau sollte immer in einem try...catch
-Block erfolgen, um PDOException
abzufangen und angemessen darauf zu reagieren (z.B. Fehlermeldung loggen, alternative Aktion ausführen).[1, 7] Es ist kritisch, detaillierte Fehlermeldungen in Produktionsumgebungen nicht an den Benutzer auszugeben, da sie sensible Informationen enthalten könnten. Die PHP-Einstellung display_errors
sollte daher auf 0
gesetzt sein.[7]
Verbindung schließen: Eine PDO-Verbindung bleibt für die Lebensdauer des PDO-Objekts aktiv. Um sie explizit zu schließen, müssen alle Referenzen auf das PDO-Objekt und alle davon abgeleiteten PDOStatement
-Objekte entfernt werden, typischerweise durch Zuweisung von null
.[7] PHP schließt die Verbindung automatisch am Ende des Skriptlaufs, wenn dies nicht manuell geschieht.
Code-Beispiel (PDO-Verbindung zu MySQL):
<?php
$host = 'localhost';
$dbname = 'meine_datenbank';
$username = 'mein_benutzer';
$password = 'mein_passwort';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
$options =;
try {
$pdo = new PDO($dsn, $username, $password, $options);
// Erfolgreich verbunden, $pdo-Objekt kann nun verwendet werden.
// echo "Verbindung erfolgreich hergestellt!";
} catch (\PDOException $e) {
// Fehler beim Verbindungsaufbau
// In Produktion: Fehler loggen, statt ausgeben
error_log("Datenbankverbindungsfehler: ". $e--->getMessage());
die("Es gab ein Problem mit der Datenbankverbindung. Bitte versuchen Sie es später erneut.");
}
//... hier Datenbankoperationen mit $pdo durchführen...
// Verbindung explizit schließen (optional, da PHP dies am Skriptende tut)
// $pdo = null;
?>
Dieses Beispiel zeigt den empfohlenen Weg, eine PDO-Verbindung zu MySQL herzustellen, inklusive wichtiger Optionen für Fehlerbehandlung und Sicherheit sowie einem try...catch
-Block zum Abfangen von Verbindungsfehlern.
MySQLi (MySQL Improved)
Die MySQLi-Erweiterung ist, wie der Name schon sagt, speziell für die Interaktion mit MySQL-Datenbanken konzipiert und bietet Unterstützung für Funktionen, die ab MySQL Version 4.1 eingeführt wurden.[14, 15] Sie ermöglicht den Zugriff auf MySQL-spezifische Features, die über PDO möglicherweise nicht direkt verfügbar sind.[16, 15]
- Duale Schnittstelle: Ein charakteristisches Merkmal von MySQLi ist die Unterstützung von zwei Programmierstilen [17]:
- Prozedural: Dieser Stil ähnelt den alten
mysql_*
Funktionen. Die Funktionsnamen beginnen typischerweise mitmysqli_
(z.B.mysqli_connect()
,mysqli_query()
). Der Datenbankverbindungs-Handle (eine Ressource oder ein Objekt) muss dabei explizit als erster Parameter an die meisten Funktionen übergeben werden.[17] Dieser Stil wird oft von Entwicklern bevorzugt, die von der altenmysql
-Erweiterung migrieren.[17] - Objektorientiert (OO): Hier wird ein Objekt der
mysqli
-Klasse erstellt, und Datenbankoperationen werden als Methoden dieses Objekts aufgerufen (z.B.$mysqli->query()
,$mysqli->prepare()
).[17] Dieser Ansatz passt oft besser zu modernen PHP-Entwicklungspraktiken und vermeidet die Notwendigkeit, den Verbindungshandle ständig zu übergeben. Die offizielle PHP-Dokumentation für MySQLi ist primär im objektorientierten Stil verfasst.[17] Es gibt keine nennenswerten Performance-Unterschiede zwischen den beiden Stilen; die Wahl ist meist eine Frage der persönlichen Präferenz oder Projektkonventionen.[17]
- Prozedural: Dieser Stil ähnelt den alten
Verbindungsaufbau mit MySQLi:
- Prozedural: Die Funktion
mysqli_connect()
wird verwendet:$link = mysqli_connect($host, $user, $pass, $db, $port, $socket);
. Sie gibt bei Erfolg einmysqli
-Objekt (oderfalse
bei Fehlern vor PHP 8.1) zurück.[17, 18] Fehler können mitmysqli_connect_errno()
undmysqli_connect_error()
geprüft werden.[18] - Objektorientiert: Ein neues
mysqli
-Objekt wird instanziiert:$mysqli = new mysqli($host, $user, $pass, $db, $port, $socket);
. Wichtig: Der Konstruktor gibt immer ein Objekt zurück, auch wenn die Verbindung fehlschlägt. Der Verbindungsstatus muss explizit über die Eigenschaften$mysqli->connect_errno
und$mysqli->connect_error
geprüft werden.[17, 18]
Fortgeschrittene Verbindung: Für feinere Kontrolle über Verbindungsoptionen (z.B. Setzen von Timeouts oder Initialisierungsbefehlen vor dem Verbindungsaufbau) können mysqli_init()
, mysqli_options()
und mysqli_real_connect()
verwendet werden.[18, 19]
- Fehlerbehandlung: Neben der manuellen Prüfung von Fehlercodes und -meldungen ist die empfohlene Methode, MySQLi so zu konfigurieren, dass es bei Fehlern Exceptions wirft. Dies geschieht mit
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
. Danach können Datenbankfehler elegant mittry...catch
-Blöcken behandelt werden, ähnlich wie bei PDO.[18, 20, 21, 22, 23, 24, 25, 26]
Verbindung schließen: Prozedural mit mysqli_close($link)
, objektorientiert mit $mysqli->close()
.[17, 18, 19]
Persistente Verbindungen: MySQLi unterstützt ebenfalls persistente Verbindungen, die über PHP-Konfigurationseinstellungen (mysqli.allow_persistent
, mysqli.max_persistent
) gesteuert werden.[27] Standardmäßig setzt MySQLi den Zustand einer wiederverwendeten persistenten Verbindung zurück (via mysqli::change_user()
), was zwar für Konsistenz sorgt, aber auch Performance kosten kann.[27]
Code-Beispiele (MySQLi-Verbindungen):
Objektorientiert (mit Exception-Handling):
<?php
$host = 'localhost';
$user = 'mein_benutzer';
$pass = 'mein_passwort';
$db = 'meine_datenbank';
// Fehlerberichterstattung für Exceptions aktivieren
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$mysqli = new mysqli($host, $user, $pass, $db);
// Zeichensatz setzen (wichtig!)
$mysqli--->set_charset('utf8mb4');
// Erfolgreich verbunden
// echo "Verbindung erfolgreich (OO)! Host-Info: ". $mysqli->host_info;
} catch (mysqli_sql_exception $e) {
// Fehler beim Verbindungsaufbau oder set_charset
error_log("MySQLi Verbindungsfehler: ". $e->getMessage());
die("Es gab ein Problem mit der Datenbankverbindung.");
}
//... Datenbankoperationen mit $mysqli...
// Verbindung schließen
// $mysqli->close();
?>
- Prozedural (mit Exception-Handling):
<!--?php$host = 'localhost';
$user = 'mein_benutzer';
$pass = 'mein_passwort';
$db = 'meine_datenbank';
// Fehlerberichterstattung für Exceptions aktivieren
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$link = mysqli_connect($host, $user, $pass, $db);
// Zeichensatz setzen
mysqli_set_charset($link, 'utf8mb4');
// Erfolgreich verbunden
// echo "Verbindung erfolgreich (Prozedural)! Host-Info: ". mysqli_get_host_info($link);
} catch (mysqli_sql_exception $e) {
// Fehler beim Verbindungsaufbau oder set_charset
error_log("MySQLi Verbindungsfehler: ". $e--->getMessage());
die("Es gab ein Problem mit der Datenbankverbindung.");
}
//... Datenbankoperationen mit $link...
// Verbindung schließen
// mysqli_close($link);
?>
Bedeutung von mysqlnd
: Die Benutzerfreundlichkeit, insbesondere bei der Arbeit mit Prepared Statements, wird stark durch den verwendeten MySQL-Treiber beeinflusst. Der MySQL Native Driver (mysqlnd
), der seit PHP 5.4 standardmäßig enthalten ist [16], ist hier entscheidend. mysqlnd
stellt die Methode get_result()
(bzw. die Funktion mysqli_stmt_get_result()
) zur Verfügung.[25, 15] Diese Methode erlaubt es, nach dem Ausführen eines Prepared Statements ein Standard-mysqli_result
-Objekt zu erhalten, von dem dann wie gewohnt mit fetch_assoc()
, fetch_all()
etc. die Ergebnisse abgerufen werden können.[25] Ohne mysqlnd
(bei Verwendung der älteren libmysqlclient
-Bibliothek) ist das Abrufen von Ergebnissen aus Prepared Statements deutlich umständlicher und erfordert die manuelle Bindung jeder einzelnen Ergebnisspalte an PHP-Variablen mittels bind_result()
und anschließendes fetch()
.[25] Da mysqlnd
heute der Standard ist, konzentrieren sich moderne Tutorials und Beispiele meist auf die get_result()
-Methode, aber es ist wichtig, diesen Hintergrund zu kennen, falls man auf ältere Systeme oder Konfigurationen trifft.
Veraltete mysql_*
Funktionen
Die mysql_*
Funktionsfamilie (mysql_connect
, mysql_query
, mysql_fetch_assoc
etc.) war die ursprüngliche Methode, um in PHP mit MySQL-Datenbanken zu interagieren. Diese Erweiterung ist jedoch stark veraltet und sollte unter keinen Umständen mehr für neue Projekte verwendet werden.
- Status: Die
mysql_*
Erweiterung wurde in PHP 5.5 offiziell als "deprecated" (veraltet) markiert, was bedeutet, dass ihre Verwendung ab dieser Version Warnungen (E_DEPRECATED
) erzeugte.[28, 29] Mit der Veröffentlichung von PHP 7.0 wurde die Erweiterung vollständig entfernt.[28, 29] PHP-Code, der diese Funktionen nutzt, führt auf PHP 7.0 oder neuer zu fatalen Fehlern und funktioniert nicht mehr.[28, 29] - Gründe für die Entfernung: Es gab zwingende Gründe für diesen Schritt:
- Gravierende Sicherheitsrisiken: Der Hauptgrund war die Anfälligkeit für SQL-Injection-Angriffe. Die
mysql_*
Funktionen boten keinen eingebauten Mechanismus wie Prepared Statements. Entwickler mussten Benutzereingaben manuell mitmysql_real_escape_string()
"escapen", eine Methode, die leicht vergessen oder falsch angewendet werden konnte, was zu weit verbreiteten Sicherheitslücken führte.[29, 30] Das Fehlen von Prepared Statements in dieser alten Erweiterung ist direkt ursächlich für die hohe Zahl an SQL-Injection-Schwachstellen in älteren PHP-Anwendungen.[29] - Fehlende Unterstützung moderner MySQL-Features: Die Erweiterung wurde ursprünglich für MySQL Version 3.23 entwickelt und seitdem kaum weiterentwickelt.[29] Sie unterstützte viele wichtige Funktionen moderner MySQL-Versionen nicht, darunter Prepared Statements, Stored Procedures, Transaktionen, SSL-Verschlüsselung, Kompression, Multi-Statements und vollständige Zeichensatzunterstützung.[29]
- Schlechte Wartbarkeit: Der Code der Erweiterung war veraltet und wurde immer schwieriger zu warten und an neue Versionen der MySQL-Client-Bibliotheken anzupassen.[29]
- Förderung unsicherer Praktiken: Da viele alte Tutorials und Codebeispiele die
mysql_*
Funktionen unsicher verwendeten, trug die fortgesetzte Verfügbarkeit zur Verbreitung schlechter und unsicherer Programmierpraktiken bei.[29]
- Gravierende Sicherheitsrisiken: Der Hauptgrund war die Anfälligkeit für SQL-Injection-Angriffe. Die
- Verwendung heute: Die klare Antwort lautet: Nein. Diese Funktionen existieren in modernen PHP-Versionen nicht mehr.[31, 30] Jede Anwendung, die sie noch verwendet, ist nicht nur unsicher, sondern auch inkompatibel mit aktueller PHP-Software. Eine Migration zu PDO oder MySQLi ist unumgänglich, um Sicherheit und Kompatibilität zu gewährleisten.[28, 29] Die erzwungene Migration durch die Entfernung in PHP 7.0 unterstreicht, wie wichtig es ist, Deprecation-Warnungen ernst zu nehmen und Code proaktiv zu aktualisieren, um zukünftige Probleme zu vermeiden.[29, 32]
Tabelle 1: Vergleich der PHP-Datenbankerweiterungen für MySQL
Merkmal | PDO (PHP Data Objects) | MySQLi (MySQL Improved) | mysql_* (Veraltet)
|
---|---|---|---|
|
|
Entfernt (seit PHP 7.0) [29] | |
|
|
Prozedural | |
|
|
Nur MySQL | |
|
|
Nein [29] | |
SQL Injection Schutz |
|
Sehr gut (durch Prepared Statements) [25] | Schlecht (manuelles Escaping nötig) [29] |
|
Sehr gute Unterstützung (spezifisch) [15] | Veraltet, viele Features fehlen [29] | |
|
|
Niedrig | |
|
|
Fehlercodes / mysql_error()
| |
Nicht verwenden! [29] |
Grundlegende Datenbankoperationen (CRUD)
Nachdem wir die verschiedenen Erweiterungen kennengelernt haben, wenden wir uns nun den grundlegenden Operationen zu, die man typischerweise mit einer Datenbank durchführt. Diese werden oft mit dem Akronym CRUD zusammengefasst:
- Create: Neue Datensätze erstellen (SQL:
INSERT
). - Read: Vorhandene Datensätze lesen/abfragen (SQL:
SELECT
). - Update: Bestehende Datensätze ändern (SQL:
UPDATE
). - Delete: Datensätze löschen (SQL:
DELETE
).
In den folgenden Abschnitten werden wir sehen, wie diese Operationen mit den modernen Erweiterungen PDO und MySQLi umgesetzt werden, wobei wir durchgehend Prepared Statements verwenden, um die Sicherheit zu gewährleisten.
Daten abfragen (SELECT)
Das Abfragen von Daten ist eine der häufigsten Datenbankoperationen.
- PDO:
- Ohne Parameter: Wenn die Abfrage keine variablen Teile enthält (z.B. keine
WHERE
-Klausel mit Benutzereingaben), kann diequery()
-Methode verwendet werden. Sie führt die SQL-Abfrage direkt aus und gibt einPDOStatement
-Objekt zurück, das zur Ergebnisauswertung dient.[2, 33]
- Ohne Parameter: Wenn die Abfrage keine variablen Teile enthält (z.B. keine
// Beispiel: Alle Benutzer auswählen
$stmt = $pdo->query("SELECT id, username FROM users");
// $stmt kann nun zum Fetchen der Ergebnisse verwendet werden
- Mit Parametern (Prepared Statement): Dies ist der Standardfall, wenn Benutzereingaben oder andere Variablen die Abfrage beeinflussen (z.B. in
WHERE
- oderLIMIT
-Klauseln).
- Mit Parametern (Prepared Statement): Dies ist der Standardfall, wenn Benutzereingaben oder andere Variablen die Abfrage beeinflussen (z.B. in
- Vorbereiten (
prepare
): Die SQL-Abfrage wird mit Platzhaltern (?
für positionale oder:name
für benannte Platzhalter) vorbereitet.[34, 33, 5]
// Beispiel: Benutzer mit bestimmter ID auswählen (positionaler Platzhalter)
$sql = "SELECT id, username, email FROM users WHERE id =?";
$stmt = $pdo->prepare($sql);
// Beispiel: Benutzer mit bestimmtem Status auswählen (benannter Platzhalter)
$sql_named = "SELECT id, username FROM users WHERE status = :status";
$stmt_named = $pdo->prepare($sql_named);
- Ausführen (
execute
): Die vorbereitete Anweisung wird mit den tatsächlichen Werten ausgeführt. Die Werte werden als Array an dieexecute()
-Methode übergeben. Bei positionalen Platzhaltern entspricht die Reihenfolge im Array der Reihenfolge der?
in der SQL-Abfrage. Bei benannten Platzhaltern wird ein assoziatives Array verwendet, dessen Schlüssel den Platzhalternamen entsprechen (mit oder ohne führenden Doppelpunkt).[34, 33, 5]
- Ausführen (
// Ausführen für positionalen Platzhalter
$user_id = 123;
$stmt->execute([$user_id]);
// Ausführen für benannten Platzhalter
$user_status = 'active';
$stmt_named->execute([':status' => $user_status]);
// oder: $stmt_named->execute(['status' => $user_status]);
- Alternative Bindung: Optional können Parameter auch explizit vor dem
execute()
-Aufruf mitbindValue()
(bindet einen Wert) oderbindParam()
(bindet eine Variable als Referenz) gebunden werden. Dies gibt mehr Kontrolle über den Datentyp (z.B.PDO::PARAM_INT
,PDO::PARAM_STR
).[5, 13]
- Alternative Bindung: Optional können Parameter auch explizit vor dem
$stmt->bindValue(1, $user_id, PDO::PARAM_INT); // Position 1, Wert von $user_id als Integer
$stmt->execute();
$stmt_named->bindParam(':status', $user_status, PDO::PARAM_STR); // Platzhalter :status, Variable $user_status als String
$stmt_named->execute();
- MySQLi:
- Ohne Parameter: Ähnlich wie bei PDO kann bei Abfragen ohne variable Teile die
query()
-Methode (OO-Stil:$mysqli->query()
) oder diemysqli_query()
-Funktion (prozeduraler Stil:mysqli_query($link, "...")
) verwendet werden. Sie geben einmysqli_result
-Objekt zurück.[20, 25]
- Ohne Parameter: Ähnlich wie bei PDO kann bei Abfragen ohne variable Teile die
- MySQLi:
// OO-Stil
$result = $mysqli->query("SELECT id, username FROM users");
// Prozedural
$result = mysqli_query($link, "SELECT id, username FROM users");
- Mit Parametern (Prepared Statement):
- Vorbereiten (
prepare
): Die SQL-Abfrage wird mit?
-Platzhaltern vorbereitet. Im OO-Stil$stmt = $mysqli->prepare("...")
, prozedural$stmt = mysqli_prepare($link, "...")
.[25, 35]
// OO-Stil
$sql = "SELECT id, username, email FROM users WHERE id =?";
$stmt = $mysqli->prepare($sql);
// Prozedural
$sql_proc = "SELECT id, username, email FROM users WHERE id =?";
$stmt_proc = mysqli_prepare($link, $sql_proc);
- Parameter binden (
bind_param
): Die Variablen werden an die Platzhalter gebunden. Dies ist ein wesentlicher Unterschied zu PDOsexecute
-Array-Bindung.bind_param()
erfordert einen String, der die Datentypen der zu bindenden Variablen angibt (z.B. "i" für Integer, "s" für String, "d" für Double/Float, "b" für Blob), gefolgt von den Variablen selbst.[25, 35]
- Parameter binden (
$user_id = 123;
// OO-Stil
$stmt->bind_param("i", $user_id); // "i" für Integer
// Prozedural
mysqli_stmt_bind_param($stmt_proc, "i", $user_id);
- Ausführen (
execute
): Die vorbereitete Anweisung wird ausgeführt. OO-Stil:$stmt->execute()
, prozedural:mysqli_stmt_execute($stmt)
.[25, 36, 35]
- Ausführen (
// OO-Stil
$stmt->execute();
// Prozedural
mysqli_stmt_execute($stmt_proc);
- Ergebnis holen: Um die Ergebnisse eines Prepared Statements in MySQLi zu erhalten, ist die Methode
get_result()
(OO) bzw.mysqli_stmt_get_result()
(prozedural) am bequemsten. Sie gibt ein Standard-mysqli_result
-Objekt zurück, das dann mit den üblichen Fetch-Methoden verarbeitet werden kann. Wichtig: Diese Methode erfordert denmysqlnd
-Treiber.[25, 15]
- Ergebnis holen: Um die Ergebnisse eines Prepared Statements in MySQLi zu erhalten, ist die Methode
// OO-Stil
$result = $stmt->get_result();
// Prozedural
$result_proc = mysqli_stmt_get_result($stmt_proc);
// $result / $result_proc kann nun mit fetch_assoc() etc. verwendet werden.
Die Alternative ohne mysqlnd
ist bind_result()
, bei der jede Ergebnisspalte an eine PHP-Variable gebunden wird, und fetch()
iterativ aufgerufen wird, um die Variablen zu füllen.[25] Dies ist deutlich umständlicher.
Ergebnisse abrufen und verarbeiten
Nachdem eine SELECT
-Abfrage erfolgreich ausgeführt wurde, müssen die zurückgegebenen Daten aus dem Ergebnisobjekt (PDOStatement
oder mysqli_result
) abgerufen ("gefetched") werden.
- Zeilenweise Iteration (
fetch
im Loop):
- Zeilenweise Iteration (
Dies ist der klassische Weg, um Ergebnisse zu verarbeiten, insbesondere wenn jede Zeile einzeln behandelt werden muss oder wenn die Ergebnismenge sehr groß ist.
- PDO: Die
fetch()
-Methode desPDOStatement
-Objekts wird wiederholt in einerwhile
-Schleife aufgerufen.fetch()
gibt bei jedem Aufruf die nächste Zeile zurück (im Format des eingestellten Fetch-Modus, z.B.PDO::FETCH_ASSOC
) undfalse
(odernull
je nach Modus/Version), wenn keine weiteren Zeilen vorhanden sind.[2, 33]
- PDO: Die
$stmt = $pdo->prepare("SELECT name, email FROM users WHERE status =?");
$stmt->execute(['active']);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo "Name: ". htmlspecialchars($row['name']). ", E-Mail: ". htmlspecialchars($row['email']). "<br>";
}
- MySQLi: Die
fetch_assoc()
-Methode (oderfetch_row()
,fetch_object()
, etc.) desmysqli_result
-Objekts wird in einerwhile
-Schleife verwendet. Sie gibt die nächste Zeile als Array (oder Objekt) zurück odernull
, wenn das Ende erreicht ist.[25, 15]
- MySQLi: Die
// OO-Stil (nach $result = $stmt->get_result();)
while ($row = $result->fetch_assoc()) {
echo "Name: ". htmlspecialchars($row['name']). ", E-Mail: ". htmlspecialchars($row['email']). "<br>";
}
// Prozedural (nach $result = mysqli_stmt_get_result($stmt);)
while ($row = mysqli_fetch_assoc($result)) {
echo "Name: ". htmlspecialchars($row['name']). ", E-Mail: ". htmlspecialchars($row['email']). "<br>";
}
Dieser Ansatz ist speichereffizient, da immer nur eine Zeile gleichzeitig im PHP-Speicher gehalten wird. Dies ist besonders wichtig bei Abfragen, die potenziell Tausende oder Millionen von Zeilen zurückgeben könnten.[37]
- Alle Ergebnisse auf einmal abrufen (
fetchAll
/fetch_all
):
- Alle Ergebnisse auf einmal abrufen (
Für kleinere bis mittlere Ergebnismengen ist es oft bequemer, alle Zeilen auf einmal in ein PHP-Array zu laden.
- PDO: Die
fetchAll()
-Methode desPDOStatement
-Objekts gibt ein Array zurück, das alle Ergebniszeilen enthält.[33, 38] Der Fetch-Modus (z.B.PDO::FETCH_ASSOC
) kann als Argument übergeben werden.
- PDO: Die
$stmt = $pdo->prepare("SELECT name, email FROM users WHERE status =?");
$stmt->execute(['active']);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
// $results ist nun ein Array von assoziativen Arrays
- MySQLi: Die
fetch_all()
-Methode desmysqli_result
-Objekts (verfügbar mitmysqlnd
) tut dasselbe. Der Modus (MYSQLI_ASSOC
,MYSQLI_NUM
,MYSQLI_BOTH
) wird als Argument übergeben.[25, 39]
- MySQLi: Die
// OO-Stil (nach $result = $stmt->get_result();)
$results = $result->fetch_all(MYSQLI_ASSOC);
// Prozedural (nach $result = mysqli_stmt_get_result($stmt);)
$results = mysqli_fetch_all($result, MYSQLI_ASSOC);
Der Vorteil dieser Methode liegt in der einfacheren Weiterverarbeitung des Ergebnis-Arrays mit Standard-PHP-Array-Funktionen wie foreach
oder count
.[40] Der Nachteil ist der potenziell hohe Speicherverbrauch, da alle Daten gleichzeitig in den PHP-Speicher geladen werden. Bei sehr großen Ergebnismengen kann dies zu Speicherlimit-Fehlern führen.[37]
- Fetch-Modi / Fetch-Funktionen (Struktur der Ergebniszeile):
Beide Erweiterungen bieten verschiedene Möglichkeiten, die Struktur der abgerufenen Datenzeilen zu bestimmen:
- PDO Fetch-Modi (Auswahl):
PDO::FETCH_ASSOC
: Gibt ein assoziatives Array zurück (['spaltenname' => 'wert',... ]
).[9, 33, 41, 42] Sehr gebräuchlich.PDO::FETCH_NUM
: Gibt ein numerisch indiziertes Array zurück ([0 => 'wert1', 1 => 'wert2',...]
).[9, 41, 42]PDO::FETCH_BOTH
(Standard): Gibt ein Array zurück, das sowohl assoziative als auch numerische Indizes enthält.[41, 42] Verbraucht mehr Speicher.PDO::FETCH_OBJ
: Gibt ein anonymesstdClass
-Objekt zurück, bei dem die Spaltennamen Eigenschaften sind ($row->spaltenname
).[9, 41, 42, 43]PDO::FETCH_CLASS
: Erstellt eine Instanz einer angegebenen Klasse und weist die Spaltenwerte den Klasseneigenschaften zu.[38, 41, 42, 43]PDO::FETCH_COLUMN
: Ruft nur den Wert einer einzelnen Spalte aus der nächsten Zeile ab.[38]PDO::FETCH_LAZY
: Eine Kombination ausFETCH_BOTH
undFETCH_OBJ
, die Werte erst bei Zugriff lädt.[9, 41, 42]
- MySQLi Fetch-Funktionen (Auswahl):
fetch_assoc()
/mysqli_fetch_assoc()
: Gibt ein assoziatives Array zurück.[17, 25, 39, 15] Sehr gebräuchlich.fetch_row()
/mysqli_fetch_row()
: Gibt ein numerisch indiziertes Array zurück.[39, 35]fetch_array()
/mysqli_fetch_array()
: Gibt je nach übergebenem Modus (MYSQLI_ASSOC
,MYSQLI_NUM
,MYSQLI_BOTH
) ein assoziatives, numerisches oder beides enthaltendes Array zurück.[39, 35]fetch_object()
/mysqli_fetch_object()
: Gibt einstdClass
-Objekt zurück oder eine Instanz einer angegebenen Klasse.[21, 15, 35]fetch_column()
/mysqli_fetch_column()
: Ruft den Wert einer einzelnen Spalte ab (erst ab PHP 8.1 verfügbar).[24]fetch_all()
/mysqli_fetch_all()
: Ruft alle Zeilen auf einmal ab, Modus (MYSQLI_ASSOC
,MYSQLI_NUM
,MYSQLI_BOTH
) wählbar.[21, 25, 39]
- PDO Fetch-Modi (Auswahl):
Tabelle 2: Ausgewählte PDO Fetch-Modi
Konstante | Rückgabeformat | Beschreibung |
---|---|---|
PDO::FETCH_ASSOC
|
[" col'=">" 'val']<="" code>')="" > |
Gängigste Form, Spaltennamen als Schlüssel. |
|
|
Spaltenwerte über numerischen Index erreichbar. |
|
|
Standard, enthält doppelte Informationen, höherer Speicherbedarf. |
|
|
Spaltennamen werden zu Objekteigenschaften. |
PDO::FETCH_CLASS |
|
Mappt Spalten auf Eigenschaften einer vordefinierten Klasse. |
PDO::FETCH_COLUMN |
|
Gibt nur den Wert der ersten (oder angegebenen) Spalte der nächsten Zeile zurück. |
Tabelle 3: Ausgewählte MySQLi Fetch-Funktionen
Funktion (OO / Prozedural) | Rückgabeformat | Beschreibung |
---|---|---|
|
|
Gängigste Form, Spaltennamen als Schlüssel. |
|
|
Spaltenwerte über numerischen Index erreichbar. |
|
|
Flexibel je nach Modus (MYSQLI_ASSOC , MYSQLI_NUM , MYSQLI_BOTH ).
|
fetch_object() / mysqli_fetch_object()
|
stdClass" -objekt="" oder="" spezifische="" klasse="" > |
Spaltennamen werden zu Objekteigenschaften. |
|
|
Holt alle Ergebniszeilen auf einmal (benötigt mysqlnd ).
|
fetch_column() / mysqli_fetch_column() |
|
Gibt nur den Wert der ersten (oder angegebenen) Spalte zurück (PHP 8.1+). |
- Zugriff auf Spaltenwerte:
Der Zugriff auf die Daten innerhalb einer abgerufenen Zeile hängt vom gewählten Fetch-Modus ab:
- Assoziatives Array:
$row['spaltenname']
- Numerisches Array:
$row
,$row[1]
,... - Objekt:
$row->spaltenname
- Assoziatives Array:
- Iteration mit
foreach
:
Nachdem Ergebnisse mit fetchAll()
oder fetch_all()
in ein Array geladen wurden, ist foreach
die natürliche Wahl zur Iteration [37, 40, 44]:
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($results as $row) {
echo "Name: ". htmlspecialchars($row['name']). "<br>";
}
Seit PHP 8 bietet PDO eine modernere Alternative, die die Lesbarkeit von foreach
mit der Speichereffizienz von fetch()
kombiniert, indem direkt über das PDOStatement
-Objekt iteriert wird [37, 45]:
// Nur PHP 8+ mit PDO
$stmt = $pdo->query("SELECT name, email FROM users");
foreach ($stmt as $row) { // PDOStatement ist direkt traversierbar
echo "Name: ". htmlspecialchars($row['name']). "<br>";
}
Diese Methode ist besonders elegant und effizient für große Ergebnismengen in modernen PHP-Versionen.
Daten manipulieren (INSERT, UPDATE, DELETE)
Das Einfügen, Aktualisieren und Löschen von Daten sind kritische Operationen, da sie den Datenbestand verändern. Hier ist die Verwendung von Prepared Statements zwingend erforderlich, um die Integrität und Sicherheit der Datenbank zu gewährleisten, insbesondere wenn Benutzereingaben beteiligt sind.
- PDO:
- Vorbereiten (
prepare
): Die SQL-Anweisung (INSERT
,UPDATE
oderDELETE
) wird mit Platzhaltern (?
oder:name
) für die einzufügenden/zu aktualisierenden Werte sowie für die Kriterien inWHERE
-Klauseln (beiUPDATE
undDELETE
) vorbereitet.[5, 46, 47, 48] - Ausführen (
execute
): Die Anweisung wird mit einem Array ausgeführt, das die Werte für die Platzhalter enthält.[5, 46, 47, 48] - Ergebnis prüfen:
- Anzahl betroffener Zeilen:
$stmt->rowCount()
gibt die Anzahl der durchUPDATE
oderDELETE
betroffenen Zeilen zurück. BeiINSERT
ist der Rückgabewert oft nicht zuverlässig oder standardisiert.[8, 9, 5, 49] Das Verhalten kann je nach Datenbanktreiber und Konfiguration (z.B.PDO::MYSQL_ATTR_FOUND_ROWS
) variieren.[9] - ID des letzten eingefügten Datensatzes: Bei
INSERT
-Anweisungen in Tabellen mit einerAUTO_INCREMENT
-Spalte liefert$pdo->lastInsertId()
die ID des neu eingefügten Datensatzes zurück.[8, 33, 48]
- Anzahl betroffener Zeilen:
- Code-Beispiele (PDO):
- Vorbereiten (
- PDO:
<!--?php// --- INSERT ---
$name = $_POST['name'];
$email = $_POST['email'];
$status = 'pending';
$sql_insert = "INSERT INTO users (username, email, status) VALUES (:name, :email, :status)";
$stmt_insert = $pdo--->prepare($sql_insert);
$stmt_insert->execute([':name' => $name, ':email' => $email, ':status' => $status]);
$lastId = $pdo->lastInsertId();
echo "Neuer Benutzer mit ID $lastId eingefügt.<br>";
// --- UPDATE ---
$new_status = 'active';
$user_id_to_update = $lastId;
$sql_update = "UPDATE users SET status = :status WHERE id = :id";
$stmt_update = $pdo->prepare($sql_update);
$stmt_update->execute([':status' => $new_status, ':id' => $user_id_to_update]);
$affectedRows = $stmt_update->rowCount();
echo "$affectedRows Benutzerstatus aktualisiert.<br>";
// --- DELETE ---
$user_id_to_delete = $lastId;
$sql_delete = "DELETE FROM users WHERE id = :id";
$stmt_delete = $pdo->prepare($sql_delete);
$stmt_delete->execute([':id' => $user_id_to_delete]);
$affectedRowsDel = $stmt_delete->rowCount();
echo "$affectedRowsDel Benutzer gelöscht.<br>";
?>
- MySQLi:
- Vorbereiten (
prepare
): Die SQL-Anweisung wird mit?
-Platzhaltern vorbereitet (OO:$mysqli->prepare()
, Proc:mysqli_prepare()
).[25, 50, 15, 35] - Parameter binden (
bind_param
): Variablen werden explizit mit Typenangabe gebunden (OO:$stmt->bind_param()
, Proc:mysqli_stmt_bind_param()
).[25, 50, 15, 35] Die Notwendigkeit, den Datentyp für jede Variable anzugeben, ist ein wesentlicher Unterschied zur einfacheren Array-Übergabe bei PDOsexecute()
. - Ausführen (
execute
): Die Anweisung wird ausgeführt (OO:$stmt->execute()
, Proc:mysqli_stmt_execute()
).[25, 36, 50, 15, 35] - Ergebnis prüfen:
- Anzahl betroffener Zeilen: Über die Eigenschaft
$mysqli->affected_rows
oder$stmt->affected_rows
(OO) bzw. die Funktionenmysqli_affected_rows($link)
odermysqli_stmt_affected_rows($stmt)
(Proc).[18, 26, 15, 35] - ID des letzten eingefügten Datensatzes: Über
$mysqli->insert_id
oder$stmt->insert_id
(OO) bzw.mysqli_insert_id($link)
odermysqli_stmt_insert_id($stmt)
(Proc).[26, 15] Die Verwendung von$mysqli->insert_id
wird oft als zuverlässiger angesehen, da sie auch nach dem Schließen des Statements funktioniert.[15]
- Anzahl betroffener Zeilen: Über die Eigenschaft
- Code-Beispiele (MySQLi OO):
- Vorbereiten (
- MySQLi:
<!--?php// --- INSERT ---
$name = $_POST['name'];
$email = $_POST['email'];
$status = 'pending';
$sql_insert = "INSERT INTO users (username, email, status) VALUES (?,?,?)";
$stmt_insert = $mysqli--->prepare($sql_insert);
// Typen: s=string, s=string, s=string
$stmt_insert->bind_param("sss", $name, $email, $status);
$stmt_insert->execute();
$lastId = $mysqli->insert_id;
echo "Neuer Benutzer mit ID $lastId eingefügt (MySQLi OO).<br>";
$stmt_insert->close(); // Statement schließen
// --- UPDATE ---
$new_status = 'active';
$user_id_to_update = $lastId;
$sql_update = "UPDATE users SET status =? WHERE id =?";
$stmt_update = $mysqli->prepare($sql_update);
// Typen: s=string, i=integer
$stmt_update->bind_param("si", $new_status, $user_id_to_update);
$stmt_update->execute();
$affectedRows = $mysqli->affected_rows; // Oder $stmt_update->affected_rows vor close()
echo "$affectedRows Benutzerstatus aktualisiert (MySQLi OO).<br>";
$stmt_update->close();
// --- DELETE ---
$user_id_to_delete = $lastId;
$sql_delete = "DELETE FROM users WHERE id =?";
$stmt_delete = $mysqli->prepare($sql_delete);
// Typ: i=integer
$stmt_delete->bind_param("i", $user_id_to_delete);
$stmt_delete->execute();
$affectedRowsDel = $mysqli->affected_rows;
echo "$affectedRowsDel Benutzer gelöscht (MySQLi OO).<br>";
$stmt_delete->close();
?>
(Anmerkung: Prozedurale Beispiele folgen demselben Muster unter Verwendung der mysqli_*
-Funktionen wie mysqli_prepare
, mysqli_stmt_bind_param
, mysqli_stmt_execute
, mysqli_insert_id
, mysqli_affected_rows
und mysqli_stmt_close
).
Sicherheit: SQL Injection verstehen und verhindern
Eines der größten Sicherheitsrisiken bei der Interaktion mit Datenbanken über Webanwendungen ist die SQL Injection (SQLi). Das Verständnis dieser Bedrohung und ihrer Abwehr ist für jeden PHP-Entwickler unerlässlich.
SQL Injection
SQL Injection ist eine Angriffstechnik, bei der ein Angreifer versucht, bösartigen oder manipulierten SQL-Code über Benutzereingabefelder einer Webanwendung (z.B. Formulare, URL-Parameter, HTTP-Header, Cookies) in die Datenbankabfragen einzuschleusen.[51, 52, 53, 54, 55, 56, 57]
Die Attacke funktioniert, indem Schwachstellen in der Anwendung ausgenutzt werden, bei denen Benutzereingaben direkt und ohne ausreichende Validierung oder Maskierung (Escaping) in SQL-Abfragestrings eingefügt (konkateniert) werden.[5, 51, 55] Dadurch wird die notwendige Trennung zwischen dem eigentlichen SQL-Befehl (Kontrollebene) und den Daten (Datenebene) aufgehoben.[51] Der Angreifer kann spezielle Zeichen (wie Anführungszeichen '
, Semikolons ;
oder Kommentare --
) verwenden, um den ursprünglichen SQL-Befehl vorzeitig zu beenden, zu verändern oder um eigene, zusätzliche SQL-Befehle anzuhängen.[51, 53]
Ein klassisches Beispiel ist eine Login-Abfrage, die etwa so aufgebaut ist:
$sql = "SELECT * FROM users WHERE username = '". $userInputUsername. "' AND password = '". $userInputPassword. "'";
Wenn ein Angreifer als Benutzernamen ' OR '1'='1
eingibt, wird die resultierende SQL-Abfrage zu:
SELECT * FROM users WHERE username = OR '1'='1' AND password = '...'
Da '1'='1'
immer wahr ist, würde diese Abfrage (je nach genauer Struktur und Datenbank) möglicherweise alle Benutzer zurückgeben oder den Login ohne korrektes Passwort ermöglichen.[5, 15, 55]
Auswirkungen und Gefahren von SQL Injection
Die potenziellen Folgen einer erfolgreichen SQL-Injection-Attacke sind gravierend und vielfältig:
- Datendiebstahl: Angreifer können auf sensible Daten zugreifen und diese auslesen, darunter Benutzerdaten, Passwörter (oft als Hashes, die aber geknackt werden können), Kreditkarteninformationen, Geschäftsgeheimnisse und andere vertrauliche Informationen.[51, 53, 56]
- Datenmanipulation/-zerstörung: Angreifer können bestehende Daten in der Datenbank verändern (z.B. Preise in einem Shop, Benutzerrollen) oder komplette Datensätze oder sogar Tabellen löschen (
DELETE
,DROP TABLE
).[51, 53, 56] - Umgehung der Authentifizierung/Identitätsdiebstahl: Wie im Beispiel oben gezeigt, können Angreifer Login-Mechanismen umgehen und sich unautorisiert Zugang verschaffen, möglicherweise sogar mit administrativen Rechten.[51, 56]
- Denial of Service (DoS): Durch das Löschen wichtiger Daten oder das Ausführen ressourcenintensiver Abfragen kann die Verfügbarkeit der Anwendung oder der Datenbank beeinträchtigt werden.[51, 56]
- Kompromittierung des Servers: In manchen Fällen können Angreifer über SQL Injection Betriebssystembefehle auf dem Datenbankserver ausführen oder vollen administrativen Zugriff auf die Datenbank erlangen, was weitreichende Konsequenzen haben kann.[51, 55, 56]
- Reputationsschaden und rechtliche Folgen: Erfolgreiche Angriffe, insbesondere wenn sie zu Datenlecks führen, können das Vertrauen der Benutzer schwer beschädigen und zu empfindlichen Strafen gemäß Datenschutzgesetzen führen.[53]
Prävention durch Prepared Statements (Primäre Methode)
Die mit Abstand wichtigste und effektivste Methode zur Verhinderung von SQL Injection ist die konsequente Verwendung von Prepared Statements (parametrisierten Abfragen).[4, 25, 5, 58, 35]
Das Grundprinzip von Prepared Statements ist die strikte Trennung von SQL-Befehlsstruktur und den Daten (Parametern).[4, 25, 5, 58, 35] Der Ablauf ist typischerweise zweistufig:
- Prepare (Vorbereiten): Die SQL-Anweisung wird mit Platzhaltern anstelle der tatsächlichen Werte an den Datenbankserver gesendet. Der Server parst die Anweisung, prüft die Syntax und bereitet einen Ausführungsplan vor, ohne die endgültigen Daten zu kennen.
- Execute (Ausführen) mit Parameterbindung: Die tatsächlichen Werte (Parameter) werden separat an den Server gesendet und an die vorbereiteten Platzhalter gebunden. Der Server führt nun die vorkompilierte Anweisung mit den sicher eingefügten Werten aus.
Der entscheidende Punkt ist, dass die Datenbank die separat gesendeten Parameter immer als Datenwerte behandelt, niemals als Teil des SQL-Befehls.[5, 15, 58] Selbst wenn ein Parameter bösartigen SQL-Code enthält (z.B. ' OR '1'='1
), wird dieser nicht ausgeführt, sondern als einfacher String-Wert behandelt, der z.B. in eine Spalte eingefügt oder in einer WHERE
-Klausel verglichen wird. Diese Trennung macht Prepared Statements immun gegen SQL Injection.
- Platzhalter:
- PDO: Unterstützt sowohl positionale Platzhalter (
?
) als auch benannte Platzhalter (:name
,:email
, etc.).[5, 13] Benannte Platzhalter machen den Code oft lesbarer, besonders bei vielen Parametern. - MySQLi: Unterstützt ausschließlich positionale Platzhalter (
?
).[25]
- PDO: Unterstützt sowohl positionale Platzhalter (
- Parameterbindung:
- PDO: Bietet flexible Bindungsoptionen:
- Implizit über
execute()
-Array: Die einfachste und häufigste Methode. Ein Array mit den Werten wird anexecute()
übergeben. PDO kümmert sich um das korrekte Escaping und die Typbehandlung (oft werden Werte sicher als Strings behandelt).[4, 12, 5, 13]
- Implizit über
- PDO: Bietet flexible Bindungsoptionen:
- Platzhalter:
$stmt->execute([$wert1, $wert2]); // für? Platzhalter
$stmt->execute([':name' => $wert1, ':id' => $wert2]); // für :name Platzhalter
- Explizit mit
bindValue()
oderbindParam()
: Erlaubt die explizite Angabe des Datentyps (z.B.PDO::PARAM_INT
,PDO::PARAM_STR
) für jeden Parameter, was in manchen Fällen für Klarheit oder spezifische Datenbankoptimierungen sorgen kann.[5, 13]bindParam
bindet eine Variable (ihr Wert wird erst beiexecute()
gelesen),bindValue
bindet den aktuellen Wert einer Variablen oder einen Literalwert.
- Explizit mit
$stmt->bindValue(':id', $user_id, PDO::PARAM_INT);
$stmt->bindParam(':name', $user_name, PDO::PARAM_STR);
$stmt->execute();
- MySQLi: Erfordert immer die explizite Bindung mittels
bind_param()
.bind_param()
(OO) /mysqli_stmt_bind_param()
(Proc): Nimmt als ersten Parameter einen String entgegen, der die Datentypen aller nachfolgend übergebenen Variablen definiert.[25, 50, 15, 58]
- MySQLi: Erfordert immer die explizite Bindung mittels
// OO-Stil
$stmt->bind_param("ssi", $name, $email, $id); // String, String, Integer
// Prozedural
mysqli_stmt_bind_param($stmt, "ssi", $name, $email, $id);
Tabelle 4: MySQLi bind_param()
Typen-String
Typ-Buchstabe | PHP-Datentyp (typisch) | Beschreibung |
---|---|---|
|
|
Integer (Ganzzahl) |
|
|
Double (Gleitkommazahl) |
|
|
String (Zeichenkette) |
|
|
BLOB (Binary Large Object), als String gesendet |
- Detaillierte Code-Beispiele zur Prävention:
Die Beispiele für INSERT, UPDATE und DELETE in Abschnitt 3.3 demonstrieren bereits die korrekte Anwendung von Prepared Statements mit PDO und MySQLi zur Verhinderung von SQL Injection. Es ist entscheidend, dieses Muster konsequent anzuwenden, wann immer externe Daten in SQL-Abfragen einfließen.
4.4 Zusätzliche Schutzmaßnahmen (Defense-in-Depth)
Obwohl Prepared Statements der wichtigste Schutz gegen SQL Injection sind, sollte Sicherheit immer als mehrschichtiges Konzept ("Defense-in-Depth") betrachtet werden.[5] Zusätzliche Maßnahmen erhöhen die Robustheit der Anwendung:
- Eingabevalidierung und -bereinigung: Überprüfen Sie alle Benutzereingaben serverseitig, bevor sie überhaupt in die Nähe der Datenbank kommen.[4, 59, 20, 5] Stellen Sie sicher, dass die Daten dem erwarteten Typ (Zahl, String, E-Mail-Format etc.) und Format entsprechen. Verwenden Sie dafür PHP-Filterfunktionen wie
filter_var()
oder reguläre Ausdrücke.[4, 5] Dies ist eine wichtige zweite Verteidigungslinie, die ungültige oder unerwartete Daten frühzeitig abfängt. Es ersetzt jedoch nicht die Notwendigkeit von Prepared Statements. - Prinzip der geringsten Rechte (Least Privilege): Konfigurieren Sie den Datenbankbenutzer, den Ihre PHP-Anwendung verwendet, mit den absolut minimal notwendigen Berechtigungen.[20, 5] Wenn die Anwendung nur Daten lesen und schreiben muss, geben Sie ihr keine Rechte zum Ändern der Tabellenstruktur (
ALTER TABLE
) oder zum Löschen von Tabellen (DROP TABLE
). Beschränken Sie den Zugriff auf die benötigten Datenbanken und Tabellen. Verwenden Sie niemals denroot
- oder Admin-Benutzer der Datenbank für die Anwendung. Dies begrenzt den potenziellen Schaden, falls doch eine Schwachstelle ausgenutzt wird. - Sichere Fehlerbehandlung: Konfigurieren Sie PHP und Ihre Datenbankerweiterung so, dass detaillierte Fehlermeldungen (insbesondere solche, die SQL-Code oder Datenbankstrukturinformationen enthalten) niemals dem Endbenutzer angezeigt werden.[20, 5] Solche Informationen sind für Angreifer wertvoll. Loggen Sie Fehler stattdessen in eine sichere Datei auf dem Server, die nur für Administratoren zugänglich ist. Verwenden Sie
PDO::ERRMODE_EXCEPTION
odermysqli_report
für strukturierte Fehlerbehandlung. - Output Escaping: Obwohl nicht direkt zur Verhinderung von SQL Injection, ist es wichtig, Daten, die aus der Datenbank gelesen und im HTML-Kontext ausgegeben werden, korrekt zu escapen (z.B. mit
htmlspecialchars()
), um Cross-Site Scripting (XSS)-Angriffe zu verhindern.
- Eingabevalidierung und -bereinigung: Überprüfen Sie alle Benutzereingaben serverseitig, bevor sie überhaupt in die Nähe der Datenbank kommen.[4, 59, 20, 5] Stellen Sie sicher, dass die Daten dem erwarteten Typ (Zahl, String, E-Mail-Format etc.) und Format entsprechen. Verwenden Sie dafür PHP-Filterfunktionen wie
Die Kombination dieser Maßnahmen – mit Prepared Statements als Kernstück – bietet einen robusten Schutz gegen SQL Injection und andere verwandte Angriffe.
Nützliche PHP Array-Funktionen für Datenbankergebnisse
Wenn Daten aus der Datenbank abgerufen werden, insbesondere mit Methoden wie fetchAll()
(PDO) oder fetch_all()
(MySQLi), liegen die Ergebnisse typischerweise als PHP-Array vor (meist ein Array von assoziativen Arrays, wobei jedes innere Array eine Ergebniszeile darstellt). PHP bietet eine Reihe nützlicher Funktionen zur Verarbeitung solcher Arrays.
- Iteration mit
foreach
:
- Iteration mit
Dies ist die Standardmethode, um die einzelnen Zeilen und Spalten eines Ergebnis-Arrays zu durchlaufen.[37, 40, 44]
- Syntax:
// Iteration über die Zeilen (jedes $row ist ein assoziatives Array)
foreach ($results as $row) {
echo "ID: ". $row['id']. ", Name: ". htmlspecialchars($row['name']). "<br>";
}
// Iteration über Zeilen mit Zugriff auf den numerischen Index der Zeile
foreach ($results as $index => $row) {
echo "Zeile ". $index. ": ID = ". $row['id']. "<br>";
}
- Code-Beispiel:
<!--?php// Annahme: $results enthält das Ergebnis von $stmt--->fetchAll(PDO::FETCH_ASSOC);
$results = ['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'],
;
echo "
";
foreach ($results as $row) {
echo "
- Benutzer #". $row['id']. ": ". htmlspecialchars($row['name']);
echo " (". htmlspecialchars($row['email']). ")
"; } echo ""; ?>
- Zählen von Ergebnissen mit
count()
:
- Zählen von Ergebnissen mit
Die Funktion count()
gibt die Anzahl der Elemente in einem Array zurück. Wenn sie auf ein mit fetchAll()
oder fetch_all()
erzeugtes Ergebnis-Array angewendet wird, liefert sie die Anzahl der abgerufenen Zeilen.[40, 60, 61]
- Syntax:
$anzahlZeilen = count($results);
.[60] - Wichtiger Hinweis:
count()
funktioniert nur zuverlässig für die Gesamtzahl der Zeilen, wenn alle Zeilen zuvor in ein Array geladen wurden (also nachfetchAll
/fetch_all
). Wenn Sie Ergebnisse zeilenweise mitfetch()
in einerwhile
-Schleife verarbeiten, müssen Sie einen manuellen Zähler innerhalb der Schleife inkrementieren, um die Anzahl der verarbeiteten Zeilen zu ermitteln.[40] Alternativ könnenPDOStatement::rowCount()
[8, 9] odermysqli_result::$num_rows
/mysqli_stmt::$num_rows
[26, 15] verwendet werden, um die Anzahl der von einerSELECT
-Abfrage zurückgegebenen Zeilen zu erhalten, bevor die Daten gefetched werden (das Verhalten vonrowCount
kann jedoch bei anderen Anweisungstypen wieUPDATE
variieren). - Code-Beispiel:
- Syntax:
<!--?php
// Annahme: $results von fetchAll()
$results = [ /*... wie oben... */ ];
$anzahl = count($results);
echo "Anzahl der Benutzer (via fetchAll/count): $anzahl<br-->";
// Vergleich: Zählen bei zeilenweiser Verarbeitung
$stmt = $pdo->query("SELECT id FROM users"); // Annahme: PDO-Statement
$manueller_zaehler = 0;
while ($row = $stmt->fetch()) {
$manueller_zaehler++;
}
echo "Anzahl der Benutzer (via while/fetch): $manueller_zaehler<br>";
// Alternative: rowCount() nach SELECT (PDO)
$stmt_count = $pdo->query("SELECT id FROM users");
$rowCountResult = $stmt_count->rowCount(); // Kann bei manchen DBs/Treibern funktionieren
echo "Anzahl der Benutzer (via rowCount nach SELECT): $rowCountResult<br>";
?>
- (Optional) Weitere nützliche Array-Funktionen:
array_column(array $input, mixed $column_key [, mixed $index_key = null ])
: Extrahiert alle Werte aus einer einzelnen Spalte des Ergebnis-Arrays und gibt sie als neues, eindimensionales Array zurück.[62] Sehr nützlich, um z.B. eine Liste aller Benutzer-IDs oder E-Mail-Adressen zu erhalten.
- (Optional) Weitere nützliche Array-Funktionen:
// Annahme: $results enthält das Array von assoziativen Arrays
$alle_emails = array_column($results, 'email');
// $alle_emails ist nun ['alice@example.com', 'bob@example.com']
print_r($alle_emails);
array_map(callable $callback, array $array1 [, array...$arrays ])
: Wendet eine benutzerdefinierte Funktion (callback
) auf jedes Element eines Arrays an und gibt ein neues Array mit den Ergebnissen zurück.[63] Nützlich zur Transformation von Daten (z.B. Formatierung, Bereinigung).array_filter(array $array [, callable $callback [, int $flag = 0 ]])
: Filtert Elemente eines Arrays basierend auf einer Callback-Funktion.[61] Nützlich, um nur bestimmte Zeilen aus dem Ergebnis-Array zu behalten (obwohl dies oft effizienter direkt in der SQL-Abfrage geschieht).array_count_values(array $array)
: Zählt, wie oft jeder eindeutige Wert in einem eindimensionalen Array vorkommt.[63] Kann nützlich sein, wenn man z.B. mitarray_column
eine Spalte extrahiert hat und die Häufigkeit bestimmter Werte (z.B. Status) wissen möchte.
Diese Funktionen, insbesondere foreach
und count
, sind grundlegende Werkzeuge bei der Verarbeitung von Datenbankergebnissen in PHP.
Zusammenfassung & Best Practices
Die Integration von Datenbanken ist ein zentraler Aspekt der PHP-Webentwicklung. Dieses Kapitel hat die Grundlagen für die Interaktion mit MySQL-Datenbanken gelegt. Hier sind die wichtigsten Punkte und Best Practices zusammengefasst:
Wahl der Erweiterung: Verwenden Sie ausschließlich die modernen Erweiterungen PDO oder MySQLi.[2, 29, 5, 15]
- PDO wird generell für neue Projekte empfohlen, da es Datenbankunabhängigkeit bietet und eine konsistentere, oft als einfacher empfundene API (insbesondere bei der Parameterbindung) hat.[16, 3, 64]
- MySQLi ist eine gute Wahl, wenn ausschließlich mit MySQL gearbeitet wird und spezifische MySQL-Funktionen benötigt werden oder wenn eine prozedurale Schnittstelle bevorzugt wird.[15]
Prepared Statements: Dies ist die wichtigste Sicherheitsmaßnahme. Verwenden Sie Prepared Statements immer dann, wenn externe Daten (Benutzereingaben, URL-Parameter etc.) Teil einer SQL-Abfrage werden (insbesondere bei WHERE
, INSERT
, UPDATE
, DELETE
).[2, 25, 5] Sie verhindern SQL Injection zuverlässig durch die Trennung von Code und Daten.
Sichere Fehlerbehandlung: Konfigurieren Sie PDO (PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
) oder MySQLi (mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
) so, dass Fehler als Exceptions behandelt werden.[1, 18] Fangen Sie diese Exceptions ab und loggen Sie detaillierte Fehlermeldungen serverseitig, anstatt sie dem Benutzer anzuzeigen.[20, 5]
Input Validation: Validieren Sie alle Benutzereingaben serverseitig auf Typ, Format und erlaubte Werte, bevor Sie sie an die Datenbank übergeben. Dies ist eine wichtige zusätzliche Schutzschicht.[4, 5]
Prinzip der geringsten Rechte (Least Privilege): Der Datenbankbenutzer, den Ihre PHP-Anwendung verwendet, sollte nur die minimal notwendigen Berechtigungen für seine Aufgaben besitzen.[5]
Sichere Zugangsdaten: Speichern Sie Datenbank-Credentials (Hostname, Benutzername, Passwort) niemals direkt im PHP-Code oder in Dateien innerhalb des Web-Roots.[20, 65] Verwenden Sie stattdessen Konfigurationsdateien außerhalb des öffentlich zugänglichen Bereichs oder Umgebungsvariablen.
Code-Struktur: Kapseln Sie Datenbankverbindungslogik und wiederkehrende Abfragen in Funktionen oder Klassen, um die Wartbarkeit und Wiederverwendbarkeit zu verbessern.[20]
Effizienz: Laden Sie große Ergebnismengen nicht komplett in den Speicher (fetchAll
/fetch_all
). Verwenden Sie stattdessen zeilenweise Verarbeitung (while
/fetch
) oder Iteratoren (PDO mit PHP 8+).[37, 40] Nutzen Sie persistente Verbindungen nur nach sorgfältiger Abwägung der Vor- und Nachteile.[7, 27]
Defense-in-Depth: Betrachten Sie Sicherheit als Gesamtkonzept. Die Kombination aus der richtigen API-Wahl, konsequenten Prepared Statements, Eingabevalidierung, minimalen Rechten, sicherer Fehlerbehandlung, sicherer Speicherung von Zugangsdaten und regelmäßigen Software-Updates bildet eine robuste Verteidigung.