• Jetzt anmelden. Es dauert nur 2 Minuten und ist kostenlos!

[PHP] MVC Tutorial auf Basis eines Gästebuchs (OOP)

T!P-TOP

Mitglied
Inhaltsverzeichnis


  1. Einleitung
    1. MVC
    2. Wieso MVC einsetzen?
  2. Tutorial: Gästebuch
    1. Verzeichnisstruktur
    2. TemplateView-Pattern
    3. Der Controller
    4. Das Model
    5. Das View (=Template)
    6. Die Bootstrap-Datei


1. Einleitung

1. MVC
MVC ist die Abkürzung für Model View Controller. In der Objekt Orientierten Programmierung handelt es sich beim Model und beim Controller um eine Klasse, welche Methoden bereit stellen. Views sind nichts anderes als Templates, welche mit dem Dateiformat .phtml abgespeichert werden. Diese können PHP-Code beinhalten, dieser sollte aber so gut wie möglich dezimiert werden. Für Views sollte der Template-Syntax von PHP verwendet werden, da dieser die Übersichtlichkeit maßgeblich steigert.

Model
Das Model stellt Methoden bereit, mit die der Controller Daten aus Datein oder Datenbanken erhalten kann. Auch beinhaltet die Model-Klase Methoden, mittels die der Controller Daten in Datein oder Datenbanken eintragen kann.

Controller
Der Controller ist sozusagen das Verbindungsstück zwischen Model und View in einer MVC-Einheit (Eine große Applikation besteht oft aus mehreren MVC-Einheiten, welche z.B. auch als Module bezeichnet werden). Der Controller instanziiert mind. 2 Objekte: das Model und ein TemplateView (= ein "Design-Pattern" oder auf deutsch "Entwurfsmuster").
Der Controller ruft Methoden des Model-Objektes auf und speichert den Return-Wert dieser Methoden in einem Attribut des TemplateView-Objektes. Die Views/Templates werden innerhalb des TemplateView-Objektes eingebunden und können über dessen getter Methode auf die Daten zugreifen, die der Controller im TemplateView-Objekt gespeichert hat.

View
Ein View-Script ist nichts anderes als ein Template. Abgespeichert wird ein View meist mit der Dateiendung .phtml, da dort eben php und html drin steckt. Welches View in das TemplateView-Objekt eingebunden wird, entscheidet der Controller.




2. Wieso MVC einsetzen?
Jeder PHP-Beginner erstellt für seine Projekte zahlreiche PHP Datein, welche sowohl aus PHP als auch HTML-Code bestehen. Sie vermischen das Display mit der Logik. Dadurch erhöht sich die Fehlerquote, Fehler können sich leichter einschleichen und schwerer ausfindig gemacht werden.
In MVC gibts es dagegen eine klare Trennung, man weiß auf der Stelle, wo man etwas findet. Zudem sind MVC Anwendungen, besonders im Objekt Orientierten Stil, sehr gut erweiterbar/abänderbar.




2. Tutorial: Gästebuch


Wieso ich ein Gästebuch als Basis für dieses Tutorial gewählt habe ist leicht zu erklären. Für ein Gästebuch wird der Zugriff auf Datein/Datenbanken benötigt, in denen Gästebuch-Einträge gespeichert werden. Somit kommt das Model anhand eines reellen Beispieles zum Einsatz.

1. Verzeichnisstruktur

wwwroot
`-- library
`-- class.TemplateView.php

`-- models
`-- guestbook.php

`-- views
`-- guestbook.phtml

`-- controllers
`-- guestbook.php

`-- css
`-- design.css

