[PHP] MVC2 und Frontcontroller

tleilax

be forever curious
ID: 27936
L
20 April 2006
1.845
184
Inhalt:
  • MVC? Was ist das denn?
  • MVC2 im Detail
  • MVC2 mit Frontcontroller in PHP
    • - Erstellen einer Sitemap
    • - Verarbeiten einer Anfrage
  • Codebeispiel anhand von Smarty

MVC? Was ist das denn?

MVC steht für das Designpattern Model View Control, welches beschreibt, wie eine Anwendung praktischerweise in 3 Komponenten aufgeteilt werden kann (und evtl auch sollte). Die 3 Komponenten sind dabei:
  1. Das Model ist das der Anwendung zugrunde liegende Datenmodell. In den gängigsten Fällen eine Datenbank oder eine ähnliche Repräsentation von Daten.
  2. Der View ist die grafische Repräsentation der Anwendung - im Allgemeinen das User Interface (oder auch Benutzungsschnittstelle).
  3. Die Control-Komponente übernimmt nun die eigentliche Logik der Anwendung. Sie erzeugt aus dem Model den View und verarbeitet eingehende Anfragen.
Wie man erkennen kann, hat man somit eine strukturelle Trennung der drei wichtigen Komponenten einer Anwendung, was mehrere Vorteile birgt:
  • Das Testen und das Austauschen einzelner Komponenten wird vereinfacht, da sie nicht ineinander verwoben sind.
  • Durch die Kapselung des Views ergibt sich die Möglichkeit, mehrere Views haben zu können, ohne die komplette Anwendung umschreiben zu müssen. Der neue View wird einfach hinzugefügt.
  • Ebenfalls wird durch die Kapselung sichergestellt, dass jegliche Schritte (Eingabe, Verarbeitung, Ausgabe) sequentiell ablaufen und nicht parallel.

MVC2 im Detail

MVC2 ist eine einfachere Variante des MVC(3), das oben beschrieben wurde. Hierbei werden Model und Control zusammengefasst, sodass nur noch 2 Komponenten übrig bleiben. In Webanwendungen mit DB-Anbindung ist es oft schwer, diese Trennung noch zu erkennen, da das Model fast immer in der Datenbank liegt und somit schon per se gekapselt ist.

MVC2 mit Frontcontroller in PHP

Der grundlegende Ansatz für MVC2 ist (meiner Meinung nach) das Benutzen eines Templatesystems. So kann effizient eine Trennung von View und Model/Control bzw. Design und Logik erreicht werden.

Kommen wir nun zum Frontcontroller. Will man eine PHP-Anwendung schreiben, bieten sich grundlegend 2 Varianten an, um beliebig viele Seiten anlegen zu können.
  1. Man legt für jede Seite eine neue PHP-Datei an, die dann für sich alleine ihre Aufgabe übernimmt. Hierbei wird im Header der Datei meistens eine weitere Datei inkludiert, die allgemeine Funktionen übernimmt (Config einlesen, DB-Verbindung aufbauen etc).
  2. Man legt eine einzige PHP-Datei an und übergibt ihr per Parameter, welche Seite angefordert wurde (das klassische index.php?site=home).
Der zweite Ansatz wäre nun der des Frontcontrollers. Er birgt unter anderem folgende Vorteile:
  • Die Initialisierungslogik liegt gebündelt und für alle Seiten vor. Das heisst, man hat keine header.php oder ähnliches, die am Anfang jeder Datei inkludiert werden muss, da man schliesslich nur eine Datei hat.
  • Änderungen am Frontcontroller werden sofort auf allen Seiten gültig. Man muss sich bei grundlegenden Änderungen nicht mehr durch alle Seiten kämpfen.

- Erstellen einer Sitemap

Für den Frontcontroller ist es zwingend notwendig, eine definierte Liste von Seiten zu haben, die die Anwendung ausmacht. Dies wäre eine sogenannte White List, da nur Seiten ausgeliefert werden, die in dieser Liste stehen. Es gibt auch andere Ansätze, wo diese Liste nicht existiert, sondern über die Existenz einer einzelnen Datei geprüft wird, ob die entsprechende Seite verfügbar ist, aber ich halte diese Variante für äußerst problematisch, da sie im Gegensatz zur White List wesentlich mehr Risiken birgt.

