Datenbankintegration (Webdevelopment)

Aus FernFH MediaWiki
Zur Navigation springen Zur Suche springen

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 mittels try...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 eine PDOException, 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 mit mysqli_ (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 alten mysql-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]
  • Verbindungsaufbau mit MySQLi:
    • Prozedural: Die Funktion mysqli_connect() wird verwendet: $link = mysqli_connect($host, $user, $pass, $db, $port, $socket);. Sie gibt bei Erfolg ein mysqli-Objekt (oder false bei Fehlern vor PHP 8.1) zurück.[17, 18] Fehler können mit mysqli_connect_errno() und mysqli_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 mit try...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);
?&gt;
  • 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 mit mysql_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]
  • 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 die query()-Methode verwendet werden. Sie führt die SQL-Abfrage direkt aus und gibt ein PDOStatement-Objekt zurück, das zur Ergebnisauswertung dient.[2, 33]
// Beispiel: Alle Benutzer auswählen
$stmt = $pdo-&gt;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- oder LIMIT-Klauseln).
  • 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-&gt;prepare($sql);

// Beispiel: Benutzer mit bestimmtem Status auswählen (benannter Platzhalter)
$sql_named = "SELECT id, username FROM users WHERE status = :status";
$stmt_named = $pdo-&gt;prepare($sql_named);
    1. Ausführen (execute): Die vorbereitete Anweisung wird mit den tatsächlichen Werten ausgeführt. Die Werte werden als Array an die execute()-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 für positionalen Platzhalter
$user_id = 123;
$stmt-&gt;execute([$user_id]);

// Ausführen für benannten Platzhalter
$user_status = 'active';
$stmt_named-&gt;execute([':status' =&gt; $user_status]);
// oder: $stmt_named-&gt;execute(['status' =&gt; $user_status]);
    1. Alternative Bindung: Optional können Parameter auch explizit vor dem execute()-Aufruf mit bindValue() (bindet einen Wert) oder bindParam() (bindet eine Variable als Referenz) gebunden werden. Dies gibt mehr Kontrolle über den Datentyp (z.B. PDO::PARAM_INT, PDO::PARAM_STR).[5, 13]
$stmt-&gt;bindValue(1, $user_id, PDO::PARAM_INT); // Position 1, Wert von $user_id als Integer
$stmt-&gt;execute();

$stmt_named-&gt;bindParam(':status', $user_status, PDO::PARAM_STR); // Platzhalter :status, Variable $user_status als String
$stmt_named-&gt;execute();
    • MySQLi:
      • Ohne Parameter: Ähnlich wie bei PDO kann bei Abfragen ohne variable Teile die query()-Methode (OO-Stil: $mysqli->query()) oder die mysqli_query()-Funktion (prozeduraler Stil: mysqli_query($link, "...")) verwendet werden. Sie geben ein mysqli_result-Objekt zurück.[20, 25]
// OO-Stil
$result = $mysqli-&gt;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-&gt;prepare($sql);
// Prozedural
$sql_proc = "SELECT id, username, email FROM users WHERE id =?";
$stmt_proc = mysqli_prepare($link, $sql_proc);
      1. Parameter binden (bind_param): Die Variablen werden an die Platzhalter gebunden. Dies ist ein wesentlicher Unterschied zu PDOs execute-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]
$user_id = 123;
// OO-Stil
$stmt-&gt;bind_param("i", $user_id); // "i" für Integer
// Prozedural
mysqli_stmt_bind_param($stmt_proc, "i", $user_id);
      1. Ausführen (execute): Die vorbereitete Anweisung wird ausgeführt. OO-Stil: $stmt->execute(), prozedural: mysqli_stmt_execute($stmt).[25, 36, 35]
// OO-Stil
$stmt-&gt;execute();
// Prozedural
mysqli_stmt_execute($stmt_proc);
      1. 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 den mysqlnd-Treiber.[25, 15]
