Mein eigenes MVC-Framework: Das Model
Jetzt, wo wir schon die letzten beiden Komponenten des MVC-Patterns haben (View, Controller), fehlt nur noch das M im Bunde. Das Model! Es fällt bei mir im Framework sehr mager aus.
Was sind die Aufgaben des Models?
Das Model ist für die Speicherung der Daten zuständig. Und sonst für nichts! Wir wollen, dass man eine Applikation, die auf eine MySQL-Datenbank ausgelegt ist, problemlos auf Oracle umstellen kann. Dazu ist es wichtig, dass die restliche Anwendung zwar Daten verändern und lesen kann, aber nicht weiß, wo und wie die Daten letztendlich gespeichert werden.
Da ich keine gemeinsamen Kriterien von Models finden konnten, sieht das Interface und die Abstrakte Model-Klasse so aus:
FW_Model_Abstract
/** * @licence See: /licence.txt */ abstract class FW_Model_Abstract implements FW_Model_Interface { //abstract public function __get($name); //abstract public function __set($name, $value); }
FW_Model_Interface
/** * @licence See: /licence.txt */ interface FW_Model_Interface { }
Der Zugriff auf die Daten funktioniert also auch, wenn weder das Interface implementiert, noch von der abstrakten Klasse geerbt werden. (Weil die beiden Dateien momentan noch keine Vorschriften machen) Da es aber häufig erst während der Entwicklung, wenn es schon zu spät ist, zu neuen Ideen konnt, sollte man präventiv trotzdem schonmal von FW_Model_Abstract erben! Schadet ja nicht!
Definition eines konkreten Models
In den meisten Fällen wir eine Datenbanktabelle von einem Model repräsentiert. Das heißt, dass wenn es folgende Datenbanktabellen gibt
- User
- Posts
es auch folgende Models gibt
- Model_User
- Model_Posts
Natürlich gibt es auch Ausnahmefälle.
Übrigens bekommt das Framework (sprich: Der FrontController) von den Models nichts mit. Er erstellt diese also auch nicht, wie das bei CakePHP der Fall ist. Das ist die Aufgabe der Controller bzw. SubController. Üblicherweise erstellt man alle notwendigen Models in der init()-Methode und speichert sich als Attribut im Objekt ab. So hat jede Methode Zugriff darauf. In manchen Fällen kann es auch notwendig sein, das Model in die Registry zu legen. So ein Fall wäre z.B., dass mehrere Controller auf das selbe Model zugreifen müssen und die Erzeugung des Models zu aufwändig wäre, um es mehrmals bei jedem Seitenaufruf zu machen.
Ein einfaches Model aus meinem aktuellen Projekt ist dieses hier:
class Model_User extends FW_Model_Abstract { private $db = null; public function __construct() { $this->db = FW_MySQL::getInstance("db1"); } public function isValidPassword($username, $password) { $res = $this->db->getRow("SELECT 1 FROM users WHERE username='".$this->db->escape($username)."' AND password='".md5($password)."'"); if($res) { return true; } else { return false; } } public function isLocked($id) { $res = $this->db->getRow("SELECT locked FROM users WHERE user_id=".(int)$this->db->escape($id)); if($res) { return ($res["locked"] == "1") ? true : false; } throw new Exception("Dieser User existiert nicht"); } public function getUserList() { $res = $this->db->getRowset("SELECT u.username, u.user_id, u.locked, u.group_id, g.group_name FROM users AS u LEFT JOIN groups AS g ON u.group_id = g.group_id ORDER BY u.user_id ASC"); return $res; } public function getUserID($username) { $res = $this->db->getRow("SELECT user_id FROM users WHERE username='".$this->db->escape($username)."'"); if($res && isset($res["user_id"])) { return $res["user_id"]; } throw new Exception("Dieser User existiert nicht"); } public function getUserName($id) { $res = $this->db->getRow("SELECT username FROM users WHERE user_id=".(int)$this->db->escape($id)); if($res && isset($res["username"])) { return $res["username"]; } throw new Exception("Dieser User existiert nicht"); } public function setLocked($userid, $locked) { if($locked !== true && $locked !== false) { $locked = true; } $userid = (int)$userid; $this->db->updateTable("users", array("locked" => (int)$locked), "user_id = ".$userid); } public function getUserData($user_id) { return $this->db->getRow("SELECT * FROM users WHERE user_id=".(int)$this->db->escape($user_id)); } public function getUserGroupID($user_id) { return $this->db->getFieldValue("SELECT group_id FROM users WHERE user_id=".(int)$this->db->escape($user_id), "group_id"); } public function setUserGroupID($group_id, $user_id) { return $this->db->updateTable("users", array("group_id" => (int)$group_id), "user_id=".(int)$user_id); } public function addUser($username, $password, $mail) { $this->db->query("INSERT INTO users (username, group_id, password, email) VALUES ('".$this->db->escape($username)."', 2, '".md5($password)."', '".$this->db->escape($mail)."')"); return $this->db->getLastId(); } public function userExists($username) { $res = $this->db->getRow("SELECT user_id FROM users WHERE username='".$this->db->escape($username)."'"); return ($res > 0) ? true : false; } public function mailExists($mail) { $res = $this->db->getRow("SELECT user_id FROM users WHERE email = '".$this->db->escape($mail)."'"); return (bool)$res; } }
Diese Klasse ist bei mir für die Speicherung der User zuständig. Sie kann folgendes:
- User anlegen
- Passwort auf Richtigkeit testen
- Testen, ob User gesperrt ist
- Alle User zurückgeben
- Die zu einem Namen gehörende ID finden
- Einen User sperren
- Alle Daten zu einer UID liefern
- Die Gruppen-ID lesen
- Die Gruppen-ID ändern
- User auf Existenz prüfen
- E-Mail-Adresse auf Vorhandensein prüfen
Das sind Funktionen, die jetzt in jedem Controller verwendet werden können.
Ein Beispiel: Die Registrierung (Anlegen eines neuen Users)
Wenn man möchte, dass man neue User über ein herkömmliches Registrierungsformular anlegen kann, aber auch ganz einfach im Adminbereich, muss man den Code zum Speichern nur einmal programmieren. Überall, wo eine Aufgabe der User-Model-Klasse ausgeführt werden soll, wird jetzt eine Instanz des Models erzeugt.
Wie bei mir die Registrierung aussieht, könnt ihr hier begutachten:
public function register2_Action() { $form = $this->getRegisterForm(); if(!$form->validate()) { if($form->getValue("password1") != $form->getValue("password2")) { $form->addErrorMessage("password1", "Passwort stimmt nicht überein"); $form->addErrorMessage("password2", "Passwort stimmt nicht überein"); } } $username = $form->getValue("username"); $pw = $form->getValue("password1"); $mail = $form->getValue("mail"); if($this->model_user->userExists($username)) { $form->addErrorMessage("username", "Diesen Usernamen gibt es schon: ".$username); } if($this->model_user->mailExists($mail)) { $form->addErrorMessage("mail", "Diese E-Mail-Adresse ist schon vergeben! (".$mail.")"); } $form->addErrorMessages(); $form->addOldValues(array("password1", "password2")); $errors = $form->getErrors(); if(count($errors) < 1) { $id = $this->model_user->addUser($username, $pw, $mail); $this->view->addTemplateContent("content", array("username" => $username, "mail" => $mail), "user/register_success.tpl.php"); } else { $this->view->addTemplateContent("content", array("errors" => $errors, "form" => $form), "user/register.tpl.php"); } }
Das ist die komplette Methode, die das Formular validiert und bei keinen Fehlern den User anlegt.
Erzeugt wird das Model so:
public function init() { $this->model_user = new Model_User(); //Das "wichtige" Model für diesen Artikel $this->model_group = new Model_Group(); $this->session = FW_Session::getInstance(); }
Tabellenübergreifende Models
In vielen Fällen wird es nötig sein, mehrere Tabellen in einem Model anzusprechen. Das widerspricht dann leider meinem Prinzip „Eine Tabelle pro Model“, aber eine andere Lösung würde die Komplexität erhöhen. Evtl. wäre hier auch der Einsatz eines ORMs angebracht.
Viele unnötige (?) Abfragen
Dadurch, dass man viele Attribute getrennt auslesen kann, werden auch entsprechend viele unnötige Abfragen getätigt. Theoretisch wäre es hier sinnvoll, die User in einem Array zu Cachen und bei jedem Zugriff auf ein Attribut alle Attribute in das Array einzulesen. Wird dann wieder ein Attribut benötigt, muss man die DB nichtmehr belasten, sondern kann aus dem Array lesen. Nur dafür war ich bisher zu faul 😉
Bei Fragen steht euch die Kommentarfunktion zur Verfügung!
[…] Mein eigenes MVC-Framework: Das Model […]
[…] Models […]
Die aktuellste Version meines HMVC-Frameworks erhaltet ihr ab sofort immer hier: http://www.net-developers.de/blog/2011/02/13/download-info-shfw-hmvc-framework-in-php/