Die Sitemap kann nun in verschiedenen Formaten gespeichert sein.
  • Man kann innerhalb von PHP ein Array definieren:
    PHP:
    $sitemap = Array('home', 'news', 'impressum');
    Vorteil:
    Einfach zu warten.
    Nachteil:
    Äußerst unflexibel und unschön, da direkt im Anwendungscode (muss man evtl. erst suchen).
  • Man lagert die Sitemap extern mittels Textdatei, per CSV/XML oder auch in der DB.
    Vorteil:
    Immer noch einfach wartbar, aber schneller aufzufinden, da sie einen fest definierten Platz hat.
    Nachteil:
    Es müssen mehr Daten geparset werden bzw. im Fall der DB (wenig) kostspielige DB-Abfragen gemacht werden.
Ich habe mich nach einiger Zeit für die XML-Variante entschieden, da sie wiederum flexibler gegenüber der CSV-Variante ist. Bei grösseren Anwendungen mit entsprechender Serverleistung und entsprechender Adminoberfläche würde sich aber die DB-Variante anbieten, da sie wesentlich einfacher zu verändern ist als Dateien.

Ein Beispiel meiner XML-Variante:
Code:
<sitemap>
  <site name="Startseite" key="home"/>
  <site name="News" key="news" description="Neuigkeiten"/>
  <site name="Impressum" key="imprint"/>
  <section name="user">
     <site name="Benutzerbereich" key="home"/>
     <site name="Login" key="login" access="public"/>
     <site name="Verwaltung" key="profile" access="private" />
  </section>
</sitemap>
Ich hoffe, das wirkt nicht zu verwirrend, es soll zum Teil auch nur die Möglichkeiten dieser Variante darstellen.

Ein kleiner Einschub noch zur Performance:

Man kann das Parsen der Sitemap vereinfachen, indem man diese cachet. Beim Einlesen wird geprüft, ob eine Cache-Datei für die Sitemap vorliegt und dann dieses Dateidatum mit dem der Sitemap verglichen. Ist das Datum der Sitemap neuer, wird neu geparset, ansonsten die gecachete Version eingelesen. Dafür bieten sich die Funktionen serialize()/unserialize() an, womit man ein Array in einen String umwandeln und vice versa.

- Verarbeiten einer Anfrage

Nun haben wir die Sitemap vorliegen und müssen eingehende Anfragen verabeiten können. Dazu wird die angefragte Seite aus dem Request ausgelesen und mit der Sitemap verglichen:
PHP:
$site = $_GET['site'];
if (empty($site))
  $site = 'home';  // Eingangsseite, wenn keine spezielle Seite angefragt wird

if (empty($sitemap[ $site ]))
  error('Ungültige Anfrage - Seite nicht vorhanden');
Ab diesem Punkt wissen wir, dass die in $site gespeicherte Seite vorhanden ist und verarbeitet werden kann. Dazu wird die entsprechende Seite mit der Logik ausgeführt oder auch in einem switch/case-Block die Logik direkt ausgeführt, wovon ich aber abraten würde, da dies viel zu unübersichtlich wird und schnell zu elendig grossen Dateien führt. Anschliessend wird das der Seite zugewiesene Template ausgeführt und die Seite ausgeliefert.

Ich für meinen Teil habe dies so realisiert, dass in einem /bin-Ordner alle PHP-Skripte der einzelnen Seiten liegen. Diese sind exakt so benannt, wie der Schlüssel der aufzurufenden Seiten. Als Beispiel würde bei ?site=home die Datei /bin/home.php inkludiert. Ebenso heisst das Template analog /templates/home.tpl. Dadurch reduziert sich der PHP-Code im Frontcontroller auf:
PHP:
require('/bin/'.$site.'.php');
$templatesystem->assign('template', $site.'.tpl');
Allerdings könnten die einzelnen anzusprechenden Skripte und Templates auch in der Sitemap definiert werden. Muss jeder für sich selbst sehen, was ihm am liebsten ist.

Codebeispiel anhand von Smarty

Für Smarty (und andere Templatesystems genauso) braucht es für die Seite ein Haupttemplate (beispiel.tpl), das exemplarisch wie folgt aussieht:
HTML:
<html>
<head>
 <title>Beispielseite{if !empty($sitetitle)} - {$sitetitle}{/if}</title>
</head>
<body>
  <h1>Beispiel</h1>
  <hr/>
  <a href="?site=home">Startseite</a>
  | <a href="?site=beispiel1">Beispiel 1</a>
  | <a href="?site=beispiel2">Beispiel 2</a>
  <hr/>
  {include file=$template}
