Model (M)
Nahezu jede Anwendung benötigt irgendeine Form der Datenhaltung. Die Idee hinter einer Abstrahierung durch ein Model ist, dass die Art und Weise dieser Datenhaltung (z. B. in einer Datenbank, Datei, Memcache, …) den Programmierer der Controller nicht kümmern muss.
Für unser Beispiel verwenden wir eine MySQL-Datenbank, die wir über PDO ansprechen. Da alle Models die selbe Basisfunktionalität haben werden, erstellen wir eine abstrakte Basisklasse.
Abstrakte Basisklasse für alle Models
Grundsätzlich gibt es zwei Wege ein ORM umzusetzen: Active Record und Data Mapper. Deren Unterschiede sind nicht Teil dieses Artikels, ich will nur erwähnen, dass wir hier unsere Models als Active Record implementieren.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
namespace Mvc\Model; abstract class ModelBase { private static $pdo; public function getPdo() { if (self::$pdo === null) { self::$pdo = new \PDO('mysql:host=localhost;dbname=mvc-example', 'root', '123'); } return self::$pdo; } public static function findFirst($options) { $model = new static(); $table = $model->getSource(); /** @var \PDO $pdo */ $pdo = $model->getPdo(); if (is_int($options)) { // we are looking for an ID $stmt = $pdo->prepare('SELECT * FROM `'.$table.'` WHERE id = ? LIMIT 1'); $stmt->execute([$options]); } elseif (is_array($options) && isset($options['criteria'])) { $stmt = $pdo->prepare('SELECT * FROM `'.$table.'` WHERE '.$options['criteria'].' LIMIT 1'); $stmt->execute($options['bind']); } else { throw new \UnexpectedValueException('you need to specify the criteria'); } return $stmt->fetchObject(get_class($model)); } public static function find(array $options) { $model = new static(); $table = $model->getSource(); /** @var \PDO $pdo */ $pdo = $model->getPdo(); if (!isset($options['criteria'])) { throw new \UnexpectedValueException('you need to specify the criteria'); } $stmt = $pdo->prepare('SELECT * FROM `'.$table.'` WHERE '.$options['criteria']); $stmt->execute($options['bind']); return $stmt->fetchAll(\PDO::FETCH_CLASS, get_class($model)); } abstract public function getSource(); } |
Laden der Daten
Als Beispiel legen wir ein Model zur Benutzerverwaltung an:
1 2 3 4 5 6 7 8 9 10 11 |
namespace Mvc\Model; class User extends ModelBase { public $id, $name, $created; public function getSource() { return 'users'; } } |
Entsprechend dazu gibt es in der Datenbank eine Tabelle, in die wir auch gleich einen Testeintrag füllen.
1 2 3 4 5 |
CREATE TABLE `users` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(45) NOT NULL, `created` DATETIME NOT NULL, PRIMARY KEY (`id`)); INSERT INTO `users` (`name`,created) VALUES ('tester',now()); |
Mit find()
und findFirst()
werden jetzt die Datensätze geladen. Dabei akzeptiert findFirst()
eine Id als Parameter um den häufigsten Fall komfortabel zur Verfügung zu stellen.
Im Controller benutzen wir das Model folgendermaßen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public function showUserAction() { $uid = (int)(isset($_GET['uid']) ? $_GET['uid'] : ''); if (!$uid) { throw new NotFoundException(); } $user = User::findFirst($uid); if (!$user instanceof User) { throw new NotFoundException(); } $this->view->setVars(['name' => $user->name]); } |
Der vollständige Code bis hierher (v0.4)
Daten anlegen und aktualisieren
Wie Daten geladen werden haben wir oben gesehen, jetzt benötigen wir noch eine Möglichkeit sie auch zu speichern. Eine rudimentäre save()
Funktion könnte so aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public function save() { $table = $this->getSource(); /** @var \PDO $pdo */ $pdo = $this->getPdo(); $fields = []; foreach ($this as $name => $val) { if ($val === null) { $fields[] = "`$name`=null"; } elseif (is_int($val)) { $fields[] = "`$name`=".$val; } else { $fields[] = "`$name`=".$pdo->quote($val); } } if ($this->id === null) { // new entry if (!$pdo->exec('INSERT INTO `'.$table.'` SET '.implode(',', $fields))) { throw new \RuntimeException('Could not crate '.get_class($this).': '.$pdo->errorInfo()[2]); } // fill the id $this->id = $pdo->lastInsertId(); } else { // update entry if ($pdo->exec('UPDATE `'.$table.'` SET '.implode(',', $fields).' WHERE `id` = '.((int)$this->id)) === FALSE) { throw new \RuntimeException('Could not update '.get_class($this).': '.$pdo->errorInfo()[2]); } } } |
Nachdem ein neuer Eintrag angelegt wurde, wird die Id der Datenbank abgerufen und dem Model zugewiesen. Falls die Id bereits vorhanden ist, wird in der Eintrag aktualisiert.
Um im Model bestimmte Felder mit Standardwerten vorzubelegen, können wir noch für jede Operation einen Hook anbieten (beforeCreate()
und beforeUpdate()
).
1 2 3 |
if (method_exists($this, 'beforeCreate')) { $this->beforeCreate(); } |
Implementiert das konkrete Model diese Funktion wird sie auch benutzt. Im Falle von User würden wir hier das aktuelle Datum in $this->created
setzen.
1 2 3 4 |
public function beforeCreate() { $this->created = date('Y-m-d H:i:s'); } |
Tolles Tutorial !!
Eine Frage habe ich jedoch dazu:
$stmt->execute($options[‚bind‘]);
welche Parameter stehen in $options ?
LG
Der $options Parameter bei den find Funktionen, soll möglichst flexibel einsetzbar sein.
Bisher versteht die findFirst() zum einen direkt eine ID ModelBase(23) und zum anderen die Definition von Parametern. Dann muss ein Array übergeben werden, dass sowohl die WHERE Bedingung und Platzhalterdefinition als auch die Werte für die Platzhalter enthält. Das sieht dann so aus:
[
'criteria' => 'userId = :uid',
'bind' => [':uid'=>5],
]
Hier gilt die Syntax von PDO.
Die find() Funktion versteht nur die zweite Variante, da ja explizit mehrere Ergebnisse gefunden werden sollen und die ID immer nur eines liefern würde.
Hallo,
geniales Tutorial (vielen Dank!), welches ich gerne dazu nutzen möchte, meine Kenntnisse in OOP zu vertiefen!
Eine Frage:
Unterstützt dieser MVC-Ansatz auch das Konzept Post-Redirect-Get? Ich möchte, dass der Benutzer die Vor-/Zurück-Buttons im Browser nutzen kann, OHNE dass er ein Post-Formular extra noch einmal re-loaden muss.
Danke und viele Grüße
Jörg
Hallo Jörg,
das funktioniert ohne Probleme.
Was du nach einen POST-Request an den User zurück gibst (Webseite, Redirect, …) hat mit MVC an sich nichts zu tun.
Und jede Action hört erst mal auf alle Anfragen egal welcher Typ (GET, POST, PUT, …). Weitere Einschränkungen wären in diesem einfachen Beispiel die Aufgabe des Entwicklers. Praktisch alle Frameworks bieten hier Unterstützung an.
Hoffe das hilft!