`-- index.php




2. TemplateView-Pattern


Der Code von wwwroot/library/class.TemplateView.php:
PHP:
<?php
class TemplateView {
    private $template;
    private $vars = array();
    
    public function __construct($template) {
        $this->template = $template;
    }
    
    public function assign($key, $value) {
        $this->vars[$key] = $value;
    }
    
    public function __get($key) {
        if (isset($this->vars[$key])) {
            return $this->vars[$key];
        }
        return false;
    }
        
    public function render() {
        ob_start();    
        $template = 'views/'. $this->template . '.phtml';        
        if (!is_file($template)) {
            return 'Template not found!';
        }        
        include $template;        
        $view = ob_get_clean();
        return $view;
    }
}


Der meiste Code sollte hier recht verständlich sein. Allerdings geht ich noch kurz auf die Method __get() ein. Dabei handelt es sich um eine Magische Interzeptor Method. Versuche ich z.B. in einem View-Script auf ein Eigenschaft zuzugreifen und existiert diese nicht bzw. sie ist als privat deklariert, dann wird automatisch diese magische Methode aufgerufen.

Beispiel:
Ich versuche folgendes im Template guestbook.phtml auszugeben:

PHP:
<p><?php echo $this->foo; ?></p>

In unserer TemplateView-Klasse gibt es dieses Attribut (foo) nicht, wodurch die __get() Methode aufgerufen wird, dabei wird Ihr als Parameter foo übergeben: __get('foo');

Der Vorteil dieser magischen Methode: wir sparen uns dadurch Code, wenn wir auf Daten zugreifen möchten, die im TemplateView abgelegt sind.




3. Der Controller
Der Controller muss über 2 Methoden verfügen:

  • [*=1]indexAction() // holt sich vom Model alle Einträge[*=1]newEntryAction() // Sorgt dafür, dass das Model einen neuen Eintrag speichert
Was sucht das Action Postfix am Methodennamen? Der Controller ist eine Klasse. Dessen Methoden sind sogenannte Aktionen, welche man über URL Parameter aufrufen kann (dazu mehr in der Bootstrap-Datei (=index.php)).


Der Code von wwwroot/controller/guestbook.php:
PHP:
<?php
class guestbook_controller {
    private $model = null;
    private $view  = null;

    public function __construct() {
        require 'models/guestbook.php';    
        require 'library/class.TemplateView.php';    
        $this->model = new guestbook();    
        $this->view  = new TemplateView('guestbook');
    }

    public function indexAction() {
        $entries = $this->model->getEntries();
        $this->view->assign('entries', $entries);
        return $this->view->render();
    }
    
    public function newEntryAction() {
        if (isset($_POST['saveEntry']) && isset($_POST['name']) && isset($_POST['message'])) {
            $entryDatas = array(
                'Name'    => trim($_POST['name']),
                'Message' => trim($_POST['message']);
            );
            $this->model->saveEntry($entryDatas);
        }
        $this->indexAction(); 
    }
}

Neben indexAction() und newEntryAction() habe ich noch einen Konstruktur (__construct()) definiert. Dieser wird beim instanziieren des Controllers aufgerufen. Er erledigt die Aufgaben, die für alle Aktionen (actions) notwendig sind.




4. Das Model
Unser Model benötigt zwei Methoden. Wie die heißen, sollte aus dem Code des Controllers schon vorhergehen:

  • [*=1]getEntries() // liefert alle Gästebuch-Einträge zurück[*=1]saveEntry() // nimmt als Parameter ein Array entgegen, welches den Gästebuch-Eintrag repräsentiert und tragt den Eintrag in die Datenbank

Der Code vom wwwroot/model/guestbook.php:
PHP:
<?php
class guestbook {
    public function getEntries() {
        $this->connectToDB();
        $sql = "INSERT `name`, `message` FROM `guestbook`";
        $result = mysql_query($sql);
        $entries = array();            
        while ($row = mysql_fetch_array($result)) {
            array_push($array, $row);
        }            
        return $entries; // true|false            
    }
    
    public function saveEntry(array $datas) {
        $this->connectToDB();
        $sql = "INSERT INTO `guestbook` 
             . "(`name`, `message`) VALUES "
             ('".mysql_real_escape_string($datas['Name'])."', '".mysql_real_escape_string($datas['Message'])."')";
        $result = mysql_query($sql);
        echo $result; // true|false
    }
    
    public function connectToDB() {
        mysql_connect('localhost', 'Username', 'Password');
        mysql_select_db('database');
    }
}




5. Das View (=Template)
Das View-Script (wwwroot/views/guestbook.phtml) beinhaltet folgenden Code:
PHP:
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" type="text/css" href="css/design.css" />
    </head>
    <body>        
        <h2>Gästebuch mit MVC</h2>
        
        <?php if (is_array($this->entries) && count($this->entries) > 0) : ?>

        <?php foreach ($this->entries as $entry) : ?>

        <p>
            <strong><?php echo $entry['name']; ?></strong>
            <?php echo $entry['message']; ?>
        </p>

        <?php endforeach; ?>

        <?php endif; ?>



        <!-- Das Formular -->
        <form action="index.php?controller=guestbook&amp;action=newEntry" method="post">
            <p>
                Name<br />
                <input type="text" name="name" />
            </p>
    
            <p>
                Nachricht<br />
                <textarea cols="40" rows="5"></textarea>
            </p>
            <p><input type="submit" name="saveEntry" value="Eintragen" /></p>
        </form>
    </body>
</html>
 
Werbung:
Da für einen Beitrag nur einen bestimmte Anzahl an Zeichen möglich ist, benötige ich nun noch mind. diesen Post, um das Tutorial fertig zu stellen. Im letzten Post hab ich mit dem View-Script aufgehört.
Weiter gehts mit der Bootstrap Datei. Dort kann nun entschieden werden, welcher Controller und dazugehörige Aktion aufgerufen wird.

Hier ein Beispiel, wie die index.php aussehen könnte:

PHP:
<?php
$controller = (isset($_GET['controller'])) ? trim($_GET['controller']) : 'guestbook'; 
$action     = (isset($_GET['action'])) ? trim($_GET['action']) : 'index'; 

if (is_file('controllers/' . $controller . '.php')) {
    require 'controllers/' . $controller . '.php';
    
    $className  = $controller . '_controller';
    $actionName = $action . 'Action';
    
    if (class_exists($className)) {
        $controller = new $className();
        
        if (method_exists($controller, $actionName)) {
            $controller->$actionName();
        }
    }
}

In den ersten beiden Zeilen werden 2 Variablen mittels Ternary Operator der Name vom Controller und der dazugehörigen Action-Methode zugewiesen. Dabei wird geprüft, ob die URL-Parameter controller und action vorhanden sind. Ist dies der Fall, wird deren Wert verwendet, ansonsten wird für den Controller als default Wert guestbook und als Action-methode index verwendet. Danach wird geprüft, ob die Klasse/Controller überhaupt existiert, den der Bentuzer über die URL angefordert hat. Existiert die Controller-Datei und die Controller-Klasse, dann wird geprüft, ob die angeforderte Action Methode vorhanden ist - ist dies ebenfalls der Fall, wird diese aufgerufen.

Ruft man nun die index.php in einem Webbrowser auf (ohne dabei URL-Parameter anzugeben), sollte automatisch das Gästebuch mit allen Einträgen angezeigt werden.




View-Helper
In der guestbook.phtml wird einfach der Name und die Nachricht, die ein User eingegeben hat, ausgegeben. Es wird bis jetzt noch nichts maskiert, wodurch Angreifern XSS ermöglicht wird. Das führt nun zum nächsten Pattern: View-Helpern.
View-Helper statten Templates/View-Scripts mit zusätzlicher Funktionalität aus. Man könnte einfach htmlentities() oder htmlspecialchars() für die Ausgaben der Gästebuch-Einträge anwenden, docht verstößt dass gegen ein wichtiges Prinzip der Objekt Orientierten Programmierung: Abstrahierung.

View-Helper nehmen einem Template Entwickler eine Menge Arbeit ab -> hinter einem View-Helper kann eine Menge Funktionalität stecken und um diese nutzen zu können, reicht ein ein einziger Methodenaufruf.
Durch die Abstrahierung kann es dem Template Entwickler egal sein, wie eine Methode z.B. namens escape() nun genau arbeitet - es reicht zu wissen, dass durch den Einsatz dieser Funktion schädlicher Code unschädlich gemacht wird.

Damit für die Nutzung eines View-Helper nur ein Methoden-Aufruf im View nötig ist, muss in die wwwroot/library/class.TemplateView.php eine weitere magische Methode implementiert werden: __call()
Sobald man versucht, eine nicht existente Methode eines Objektes aufzurufen, wird - sofern implementiert - die __call-Methode aufgerufen.

Die wwwroot/library/class.TemplateView.php sieht nach der Impementierung dieser Interzeptor-Methode folgendermaßen aus:

PHP:
<?php
class TemplateView {
    private $template;
    private $helpers = array();
    private $vars = array();
    
    public function __construct($template) {
        $this->template = $template;
    }
    
    public function assign($key, $value) {
        $this->vars[$key] = $value;
    }
    
    public function __get($key) {
        if (isset($this->vars[$key])) {
            return $this->vars[$key];
        }
        return false;
    }
    
    public function __call($methodName, $params) {
        if (!isset($this->helpers[$methodName])) {
            if (!file_exists('views/helpers/' . $methodname . '.php')) {
                return 'Der View Helper "' . $methodname . '" existiert nicht!';   
            }
            include 'views/helpers/' . $methodname . '.php';
            $this->helpers[$methodname] = new $methodname();
            return $this->helpers[$methodname]->execute($params);
        }
        return $this->helpers[$methodname]->execute($params);
    }
        
    public function render() {
        ob_start();    
        $template = 'views/'. $this->template . '.phtml';        
        if (!is_file($template)) {
            return 'Template not found!';
        }        
        include $template;        
        $view = ob_get_clean();
        return $view;
    }
}

Als Attribut der TemplateView-Klasse wurde ein Array names helpers hinzugefügt. In diesem Array werden instanziierte View-Helpter gespeichert. In der __call-Methode wird am Anfang geprüft, ob der geforderte View-Helper nicht schon vorhanden ist. Falls nicht, wird versucht, den View-Helper einzubinden und ein Objekt davon zu erzeugen. In der __call Methode wurde ein weiterer Design-Pattern implementiert: Singleton -> gibt es bereits einen View-Helper, wird kein neuer erzeugt, sondern der existierende verwendet.


In der Verzeichnisstruktur kommt nun eine kleine Änderung hinzu:

wwwroot
`-- library
`-- class.TemplateView.php

`-- models
`-- guestbook.php

`-- views
`-- helpers
`-- escape.php
`-- guestbook.phtml

`-- controllers
`-- guestbook.php

`-- css
`-- design.css

`-- index.php