</body>
</html>
Der Frontcontroller sähe nun folgendermassen aus (hier habe ich der Einfachheit halber die Sitemap innerhalb der Datei realisiert):
PHP:
<?php
  require('./classes/Smarty.class.php');
  $smarty = new Smarty;

  $sitemap = Array('home'=>'Startseite', 
                          'beispiel1'=>'Beispiel 1', 
                          'beispiel2'=>'Beispiel 2');

  $site = $_GET['site'];
  if (empty($site))
    $site = 'home';

  if (empty($sitemap[ $site ]))
    die('Ungültige Anfrage - Seite nicht vorhanden');

  require('/bin/'.$site.'.php');
  $smarty->assign('template', '/templates/'.$site.'.tpl');
  $smarty->assign('sitetitle', $sitemap[$site]);

  $smarty->display('beispiel.tpl');
?>
Nun fehlen nur noch die Seiten:
PHP:
<?php
  # /bin/home.php
  // leer
?>

<?php
  # /bin/beispiel1.php
  $smarty->assign('foo', 'bar');
?>

<?php
  # /bin/beispiel2.php
  $smarty->assign('bar', 'foo');
?>
Und der HTML-Teil:
HTML:
<!-- /templates/home.tpl -->
Dies ist die leere Startseite

<!-- /templates/beispiel1.tpl -->
Die Variable <i>foo</i> enthält den Wert <b>{$foo}</b>.

<!-- /templates/beispiel2.tpl -->
Und nun enthält die Variable <i>bar</i> den Wert <b>{$bar}</b>.
Damit hätte man das Grundgerüst einer lauffähigen Seite mit Frontcontroller.

Ich hoffe, Ihr könnt damit etwas anfangen und bin auf Kritik/Anregungen/Verbesserungen gespannt. ;)

@theHacker:

Wenn's für gut befunden wird, könntest Du's dann ins FAQ-Forum verschieben? Ich darf da ja nicht posten. :-? ;)

PS: Ich geh nun erstmal was essen. Da tippert man ja doch länger dran als ich dachte... :ugly:
 
Zuletzt bearbeitet:
tleilax schrieb:
@theHacker:
Wenn's für gut befunden wird, könntest Du's dann ins FAQ-Forum verschieben? Ich darf da ja nicht posten. :-? ;)
Wo soll's denn sonst hin ? :clap:

:arrow: Programmierung / FAQ und Archiv

Das mit der XML-Sitemap gefällt mir. Das muss ich irgendwann mal aufgreifen :)
Bei meiner Sitemap erkennt man schön, dass die XML-Struktur da sehr gut passt. Da könnte ich dann z.B. auch noch Infos wie das Symbol etc. rein hauen.
 
Ein kleiner Einschub noch zur Performance:

Man kann das Parsen der Sitemap vereinfachen, indem man diese cachet. Beim Einlesen wird geprüft, ob eine Cache-Datei für die Sitemap vorliegt und dann dieses Dateidatum mit dem der Sitemap verglichen. Ist das Datum der Sitemap neuer, wird neu geparset, ansonsten die gecachete Version eingelesen. Dafür bieten sich die Funktionen serialize()/unserialize() an, womit man ein Array in einen String umwandeln und vice versa.

viele Frameworks oder Klassensammlungen bieten zudem dafür noch die Möglichkeit diese Daten zu cachen.
So bietet zB. das Zend-Framework die Klasse Zend_Cache, welche das Parsen des XML-Files Cachen kann und sollte die Datei geändert werden (das Framework merkt dies von alleine) wird die Datei sofort neu eingelesen und gecached, somit lässt sich die Sitemap gut im XML-Format speichern und trotzdem wird die Sitemap sehr effizient verwaltet und ausgeführt.

Ein Beispiel:
PHP:
<?php
require_once 'Zend/Cache.php';

$frontendOptions = array(
   'master_file' => 'sitemap.xml', // die XML-Datei
   'automatic_serialization' => true //alle nicht primitiven Datenstrukturen werden serializiert
);

// Cache-Objekt erstellen - Daten werden im Arbeitsspeicher gehalten
$cache = Zend_Cache::factory('File', 'Memcached', $frontendOptions);

//Cache auslesen
if($xml = $cache->load('xmlsitemap')){
  //Cache-Datei existiert nicht oder das File wurde verändert

  $xml=doAll(); //das XML-File wird geparsed und von doAll() als Array zurückgegeben
  $cache->save($xml, 'xmlsitemap'); //die Sitemap wird nun gespeichert, damit sie nicht jedesmal neu geparsed werden muss
}
print_r($xml); //$xml steht hier immer zur Verfügung und ist immer gefüllt