Wie die PHP Data Objects (PDO) die Datenbankprogrammierung mit PHP verändern werden
PHP 5.1, die erste Version von PHP mit neuen Leistungsmerkmalen seit der Veröffentlichung von PHP 5.0 im Juli 2004, bringt neben einer verbesserten virtuellen Maschine (VM) unter anderem auch eine Erweiterung mit sich, die die Programmierung von Datenbankanwendungen mit PHP verändern wird. Dieser Artikel stellt diese Erweiterung vor und gibt eine Einführung in die Programmierung mit den PHP Data Objects.
Soll eine PHP-Anwendung mit unterschiedlichen Datenbankmanagementsystemen verwendet werden können, so wird eine Abstraktion von der Datenbank benötigt. Diese bietet eine einheitliche Programmierschnittstelle für die Arbeit mit den spezifischen Programmierschnittstellen der unterschiedlichen Systeme (beispielsweise den MySQL- und PostgreSQL-Erweiterungen von PHP). Über diese Abstraktion der Programmierschnittstellen hinaus können aber auch Unterschiede in der Implementierung des SQL-Standards durch die Datenbankmanagementsysteme berücksichtigt und vor dem Programmierer verborgen werden. Der bislang vorherrschende Lösungsansatz für das Problem der Datenbankabstraktion bei der PHP-Programmierung ist die Implementierung einer Datenbankabstraktionsschicht in PHP.
In den Zeiten von PHP 3 waren die Datenbankprogrammierschnittstellen der PHPLIB der De-facto-Standard für die datenbankunabhängige PHP-Programmierung. Mit PHP 4 kam eine wahre Flut von verschiedenen Bibliotheken und Lösungen für die Datenbankabstraktion heraus. Die populärsten Vertreter dürften die PEAR-Pakete DB und MDB sein. Während das PEAR-Paket DB nur die unterschiedliche Verwendung der PHP-Erweiterungen für die einzelnen Datenbankmanagementsysteme sowie einfache Unterschiede in der Implementierung des SQL-Standards wie die Limitierung der Anzahl von Ergebniszeilen oder die Arbeit mit Sequenzen wegabstrahiert, bietet das PEAR-Paket MDB, das eine Zusammenführung des PEAR-Pakets DB und dem Metabase-Paket darstellt, unter anderem auch eine Abstraktion der Datentypen an, was eine höhere Portabilität der PHP-Anwendung ermöglicht.
Die Veröffentlichung von PHP 5 hat einen Schwung neuer Portierungen von Lösungen und Werkzeugen aus der Java-Welt nach PHP hervorgebracht. Eine dieser Lösungen ist Creole, eine Portierung der Java-Database-Connectivity-(JDBC-)Programmierschnittstellen nach PHP 5. Diese bietet eine komfortable, objektorientierte Abstraktion der PHP-Erweiterungen für die einzelnen Datenbankmanagementsysteme. In diesem Zusammenhang ist Propel, eine auf Creole aufbauende Lösung für das objektrelationale Mapping, also das Speichern von Objekten in einer relationalen Datenbank, zu erwähnen. Mit Propel ist es möglich, PHP-Applikationen zu entwickeln, die vollständig unabhängig von dem eingesetzten Datenbankmanagementsystem sind, da der Entwickler keine SQL-Anweisungen mehr formulieren muss. Diese werden vielmehr automatisch für das verwendete Datenbankmanagementsystem erzeugt.
Tue in C, was Du in C tun kannst
Ein entscheidender Nachteil der genannten Lösungen ist die Tatsache, dass diese in PHP und nicht in C oder C++ als Bestandteil oder Erweiterung von PHP implementiert sind. Dies bedeutet, dass zusätzlich zum Quelltext der eigentlichen Applikation auch der - meist sehr umfangreiche Quelltext - der verwendeten Datenbankabstraktionsschicht übersetzt und ausgeführt werden muss. Zwar kann zumindest der Aufwand für die Übersetzung der Quelltexte durch den Einsatz einen PHP-Bytecode-Caches abgefangen werden, jedoch kann ein solcher Ansatz nicht an die Performanz einer in C geschriebenen Lösung heranreichen.
Die PHP Data Objects (PDO) stehen in Form einer in C geschriebenen Erweiterung für PHP zur Verfügung. Hieraus folgt automatisch die gute Performanz der von PDO bereitgestellten Datenzugriffsabstraktion. Hinter diesem Begriff verbirgt sich nichts anderes als eine einheitliche Programmiererschnittstelle für alle von PHP unterstützten Datenbankmanagementsysteme. Dieses einheitliche Datenbank-API erleichtert PHP-Programmierern nicht nur die Arbeit mit unterschiedlichen Datenbankmanagementsysteme, sondern erspart den Entwicklern von PHP in der Zukunft einiges an Entwicklungsarbeit: Gemeinsamer C-Code, der allen Datenbanktreibern gemein ist, muss nicht mehr dupliziert werden, sondern kann an zentraler Stelle gewartet und weiterentwickelt werden.
PDO besteht aus mehreren PHP-Erweiterungen, der eigentlichen PDO-Erweiterung sowie je einer datenbankspezifischen Erweiterung für jedes von PDO unterstützte Datenbankmanagementsystem. Beginnend mit der Version 5.1 ist PDO Bestandteil der Standarddistribution von PHP. Mit
./configure --enable-pdo --with-pdo-sqlite wird beispielsweise PDO mit dem entsprechenden Treiber für SQLite in PHP 5.1 aktiviert. Für PHP 5.0 kann PDO über PECL bezogen und mit dem PEAR-Installer installiert werden (Listing 1).
Listing 1 ./configure --prefix=/usr/local/php-5.0 --with-zlib
make && make install
pear upgrade-all
pear install PDO
<i>extension=pdo.so in der php.ini eintragen</i>
pear install PDO_sqlite
<i>extension=pdo_sqlite in der php.ini eintragen</i>
Derzeit von PDO unterstützte Datenbankmanagementsysteme
- MySQL
- PostgreSQL
- SQLite
- Firebird
- Microsoft SQL Server
- Oracle OCI
- ODBC Version 3, IBM DB2
Listing 2 zeigt, wie die Verbindung zu einer Datenbank mit PDO erfolgt. Hierzu wird ein neues Objekt der Klasse PDO erzeugt, deren Konstruktor vier Parameter akzeptiert. In unserem Beispiel, bei dem wir eine SQLite-Datenbankdatei verwenden wollen, benötigen wir nur den ersten Parameter, der einen String im DSN-Format erwartet. Die zweiten und dritten Parameter nehmen Benutzername und Passwort auf, der vierte Parameter ist für optionale Konfigurationsparameter vorgesehen. Für die Behandlung von Fehlern wird die Ausnahmenbehandlung PHP 5 verwendet: Tritt bei der Verwendung von PDO ein Fehler auf, so wird eine Ausnahme vom Typ PDOException ausgelöst.
Listing 2 <?php
try {
$db = new PDO('sqlite:/home/sb/test.db');
}
catch (PDOException $e) {
echo "Fehler beim Öffnen der Datenbank: " . $e->getMessage();
}
// Code, der auf die Datenbank zugreift.
// Datenbankverbindung freigeben.
$db = NULL;
?>
Data-Source-Name-(DSN-)Format von PDO
- odbc:odbc_dsn
- mysql:host=name;dbname=dbname
- sqlite:/path/to/db/file
- sqlite::memory:
- sqlite2:/path/to/sqlite2/file
- pgsql:native_pgsql_connection_string
- oci:dbname=dbname;charset=charset
- firebird:dbname=dbname;charset=charset;role=role

