Referenzen:
Prepared Statements mit PDO (Teil 1 - Einführung)
Prepared Statements mit PDO (Teil 2 - Fehlerbehandlung)
Ein Basis-OR-Mapper für PDO
Vorwort
Nach der unbequemen Vorarbeit und der ganzen Basis-Theorie folgen die Aspekte, die wirklich Spaß machen und die Code-Qualität auch von PHP-Scripten enorm steigern.
Wir nehmen uns das Thema OOP noch näher zur Brust und schauen, wie wir Objekte auf Datenbank-Tabellen mappen können. Das bisher erworbene Wissen stellt die Basis dar. Wir bauen darauf auf und erstellen uns einen OR-Mapper allein mit den Möglichkeiten der PDO-Klasse und deren Kind PDOStatement.
Doch leider wieder etwas Theorie. Diejenigen, die schon wissen was ein OR-Mapper ist, können den folgenden Abschnitt überspringen.
Was ist ein OR-Mapper und wofür brauch ich ihn?
Wir stellen uns vor, wir haben ein Objekt $benutzer, dass aus einer Klasse User erzeugt wurde und diese Klasse bietet die Möglichkeit, das Objekt zu laden, zu speichern und das alles mit ganz wenig Code. Ein OR-Mapper (Object-Relational-Mapper) ist also ein "Monster", dass die Datenbank-Tabellenstruktur auf reguläre Klassen abbilden kann. Das bedeutet, wir können einfach Klassen schreiben, die genauso aussehen, wie unsere Tabellen-Struktur und quasi im Code so tun, als würden wir direkt in die Datenbank-Tabelle rein greifen. So fühlt es sich zumindest an. Da stellt sich dann natürlich die Frage, wofür man dann noch SQL braucht. Nun ganz ohne SQL geht es nicht, denn irgendwer muss ja die Daten laden und speichern oder löschen können. Außerdem gibt es noch die eine oder andere Gelegenheit, wo man optimieren muss oder mit den vom ORM angebotenen Methoden nicht ganz weiter kommt.
Aber schauen wir uns das doch erstmal Schritt für Schritt an.
Wir bauen unsere Modell-Klasse "Users"
Die Klasse hat die gleichen Felder, die auch in der zugehörigen Datenbank-Tabelle "users" zu finden sind und jeweils einen Getter und einen Setter für das entsprechende Feld. In groben Zügen sieht diese Klasse also so aus:
Ok, und wie wird da jetzt eine Verbindung zwischen dem Objekt User und der Tabelle 'users' hergestellt? So:
Verwendung von fetch(PDO::FETCH_CLASS)
Die Methode fetch() bietet eine weitere Möglichkeit, den Rückgabe-Wert zu beeinflussen. Mit Hilfe des Flags PDO::FETCH_CLASS kann man PDOStatement dazu veranlassen, ein Objekt zurück zu liefern, welches nicht von stdclass sondern der angebenen Klasse instanziiert wurde. Allerdings müssen wir auch das SELECT-Statement etwas anpassen, denn das liefert derzeit nur die id-Spalte zurück, wir benötigen aber alle Werte aus der Tabelle für eine sinnvolle Verwendung des Modells. Also haben wir insgesamt vier Änderungen vorzunehmen und lernen eine neue Methode von PDOStatement kennen:
1. Anpassung
2. Anpassung
3. Anpassung, diese sollte direkt nach die prepare()-Methode
4. Anpassung
Soweit so gut. Aber wie hat sich dieses PDO::FETCH_PROPS_LATE eingeschlichen und was macht es?
Bei der angeforderten Klasse handelt es sich um eine, welche nicht den leeren Standard-Konstruktor hat, sondern einen klassen-spezifischen mit Parameterliste, um das Objekt direkt beim erstellen mit Werten zu befüllen. Da PDO Beim erstellen des Objekts zuerst die Klassenfelder füllen will und dann erst den Konstruktor aufruft, werden natürlich die Werte direkt wieder überschrieben. Mit diesem Flag dreht man die Reihenfolge um, zuerst wird der Konstruktor ausgeführt und anschließend die Felder (Properties) gefüllt.
Getestet wird durch die Änderung der Ausgabe:
Zeit für Kaffee und Entkopplung
Es wurde erwähnt, dass ein OR-Mapper Datenbank-Einträge auf Klassen mappt. Aber was haben wir hier? Einen wilden Wust von Datenbank-Code, Modell (die Users-Klasse ist eines) und Geschäftslogik. Es wird Zeit, die Zuständigkeiten aufzuteilen. Dafür bauen wir jetzt ein bisschen was um. Zu allererst lassen wir die Users-Klasse von ORM ableiten, in dem das extends Schlüsselwort an die Klassen-Definition angehängt wird:
Wie man sieht, wurde ein Import der ORM.php vorgenommen. Diese Klasse ist sehr umfangreich und ich möchte ihr gern ein komplettes eigenes Kapitel widmen. Der Code für die Klasse kann in dem allgemeinen Beitrag Ein Basis-OR-Mapper für PDO gefunden werden.
Kopiert die Klasse einfach in eine neue PHP-Datei mit dem Namen ORM.php und speichert ab.
Jetzt können wir in unserer Test-Datei den Code sehr stark aufräumen:
Wie man sehen kann, sind hier nur noch die Imports für den Log-Handler, den Logger und unser Modell zu finden. Anschließend verwenden wir die statische Methode find() der Users-Klasse um einen spezifischen Datensatz zu suchen. Ist der Datensatz nicht vorhanden, wird eine ORMException geworfen, die wie gewohnt unten abgefangen wird. Wenn der Datensatz (der durch die Testdaten aus dem ersten Teil der PDO-Serie eingefügt wurde) vorhanden ist, bekommen wir ein gültiges Users-Objekt, auf dem wir die Getter aufrufen können. Der ORM-Mapper erledigt für uns das Füllen der Klassen-Eigenschaften und wir können uns auf auf das Implementieren der Modelle konzentrieren. Unser Beispiel-ORM-Mapper arbeitet nach dem Prinzip, dass er den Klassen-Namen als Tabellen-Name verwendet und damit die Datenbank nach den Kriterien selektiert, die als Argumente an die find()-Methode übergeben wurden.
Fazit
Die Aufteilung der Zuständigkeiten auf mehrere Klassen beschert uns sehr viel Übersichtlichkeit im Code. PDO greift uns dabei enorm unter die Arme. Wir können nun einfach ein weiteres Modell implementieren, dass als Eigenschaften wiederum Tabellen-Spalten benennt und von der Klasse ORM erbt. Dann können wir auf diesem Modell wieder find() aufrufen, um bestimmte Datensätze aus der Datenbank zu selektieren. find() liefert entweder einen einzelnen Datensatz (vom Typ des Modells) oder eine Liste von Datensätzen (Array aus Objekten vom Typ des Modells) zurück, die wir in unserer Geschäftslogik verwenden können.
Im nächsten Kapitel werden wir auf die Details der OR-Mapper-Implementierung eingehen und uns einmal ansehen, wie wir Datensätze speichern oder löschen können. Außerdem machen wir einen kurzen Abstecher ins Thema Security im Hinblick auf das Abspeichern von Passwörtern.
Prepared Statements mit PDO (Teil 1 - Einführung)
Prepared Statements mit PDO (Teil 2 - Fehlerbehandlung)
Ein Basis-OR-Mapper für PDO
Vorwort
Nach der unbequemen Vorarbeit und der ganzen Basis-Theorie folgen die Aspekte, die wirklich Spaß machen und die Code-Qualität auch von PHP-Scripten enorm steigern.
Wir nehmen uns das Thema OOP noch näher zur Brust und schauen, wie wir Objekte auf Datenbank-Tabellen mappen können. Das bisher erworbene Wissen stellt die Basis dar. Wir bauen darauf auf und erstellen uns einen OR-Mapper allein mit den Möglichkeiten der PDO-Klasse und deren Kind PDOStatement.
Doch leider wieder etwas Theorie. Diejenigen, die schon wissen was ein OR-Mapper ist, können den folgenden Abschnitt überspringen.
Was ist ein OR-Mapper und wofür brauch ich ihn?
Wir stellen uns vor, wir haben ein Objekt $benutzer, dass aus einer Klasse User erzeugt wurde und diese Klasse bietet die Möglichkeit, das Objekt zu laden, zu speichern und das alles mit ganz wenig Code. Ein OR-Mapper (Object-Relational-Mapper) ist also ein "Monster", dass die Datenbank-Tabellenstruktur auf reguläre Klassen abbilden kann. Das bedeutet, wir können einfach Klassen schreiben, die genauso aussehen, wie unsere Tabellen-Struktur und quasi im Code so tun, als würden wir direkt in die Datenbank-Tabelle rein greifen. So fühlt es sich zumindest an. Da stellt sich dann natürlich die Frage, wofür man dann noch SQL braucht. Nun ganz ohne SQL geht es nicht, denn irgendwer muss ja die Daten laden und speichern oder löschen können. Außerdem gibt es noch die eine oder andere Gelegenheit, wo man optimieren muss oder mit den vom ORM angebotenen Methoden nicht ganz weiter kommt.
Aber schauen wir uns das doch erstmal Schritt für Schritt an.
Wir bauen unsere Modell-Klasse "Users"
Die Klasse hat die gleichen Felder, die auch in der zugehörigen Datenbank-Tabelle "users" zu finden sind und jeweils einen Getter und einen Setter für das entsprechende Feld. In groben Zügen sieht diese Klasse also so aus:
PHP:
<?php
/**
* Users.php
* This class maps the database table 'users' into a PHP Object
*/
class Users
{
/**
* Users id
* @var number
*/
private $id;
/**
* Users name
* @var string
*/
private $username;
/**
* Users password
* @var string
*/
private $passwd;
/**
* New instance of user
*
* @param number $id
* @param string $username
* @param string $passwd
*/
public function __construct($id = 0, $username = '', $passwd = '')
{
$this->id = $id;
$this->passwd = $passwd;
$this->username = $username;
}
/**
* Getter for field id
* @return number
*/
public function getId()
{
return $this->id;
}
/**
* Setter for field id
* @param number $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* Getter for field username
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Setter for field username
* @param string $username
*/
public function setUsername($username)
{
$this->username = $username;
}
/**
* Getter for field passwd
* @return string
*/
public function getPasswd()
{
return $this->passwd;
}
/**
* Setter for field passwd
* @param string $passwd
*/
public function setPasswd($passwd)
{
$this->passwd = $passwd;
}
}
Ok, und wie wird da jetzt eine Verbindung zwischen dem Objekt User und der Tabelle 'users' hergestellt? So:
Verwendung von fetch(PDO::FETCH_CLASS)
Die Methode fetch() bietet eine weitere Möglichkeit, den Rückgabe-Wert zu beeinflussen. Mit Hilfe des Flags PDO::FETCH_CLASS kann man PDOStatement dazu veranlassen, ein Objekt zurück zu liefern, welches nicht von stdclass sondern der angebenen Klasse instanziiert wurde. Allerdings müssen wir auch das SELECT-Statement etwas anpassen, denn das liefert derzeit nur die id-Spalte zurück, wir benötigen aber alle Werte aus der Tabelle für eine sinnvolle Verwendung des Modells. Also haben wir insgesamt vier Änderungen vorzunehmen und lernen eine neue Methode von PDOStatement kennen:
1. Anpassung
PHP:
// Benötigte externe Scripte
require 'ErrorHandler.php';
require 'SimpleLogger.php';
// Für das Mapping der Tabelle auf eine PHP-Klasse
require 'Users.php';
2. Anpassung
PHP:
// Parameterlose Anfrage vorbereiten, diesmal aber mit der Selektion aller Spalten statt nur id
$statement = $db->prepare('SELECT * FROM users WHERE username = :u_name AND passwd = :u_pass');
3. Anpassung, diese sollte direkt nach die prepare()-Methode
PHP:
// Rückgabe wird als User-Objekt angefordert
$statement->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'Users');
4. Anpassung
PHP:
// Abholen der Daten als Objekt vom Typ "Users"
$result = $statement->fetch(PDO::FETCH_CLASS);
Soweit so gut. Aber wie hat sich dieses PDO::FETCH_PROPS_LATE eingeschlichen und was macht es?
Bei der angeforderten Klasse handelt es sich um eine, welche nicht den leeren Standard-Konstruktor hat, sondern einen klassen-spezifischen mit Parameterliste, um das Objekt direkt beim erstellen mit Werten zu befüllen. Da PDO Beim erstellen des Objekts zuerst die Klassenfelder füllen will und dann erst den Konstruktor aufruft, werden natürlich die Werte direkt wieder überschrieben. Mit diesem Flag dreht man die Reihenfolge um, zuerst wird der Konstruktor ausgeführt und anschließend die Felder (Properties) gefüllt.
Getestet wird durch die Änderung der Ausgabe:
PHP:
// Anzeige der abgeholten Daten
printf("Ein Benutzer mit der ID %d wurde gefunden, der den Namen %s hat", $result->getId(), $result->getUsername());
Zeit für Kaffee und Entkopplung
Es wurde erwähnt, dass ein OR-Mapper Datenbank-Einträge auf Klassen mappt. Aber was haben wir hier? Einen wilden Wust von Datenbank-Code, Modell (die Users-Klasse ist eines) und Geschäftslogik. Es wird Zeit, die Zuständigkeiten aufzuteilen. Dafür bauen wir jetzt ein bisschen was um. Zu allererst lassen wir die Users-Klasse von ORM ableiten, in dem das extends Schlüsselwort an die Klassen-Definition angehängt wird:
PHP:
<?php
require 'ORM.php';
class Users extends ORM
{
/**
* Users id
* @var number
*/
private $id;
Wie man sieht, wurde ein Import der ORM.php vorgenommen. Diese Klasse ist sehr umfangreich und ich möchte ihr gern ein komplettes eigenes Kapitel widmen. Der Code für die Klasse kann in dem allgemeinen Beitrag Ein Basis-OR-Mapper für PDO gefunden werden.
Kopiert die Klasse einfach in eine neue PHP-Datei mit dem Namen ORM.php und speichert ab.
Jetzt können wir in unserer Test-Datei den Code sehr stark aufräumen:
PHP:
<?php
// Benötigte externe Scripte
require 'ErrorHandler.php';
require 'SimpleLogger.php';
require 'Users.php';
$appUser = 'Alvin';
$appPass = 'chipmunk2014';
try
{
$user = Users::find(array('username' => $appUser, 'passwd' => $appPass));
// Anzeige der abgeholten Daten
printf("Ein Benutzer mit der ID %d wurde gefunden, der den Namen %s hat\n", $user->getId(), $user->getUsername());
}
catch (Exception $ex)
{
SimpleLogger::logException($ex);
die("Beim Datenbankzugriff ist ein Fehler aufgetreten!");
}
Wie man sehen kann, sind hier nur noch die Imports für den Log-Handler, den Logger und unser Modell zu finden. Anschließend verwenden wir die statische Methode find() der Users-Klasse um einen spezifischen Datensatz zu suchen. Ist der Datensatz nicht vorhanden, wird eine ORMException geworfen, die wie gewohnt unten abgefangen wird. Wenn der Datensatz (der durch die Testdaten aus dem ersten Teil der PDO-Serie eingefügt wurde) vorhanden ist, bekommen wir ein gültiges Users-Objekt, auf dem wir die Getter aufrufen können. Der ORM-Mapper erledigt für uns das Füllen der Klassen-Eigenschaften und wir können uns auf auf das Implementieren der Modelle konzentrieren. Unser Beispiel-ORM-Mapper arbeitet nach dem Prinzip, dass er den Klassen-Namen als Tabellen-Name verwendet und damit die Datenbank nach den Kriterien selektiert, die als Argumente an die find()-Methode übergeben wurden.
Fazit
Die Aufteilung der Zuständigkeiten auf mehrere Klassen beschert uns sehr viel Übersichtlichkeit im Code. PDO greift uns dabei enorm unter die Arme. Wir können nun einfach ein weiteres Modell implementieren, dass als Eigenschaften wiederum Tabellen-Spalten benennt und von der Klasse ORM erbt. Dann können wir auf diesem Modell wieder find() aufrufen, um bestimmte Datensätze aus der Datenbank zu selektieren. find() liefert entweder einen einzelnen Datensatz (vom Typ des Modells) oder eine Liste von Datensätzen (Array aus Objekten vom Typ des Modells) zurück, die wir in unserer Geschäftslogik verwenden können.
Im nächsten Kapitel werden wir auf die Details der OR-Mapper-Implementierung eingehen und uns einmal ansehen, wie wir Datensätze speichern oder löschen können. Außerdem machen wir einen kurzen Abstecher ins Thema Security im Hinblick auf das Abspeichern von Passwörtern.