Heute geht es darum, wie man auf möglichst einfache Weise eine Klasse in PHP realisiert, welche den Datensatz einer MySQL-Datenbanktabelle repräsentiert. Dazu soll ausschließlich auf die Verwendung von Standard-PHP-Mitteln zurückgegriffen werden, ohne dass irgendwelche Frameworks oder PlugIns benötigt werden. Zunächst sollte man sich überlegen, welche Operationen notwendig sind, um sinnvoll mit einem konkreten Objekt arbeiten zu können – prinzipiell sollen Daten gelesen und geschrieben werden.
Zunächst benötigt man einen Datenbank-Handler, welche für die Kommunikation mit der Datenbank verwendet wird. Anschließend kann man sich auch schon der Klasse widmen, die den Datensatz einer Tabelle repräsentieren soll. Da wir den Datenbankhandler innerhalb der Klasse verwenden wollen ist es sinnvoll diesen auch dort als Variable zu deklarieren. Weiterhin benötigen wir den Namen der Tabelle, auf die zugegriffen werden soll, den Namen des ID-Feldes, sowie ein Array, welches die Informationen aus der Datenbank hält. Außerdem soll die Klasse mit Auto_increment (die ID wird automatisch bei einem Insert erzeugt) und ohne Auto_increment arbeiten können. Natürlich soll auf die Tabellenspalten auf die übliche Weise ($obj->spaltenname) zugegriffen werden können, sodass die __get und die __set-Methoden überschrieben werden müssen. Bei der Speicherung eines Datensatz muss nun zwischen Insert und Update unterschieden werden, je nachdem, ob der Datensatz bereits in der Datenbank existiert oder nicht. Hierzu wird sowohl eine save()-Methode realisiert, welche die Unterscheidung automatisch vornimmt, also auch eine insert() und eine update()-Methode. Dadurch wird erreicht, dass Datensätze später sehr einfach kopiert werden können.
Eine Implementierung, welche all diese Anforderungen erfüllt könnte wie folgt aussehen:
$dbHandler = mysql_connect("server", "user", "pass"); mysql_select_db("datenbankname", $dbHandler); class Data { protected $dbHandler; protected $tableName; protected $idField; protected $autoIncrement; protected $data; /** * Konstruktor * * Setzt die Tabellenparameter, laedt die Feldnamen und prueft, * ob die Tabelle mit Auto-Increment arbeitet * * @param string $tableName Name der Tabelle * @param string $idField Name des ID-Feldes */ public function __construct($tableName, $idField = null) { global $dbHandler; $this->dbHandler = $dbHandler; $this->tableName = $tableName; $this->idField = $idField; $this->loadFieldsNames(); $this->setAutoIncrementValue(); } /** * Laedt die Spaltenname der Tabelle und erstellt die * entsprechenden Felder im $data-Array */ private function loadFieldsNames() { $query = "SHOW COLUMNS FROM ".$this->tableName; $res = mysql_query($query, $this->dbHandler) or die(mysql_error()); while ($row = mysql_fetch_assoc($res)) { $this->data[$row["Field"]] = null; } } /** * Prueft, ob die Tabelle mit Auto-Increment arbeitet oder nicht. */ private function setAutoIncrementValue() { $query = "SHOW TABLE STATUS LIKE '".$this->tableName."'"; $res = mysql_query($query) or die(mysql_error()); $row = mysql_fetch_assoc($res); if ( is_null($row['Auto_increment']) ) { $this->autoIncrement = false; } else { $this->autoIncrement = true; } } /** * Laedt den Datensatz mit dem angegebenen Identifier aus der Datenbank * * @param mixed $id Identifier der Tabelle * * @return bool */ public function load($id) { $this->$idField = $id; $query = "SELECT * FROM ".$this->tableName." WHERE ".$this->idField." = '".$id."'"; $res = mysql_query($query, $this->dbHandler) or die(mysql_error()); if ( mysql_num_rows($res) > 0 ) { $this->data = mysql_fetch_assoc($res); return true; } else { return false; } } /** * Ueberschreibt die __get-Methode und ermittelt den Wert des Datensatzes * * @param string $fieldName Name des Feldes * * @return mixed */ public function __get($fieldName) { return $this->data[$fieldName]; } /** * Ueberschreibt die __set-Methode und setzt den Wert im $data-Array * * @param string $fieldName Name des Feldes * @param mixed $value Wert des Feldes */ public function __set($fieldName, $value) { if ( array_key_exists($fieldName, $this->data) ) { $this->data[$fieldName] = $value; } } /** * Speichert den Datensatz in der Datenbank. Ist der Datensatz bereits * vorhanden wird ein Update durchgefuehrt, andernfalls ein Insert. * * @return bool */ public function save() { if ( is_null($this->data[$this->idField]) ) { return $this->insert(); } else { return $this->update(); } } /** * Fuegt den Datensatz in die Datenbank ein. * * @return bool */ public function insert() { if ( $this->autoIncrement ) { $values = array_diff_key($this->data, array($this->idField => null)); $query = "INSERT INTO ".$this->tableName." (".implode(",", array_keys($values)).") VALUES ('".implode("','", $values)."')"; } else { $this->data[$this->idField] = $this->getNextId(); $query = "INSERT INTO ".$this->tableName." (".implode(",", array_keys($this->data)).") VALUES ('".implode("','", $this->data)."')"; } if ( mysql_query($query, $this->dbHandler) ) { if ( $this->autoIncrement ) { $this->data[$this->idField] = mysql_insert_id(); } return true; } else { if ( !$this->autoIncrement ) { $this->data[$this->idField] = null; } return false; } } /** * Fuehrt ein Update auf den Datensatz durch. * * @return bool */ public function update() { $values = array_diff_key($this->data, array($this->idField => null)); foreach ($values as $key => $value) { $values[$key] = $key."='".$value."'"; } $query = "UPDATE ".$this->tableName. " SET ".implode(" AND ", $values)." WHERE ".$this->idField." = ".$this->data[$this->idField]; if ( mysql_query($query, $this->dbHandler) ) { return true; } else { return false; } } /** * Ermittelt die naechste ID fuer ein Insert. Wird nur dann verwendet, * wenn die Tabelle ohne Auto_increment arbeitet. * * @return int */ private function getNextID() { $query = "SELECT MAX(".$this->idField.") as max FROM ".$this->tableName; $res = mysql_query($query, $this->dbHandler); $row = mysql_fetch_assoc($res); return $row['max']+1; } }
Nun hat man mehrere Möglichkeiten mit dieser Klasse zu arbeiten. Man könnte die Klasse direkt benutzen und Objekte von ihr erzeugen. Die wesentlich elegantere Variante ist allerdings, dass man für jede Tabelle in der Datenbank eine eigene Klasse erstellt, welche dann von der vorgestellten Klasse Data ableitet. Dadurch ergiben sich mehrere Vorteile.
- Man kann problemlos Methoden überschreiben, falls diese nicht auf das zu lösende Problem passen. Andere Klassen bleiben dadurch unberührt.
- Sinnvolle Kapselung – Die Grundfunktionalitäten bleiben in der Data-Klasse, während spezielle Methoden in der abgeleiteten Klasse implementiert werden.
- Bessere Übersicht und intuitives Handling, da jede Datenbanktabelle auch als Klasse in der Anwendung existiert.
Angenommen in der Datenbank existiert eine Tabelle namens “testTabelle” und den Spalten “id” (ID-Feld) und “name”, dann könnte eine solche Klasse wie folgt aussehen:
class BeispielKlasse extends Data { function __construct($id = null) { parent::__construct("testtabelle", "id"); if (!is_null($this->id)) { $this->load($id); } } }
Ich finde diese Mapper immer sehr spannend. Produktiv würde ich momentan aber nichts selbstgebasteltes nutzen, sondern eher zu Doctrine (http://web.archive.org/web/20110521032959/http://www.doctrine-project.org/) tendieren. Das Ding erzeugt mir aus der DB die Klassen oder ich kann über eine Konfigurationsdatei Klassen und Datenbank erstellen.
Aber zu deinem Code. Ich sehe da ein Problem. Du machst bei jedem Objekt ein Select auf die Datenbank um die Felder zu bekommen. Das könnte ein Performanceproblem sein, wenn das Projekt groß ist und die DB nicht sonderlich schnell antwortet. Klassen mit vordefinierten Methoden haben den gleichen Effekt (nämlich kein nicht vorhandenes Feld kann gefüllt werden) aber ohne die DB zu belästigen.
Das mit dem Select ist natürlich absolut richtig. Hast du einen Ansatz, mit welchem man ohne dieses Statement auskommt ohne Einbußen (z.B. in der Dynamik) in Kauf nehmen zu müssen?
Wie du allerdings schon sagst, würde auch ich in großen Projekte eher auf bewährte Frameworks setzen, denn von der Performance mal abgesehen, sind hier auch keinerlei Sicherheitsmechanismen implementiert. Beispielsweise müssten man sich noch um SQL-Injections und Ähnliches kümmern.
Alles in Allem sollte dieser Beitrag allerdings auch nur auf die PHP-eigenen Bordmittel eingehen, und zeigen, wie einfach man ein solches Problem angehen kann.
Ohne Einbußen wird es natürlich schwierig. Aber so ein Objekt muss ja auch nicht super generisch sein. Da sich das das DB-Layout nicht sooft ändert, ist ein CodeGenerator sicher okay.
Und ja, der Artikel zeigt wie man Objekte mit einer DB verbindet 🙂