Das Verzeichniss helpers wurd im Verzeichnis views hinzugefügt. Darin muss sich die Datei escape.php befindet. Der Code dieser Datei sieht folgendermaßen aus:
PHP:
<?php
class escape {
    public function execute($params) {
        return htmlentities($params[0], ENT_QUOTES, 'UTF-8');
    }
}

In der wwwroot/views/guestbook.phtml können wird nun mittels$this->escape('Ein String') Bentuzereingaben maskieren und somit XSS vermeiden. Unsere wwwroot/views/guestbook.phtml sieht nun so aus:

PHP:
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" type="text/css" href="css/design.css" />
    </head>
    <body>        
        <h2>Gästebuch mit MVC</h2>
        
        <?php if (is_array($this->entries) && count($this->entries) > 0) : ?>

        <?php foreach ($this->entries as $entry) : ?>

        <p>
            <strong><?php echo $this->escape($entry['name']); ?></strong>
            <?php echo $this->escape($entry['message']); ?>
        </p>

        <?php endforeach; ?>

        <?php endif; ?>



        <!-- Das Formular -->
        <form action="index.php?controller=guestbook&amp;action=newEntry" method="post">
            <p>
                Name<br />
                <input type="text" name="name" />
            </p>
    
            <p>
                Nachricht<br />
                <textarea cols="40" rows="5"></textarea>
            </p>
            <p><input type="submit" name="saveEntry" value="Eintragen" /></p>
        </form>
    </body>