// OO-Stil
$result = $stmt-&gt;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):

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 des PDOStatement-Objekts wird wiederholt in einer while-Schleife aufgerufen. fetch() gibt bei jedem Aufruf die nächste Zeile zurück (im Format des eingestellten Fetch-Modus, z.B. PDO::FETCH_ASSOC) und false (oder null je nach Modus/Version), wenn keine weiteren Zeilen vorhanden sind.[2, 33]
$stmt = $pdo-&gt;prepare("SELECT name, email FROM users WHERE status =?");
$stmt-&gt;execute(['active']);
while ($row = $stmt-&gt;fetch(PDO::FETCH_ASSOC)) {
 echo "Name: ". htmlspecialchars($row['name']). ", E-Mail: ". htmlspecialchars($row['email']). "<br>";
}
        • MySQLi: Die fetch_assoc()-Methode (oder fetch_row(), fetch_object(), etc.) des mysqli_result-Objekts wird in einer while-Schleife verwendet. Sie gibt die nächste Zeile als Array (oder Objekt) zurück oder null, wenn das Ende erreicht ist.[25, 15]
// OO-Stil (nach $result = $stmt-&gt;get_result();)
while ($row = $result-&gt;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):

Für kleinere bis mittlere Ergebnismengen ist es oft bequemer, alle Zeilen auf einmal in ein PHP-Array zu laden.

        • PDO: Die fetchAll()-Methode des PDOStatement-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.
$stmt = $pdo-&gt;prepare("SELECT name, email FROM users WHERE status =?");
$stmt-&gt;execute(['active']);
$results = $stmt-&gt;fetchAll(PDO::FETCH_ASSOC);
// $results ist nun ein Array von assoziativen Arrays
        • MySQLi: Die fetch_all()-Methode des mysqli_result-Objekts (verfügbar mit mysqlnd) tut dasselbe. Der Modus (MYSQLI_ASSOC, MYSQLI_NUM, MYSQLI_BOTH) wird als Argument übergeben.[25, 39]
// OO-Stil (nach $result = $stmt-&gt;get_result();)
$results = $result-&gt;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 anonymes stdClass-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 aus FETCH_BOTH und FETCH_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 ein stdClass-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]

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
      • 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-&gt;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-&gt;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 oder DELETE) wird mit Platzhaltern (? oder :name) für die einzufügenden/zu aktualisierenden Werte sowie für die Kriterien in WHERE-Klauseln (bei UPDATE und DELETE) 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 durch UPDATE oder DELETE betroffenen Zeilen zurück. Bei INSERT 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 einer AUTO_INCREMENT-Spalte liefert $pdo->lastInsertId() die ID des neu eingefügten Datensatzes zurück.[8, 33, 48]
        • Code-Beispiele (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-&gt;execute([':name' =&gt; $name, ':email' =&gt; $email, ':status' =&gt; $status]);
$lastId = $pdo-&gt;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-&gt;prepare($sql_update);
$stmt_update-&gt;execute([':status' =&gt; $new_status, ':id' =&gt; $user_id_to_update]);
$affectedRows = $stmt_update-&gt;rowCount();
echo "$affectedRows Benutzerstatus aktualisiert.<br>";

// --- DELETE ---
$user_id_to_delete = $lastId;

$sql_delete = "DELETE FROM users WHERE id = :id";
$stmt_delete = $pdo-&gt;prepare($sql_delete);
$stmt_delete-&gt;execute([':id' =&gt; $user_id_to_delete]);
$affectedRowsDel = $stmt_delete-&gt;rowCount();
echo "$affectedRowsDel Benutzer gelöscht.<br>";
?&gt;
      • 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 PDOs execute().
        • 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 Funktionen mysqli_affected_rows($link) oder mysqli_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) oder mysqli_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]
        • Code-Beispiele (MySQLi OO):