Abb. 1: Die Klassen PDO und PDOStatement
Der Destruktor der PDO-Klasse kümmert sich automatisch um die Freigabe aller mit der Datenbankverbindung assoziierten Ressourcen. Da der Zeitpunkt des Destruktoraufrufs nicht immer vorhersehbar ist, bietet sich die explizite Freigabe an. Dies geschieht durch Setzen der Variable, die die Referenz auf das PDO-Objekt entält, auf NULL.
Listing 3 zeigt die Ausführung einer schreibenden SQL-Anfrage unter Verwendung der Methode
PDO::exec(). Diese liefert als Rückgabewert die Anzahl der geänderten Zeilen in der Tabelle.
Listing 3 <?php
try {
$db = new PDO('sqlite:/home/sb/test.db');
}
catch (PDOException $e) {
print "Fehler beim Öffnen der Datenbank: " . $e->getMessage();
}
try {
$count = $db->exec("DELETE FROM tabelle WHERE spalte = 'wert'");
}
catch (PDOException $e) {
print "Fehler beim Ausführen der SQL-Anfrage.";
}
print "$count Zeilen wurden gelöscht.\n";
// Datenbankverbindung freigeben.
$db = NULL;
?>
Listing 4 zeigt das Vorbereiten und Ausführen einer lesenden SQL-Anfrage sowie die Verarbeitung der Ergebniszeilen.
Listing 4 <?php
try {
$db = new PDO('sqlite:/home/sb/test.db');
}
catch (PDOException $e) {
print "Fehler beim Öffnen der Datenbank: " . $e->getMessage();
}
try {
// SQL-Anfrage vorbereiten.
$statement = $db->prepare("SELECT spalte FROM tabelle");
// SQL-Anfrage ausführen.
$statement ->execute();
// Ergebniszeilen verarbeiten.
while ($row = $statement->fetch()) {
print_r($row);
}
// Ergebniszeilen freigeben.
$statement = NULL;
}
catch (PDOException $e) {
print "Fehler beim Ausführen der SQL-Anfrage.";
}
// Datenbankverbindung freigeben.
$db = NULL;
?>
Im Standardfall verwendet PDO so genannte Forward-Only-Cursor, die in ihrem Verhalten mit den ungepufferten Abfragen der MySQL-Erweiterung (mysql_unbuffered_query) vergleichbar sind. Hierbei werden die Ergebniszeilen nach der Ausführung der Anfrage nicht direkt und vollständig von der Datenbank in den PHP-Interpreter eingelesen. Vielmehr zeigt ein Cursor auf die erste Ergebniszeile. Mithilfe der Methode
fetch() wird dann jeweils eine Ergebniszeile gelesen sowie der Cursor auf die nächste Ergebniszeile gesetzt. Es kann stets nur auf die Zeile zugegriffen werden, auf die der Cursor zeigt. Dieser kann nur vorwärts bewegt werden und da der PHP-Interpreter keine Information darüber hat, wie viele Ergebniszeilen vorhanden sind, liefert die Methode
rowCount() kein relevantes Ergebnis.
Alternativ kann, wie in Listing 5 gezeigt, eine gepufferte Verarbeitung der Ergebniszeilen erreicht werden, indem man die Methode
fetchAll() aufruft und so alle Ergebniszeilen in ein Array einliest. Dies ist allerdings langsamer und speicherintensiver als die in Listing 4 gezeigte Verarbeitung der Ergebniszeilen.
Listing 5 <?php
try {
$db = new PDO('sqlite:/home/sb/test.db');
}
catch (PDOException $e) {
print "Fehler beim Öffnen der Datenbank: " . $e->getMessage();
}
try {
// SQL-Anfrage ausführen.
$statement = $db->query("SELECT spalte FROM tabelle");
// Ergebniszeilen holen.
$rows = $statement ->fetchAll();
// Ergebniszeilen verarbeiten.
foreach ($rows as $row) {
print_r($row);
}
// Ergebniszeilen freigeben.
$statement = NULL;
}
catch (PDOException $e) {
print "Fehler beim Ausführen der SQL-Anfrage.";
}
// Datenbankverbindung freigeben.
$db = NULL;
?>
PDO unterstützt das Binden von PHP-Variablen an SQL-Platzhalter ebenso wie das Binden von Ergebnisspalten an PHP-Variablen für alle unterstützten Datenbankmanagementsysteme. In PHP 5.0 bzw. ohne PDO ist dies in vergleichbarer Form nur mit der MySQLi-Erweiterung möglich.
Listing 6 demonstriert das Binden von PHP-Variablen an SQL-Platzhalter. Mithilfe der Methode
PDO::prepare() erzeugen wir zunächst für die SQL-Anfrage ein neues Objekt der Klasse PDOStatement. Danach benutzen wir die Methode
PDOStatement::bindParam(), um die PHP-Variablen $foo und $bar an die in der SQL-Anfrage verwendeten Platzhalter
:foo und
:bar zu binden. Hierbei ist auch der entsprechende SQL-Datentyp anzugeben. Die in PDO zur Verfügung stehenden SQL-Datentypen sind in einem Kasten aufgeführt. Bei der Ausführung der so vorbereiteten SQL-Anfrage mit der Methode
PDOStatement::execute() werden nun die Platzhalter durch den Inhalt der an sie gebundenen PHP-Variablen ersetzt.
Listing 6 <?php
// Zwei PHP-Variablen.
$foo = 23;
$bar = 42;
// Datenbank öffnen.
try {
$db = new PDO('sqlite:/home/sb/test.db');
}
catch (PDOException $e) {
echo "Fehler beim Öffnen der Datenbank: " . $e->getMessage();
}
try {
// SQL-Anfrage vorbereiten.
$statement = $db->prepare(
'SELECT foo, bar
FROM tabelle
WHERE foo < :foo AND bar = :bar'
);
// SQL-Platzhalter an PHP-Variablen binden.
$statement->bindParam(':foo', $foo, PDO_PARAM_INT);
$statement->bindParam(':bar', $bar, PDO_PARAM_INT);
// Vorbereitete SQL-Anfrage ausführen:
//
// SELECT foo, bar
// FROM tabelle
// WHERE foo < 23 AND bar = 42
$statement->execute();
}
catch (PDOException $e) {
echo "Fehler beim Ausführen der SQL-Anfrage: " . $e->getMessage();
}
// Datenbankverbindung freigeben.
$db = NULL;
?>
Die von PDO unterstützten SQL-Datentypen
- PDO_PARAM_NULL repräsentiert den SQL-Datentyp NULL.
- PDO_PARAM_INT repräsentiert den SQL-Datentyp Integer.
- PDO_PARAM_STR repräsentiert die SQL-Datentypen CHAR und VARCHAR.
- PDO_PARAM_LOB repräsentiert den SQL-Datentyp LARGE OBJECT.
In Listing 7 binden wir die Spalten der Ergebniszeilen einer SQL-Anfrage an PHP-Variablen. Zunächst benutzen wir wiederum die Methode
PDO::prepare(), um ein neues Objekt der Klasse PDOStatement für die SQL-Anfrage zu erzeugen. Diese vorbereitete Anfrage führen wir mit
PDOStatement::execute() aus und binden die Spalten der Ergebniszeilen an die PHP-Variablen $foo und $bar. Wenn wir nun in der while()-Schleife die Methode
PDOStatement::fetch() mit dem Parameter
PDO_FETCH_BOUND aufrufen, so werden die Spalten der jeweils aktuellen Ergebniszeile in die zuvor angegeben PHP-Variablen gelesen.
Listing 7 <?php
// Datenbank öffnen.
try {
$db = new PDO('sqlite:/home/sb/test.db');
}
catch (PDOException $e) {
echo "Fehler beim Öffnen der Datenbank: " . $e->getMessage();
}
try {
// SQL-Anfrage vorbereiten.
$statement = $db->prepare('SELECT foo, bar FROM tabelle');
// Vorbereitete SQL-Anfrage ausführen.
$statement->execute();
// Erste Ergebnisspalte an PHP-Variable $foo vom Typ Integer binden.
$statement->bindColumn(1, $foo, PDO_PARAM_INT);
// Zweite Ergebnisspalte an PHP-Variable $bar vom Typ Integer binden.
$statement->bindColumn(2, $bar, PDO_PARAM_INT);
// Ergebniszeilen verarbeiten.
while ($row = $statement->fetch(PDO_FETCH_BOUND)) {
printf(
"foo: %s, bar: %s\n",
$foo,
$bar
);
}
}
catch (PDOException $e) {
echo "Fehler beim Ausführen der SQL-Anfrage: " . $e->getMessage();
}
// Datenbankverbindung freigeben.
$db = NULL;
?>
Von PDOStatement::fetch() unterstützte Modi
- PDO_FETCH_ASSOC liefert eine Ergebniszeile als assoziatives Array, in dem die Spaltennamen die Schlüssel darstellen.
- PDO_FETCH_NUM liefert eine Ergebniszeile als numerisch indiziertes Array.
- PDO_FETCH_BOTH liefert eine Ergebniszeile als Arrays, das sowohl assoziativ als auch numerisch indiziert ist.
- PDO_FETCH_BOUND liest die Spalteninhalte einer Ergebniszeile in die PHP-Variablen, die zuvor an die entsprechenden Ergebnisspalten gebunden wurden.
- PDO_FETCH_CLASS liest die Spalteninhalte einer Ergebniszeile in die Instanzvariablen eines neuen Objekts einer Klasse, deren Name angegeben wird.
- PDO_FETCH_INTO liest die Spalteninhalte einer Ergebniszeile in die Instanzvariablen eines bereits existierenden Objekts einer Klasse, deren Name angegeben wird.
- PDO_FETCH_OBJ liest die Spalteninhalte einer Ergebniszeile in die Instanzvariablen eines Objektes.
- PDO_FETCH_LAZY stellt die Spalteninhalte einer Ergebniszeile in den Instanzvariablen eines Objekts zur Verfügung. Die Spalteninhalte werden jedoch erst dann von der Datenbank gelesen, wenn zum ersten Mal auf die entsprechende Instanzvariable zugegriffen wird.
- PDO_FETCH_COLUMN liest nur eine bestimmte Spalte einer Ergebniszeile.
PDO unterstützt ebenfalls die Verwendung von Transaktionen bei der Ausführung von Datenbankabfragen. Eine Transaktion ist eine Folge von (den Datenbestand ändernden) SQL-Anfragen, die nur gemeinsam oder gar nicht durchgeführt werden dürfen und so immer einen konsistenten Zustand des Datenbestands garantieren. Die Ausführung der SQL-Anfragen einer Transaktion kann jederzeit durch einen Rollback abgebrochen, der Datenbestand auf den Zustand vor Beginn der Transaktion zurückgesetzt werden. Erst wenn die Transaktion mit einem Commit beendet wird, werden sämtliche Änderungen übernommen.
Listing 8 zeigt die Verwendung von Transaktionen mit PDO. Mit der Methode
beginTransaction() wird die Transaktion eingeleitet. Nach der Ausführung der SQL-Anfragen der Transaktion wird diese entweder mit der Methode
commit() beendet oder mit der Methode
rollBack() abgebrochen.
Listing 8 <?php
// Datenbank öffnen.
try {
$db = new PDO('sqlite:/home/sb/test.db');
}
catch (PDOException $e) {
echo "Fehler beim Öffnen der Datenbank: " . $e->getMessage();
}
// AUTOCOMMIT deaktivieren, Transaktion beginnen.
$db->beginTransaction();
// Tabelle "tabelle" löschen.
$db->execute("DROP TABLE tabelle");
// Löschen der Tabelle rückgängig machen.
$db->rollBack();
// Datenbankverbindung ist nun wieder im AUTOCOMMIT-Modus.
// Datenbankverbindung freigeben.
$db = NULL;
?>
Fazit
PHP 5.1 bringt mit den PHP Data Objects (PDO) endlich eine einheitliche Programmierschnittstelle für die von PHP unterstützten Datenbankmanagementsysteme. Diese macht die Entwicklung von Abstraktionsschichten in PHP, die nur die Unterschiede zwischen den Programmierschnittstellen verbergen, überflüssig und erleichtert die Entwicklung von richtigen Datenbankabstraktionslösungen wie Creole.
Da Objekte der Klasse
PDOStatement die Iterator-Schnittstelle von PHP 5 anbieten, können die Ergebniszeilen einer Datenbankabfrage mit dem
foreach()-Operator bzw. von Methoden, die Parameter vom Typ Iterator erwarten, verarbeitet werden. In diesem Zusammenhang macht auch die Bezeichnung
Datenzugriffsabstraktion nochmals Sinn. Da in PHP 5 beispielsweise auch die Elemente eines XML-Dokumentes oder die Zeilen einer Textdatei durch ein Objekt, das die Iterator-Schnittstelle anbietet, repräsentiert werden können, werden Daten aus Datenbank, Dateisystem und XML-Dokument einheitlich verwendet, ohne sich um die konkrete Implementierung des Datenzugriffes kümmern zu müssen.
Sebastian Bergmann, Jahrgang 1978, lebt in Siegburg und studiert Informatik an der Rheinischen Friedrich-Wilhelms-Universität in Bonn. In seiner Freizeit arbeitet er an freier Software (unter anderem an PHP und Gentoo Linux). Er ist Autor bekannter PHP-Anwendungen wie PHPUnit oder phpOpenTracker. Er ist der Autor der Bücher Professionelle Softwareentwicklung mit PHP 5 und PHPUnit Pocket Guide, veröffentlicht Artikel in Fachzeitschriften und hält Vorträge zu PHP und verwandten Themen. Links