</html>
 
Bitte nicht so benutzen, wie's hier steht. Jegliche PHP-Datei, die sich auf dem Server befindet kann damit ausgeführt werden. Unter anderem in der vorgeschlagenen index.php-Seite. Im besten Fall kann das Script ein fatales Error ohne weitere Probleme verursachen, im schlimmsten aber 'ne Datei erwischen, die etwas wichtiger ist und beispielsweise Datenbankeinträge löscht oder komplett anzeigt.
 
Werbung:
Moved -> PHP-Forum

Vielen Danke für deine Mühen, T!P-TOP!
Da nach Meinung von Asterixus das Tutorial möglicherweise noch nicht ausgereift ist, möchte ich bitten, dies hier zu diskutieren. Ich würde mich freuen, wenn im Anschluss daran ein umso besseres Tutorial daraus entsteht.

Grüße Corvulus
 
Die index.php sollte man eher nicht so nutzen, wie sie hier abgebildet ist. Ein Resolver, Domain-Objects für Reponse und Request, ein Router sowie ein Front-Controller fehlen. Allerdings ist ein Front-Controller-Layout für MVC nicht zwingend notwendig.

Bitte nicht so benutzen, wie's hier steht. Jegliche PHP-Datei, die sich auf dem Server befindet kann damit ausgeführt werden. Unter anderem in der vorgeschlagenen index.php-Seite. Im besten Fall kann das Script ein fatales Error ohne weitere Probleme verursachen, im schlimmsten aber 'ne Datei erwischen, die etwas wichtiger ist und beispielsweise Datenbankeinträge löscht oder komplett anzeigt.
Entweder liegt das am Restalkohol und ich bemerke die Schwachstelle nicht oder aber es ist so eine, wie Du sie schilderst, gar nicht vorhanden.