<!--?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-&gt;bind_param("sss", $name, $email, $status);
$stmt_insert-&gt;execute();
$lastId = $mysqli-&gt;insert_id;
echo "Neuer Benutzer mit ID $lastId eingefügt (MySQLi OO).<br>";
$stmt_insert-&gt;close(); // Statement schließen

// --- UPDATE ---
$new_status = 'active';
$user_id_to_update = $lastId;

$sql_update = "UPDATE users SET status =? WHERE id =?";
$stmt_update = $mysqli-&gt;prepare($sql_update);
// Typen: s=string, i=integer
$stmt_update-&gt;bind_param("si", $new_status, $user_id_to_update);
$stmt_update-&gt;execute();
$affectedRows = $mysqli-&gt;affected_rows; // Oder $stmt_update-&gt;affected_rows vor close()
echo "$affectedRows Benutzerstatus aktualisiert (MySQLi OO).<br>";
$stmt_update-&gt;close();

// --- DELETE ---
$user_id_to_delete = $lastId;

$sql_delete = "DELETE FROM users WHERE id =?";
$stmt_delete = $mysqli-&gt;prepare($sql_delete);
// Typ: i=integer
$stmt_delete-&gt;bind_param("i", $user_id_to_delete);
$stmt_delete-&gt;execute();
$affectedRowsDel = $mysqli-&gt;affected_rows;
echo "$affectedRowsDel Benutzer gelöscht (MySQLi OO).<br>";
$stmt_delete-&gt;close();
?&gt;

(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:

      1. 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.
      2. 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]
      • Parameterbindung:
        • PDO: Bietet flexible Bindungsoptionen:
          • Implizit über execute()-Array: Die einfachste und häufigste Methode. Ein Array mit den Werten wird an execute() übergeben. PDO kümmert sich um das korrekte Escaping und die Typbehandlung (oft werden Werte sicher als Strings behandelt).[4, 12, 5, 13]
$stmt-&gt;execute([$wert1, $wert2]); // für? Platzhalter
$stmt-&gt;execute([':name' =&gt; $wert1, ':id' =&gt; $wert2]); // für :name Platzhalter
          • Explizit mit bindValue() oder bindParam(): 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 bei execute() gelesen), bindValue bindet den aktuellen Wert einer Variablen oder einen Literalwert.
$stmt-&gt;bindValue(':id', $user_id, PDO::PARAM_INT);
$stmt-&gt;bindParam(':name', $user_name, PDO::PARAM_STR);
$stmt-&gt;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]
// OO-Stil
$stmt-&gt;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 den root- 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 oder mysqli_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.

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:

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 =&gt; $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' =&gt; 1, 'name' =&gt; 'Alice', 'email' =&gt; '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():

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 nach fetchAll/fetch_all). Wenn Sie Ergebnisse zeilenweise mit fetch() in einer while-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 PDOStatement::rowCount() [8, 9] oder mysqli_result::$num_rows / mysqli_stmt::$num_rows [26, 15] verwendet werden, um die Anzahl der von einer SELECT-Abfrage zurückgegebenen Zeilen zu erhalten, bevor die Daten gefetched werden (das Verhalten von rowCount kann jedoch bei anderen Anweisungstypen wie UPDATE variieren).
        • Code-Beispiel:
<!--?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-&gt;query("SELECT id FROM users"); // Annahme: PDO-Statement
$manueller_zaehler = 0;
while ($row = $stmt-&gt;fetch()) {
 $manueller_zaehler++;
}
echo "Anzahl der Benutzer (via while/fetch): $manueller_zaehler<br>";

// Alternative: rowCount() nach SELECT (PDO)
$stmt_count = $pdo-&gt;query("SELECT id FROM users");
$rowCountResult = $stmt_count-&gt;rowCount(); // Kann bei manchen DBs/Treibern funktionieren
echo "Anzahl der Benutzer (via rowCount nach SELECT): $rowCountResult<br>";
?&gt;
      • (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.
// 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. mit array_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.