Freilich ist die index.php meiner Meinung nach nicht optimal, aber dennoch können nicht alle Scripte am Server ausgeführt werden. Ein Resolver-Object für die Command Control Schicht selektiert den Controller auf ähnliche Weise - vorallem wenn man keine statischen Routen festlegt (bspw. mittels Array). Soll der Action-Controller und die dazugehörige Aktion dynamisch geladen werden, dann müssen zumindest zwei Parameter per URL übergeben werden.

In den ersten beiden Zeilen der index.php wird mittes Ternary-Op. geprüft, ob 2 Parameter übergeben worden sind, falls nicht, werden Default-Werte verwendet. Durch die darauf folgende if-Bedingung wird schonmal maximal ein Script/Controller aus dem Verzeichnis "controllers" geladen - somit ist das ausführen jeglicher am Server befindlichen Datein ausgeschlossen! Es kann auch nicht zu einem fatalen Error kommen, da nur ein Objekt von einem existierenden Controller instanziiert wird. Auch wird geprüft, ob die übergebene Action-Methode vorhanden ist, wodurch auch der 2. mögliche fatale Error nicht erzeugt werden kann.

Die Parameter "controller" und "action" sollten dennoch nicht so übernommen werden, wie Sie über die URL übergeben werden.

Ein gutes neues Jahr wünsch ich euch allen!

Grüße
 
Mal angenommen, der Benutzer gibt als GET-Parameter des Controllers "../../boese_datei" (und wir gehen mal davon aus, dass diese Datei auch existiert) ein. Erst überprüfst du die Existenz der Datei, is_file sagt: Ja, ja, das ist eine Datei. PHP includet diese Datei und führt sie aus. Das Problem ist, dass ein Benutzer in einen Get-Parameter das eingeben kann, was er will. Du überprüfst nicht, ob der Controller existiert, du überprüfst lediglich, ob eine Datei existiert.
 
Werbung:
Du überprüfst nicht, ob der Controller existiert
Doch, das prüfe ich schon - class_exists('Controller-Klassenname')

Du hast aber in dem Punkt natürlich Recht, dass sich auch eine andere Datei inkludieren lässt (sofern sie existiert). Aber wie ich schon sagte:
Die Parameter "controller" und "action" sollten dennoch nicht so übernommen werden, wie Sie über die URL übergeben werden.


Es handelt sich bei diesem Tutorial übrigens eher um die "MVC-Zend-Variante" - diese ist einfach zu verstehen und besteht nur aus 3 Schichten. Ich wollte in diesem Tutorial eigentlich nur die Aufteilung auf diese 3 Schichten zeigen, aber wir können das ganze ja erweitern und das Front Controller Pattern implementieren.
 
Zurück
Oben