MVC mit PHP

Dieses Tutorial erklärt wie MVC mit PHP verwendet werden kann. Es ist auf keinen Fall als fertiges Framework zu verwenden, es soll nur das Prinzip von Model, View & Controller erklären und eine Möglichkeit aufzeigen, wie es implementiert werden kann und dem Leser helfen, ein Gefühl dafür zu vermitteln, was MVC ist und wie es funktioniert. Eine Übersicht fertiger PHP MVC-Frameworks findet ihr hier oder hier oder hier.

Trotzdem ist es natürlich nicht verboten, den Code hier auf seine eigenen Bedürfnisse anzupassen und zu verwenden.

Grundlagen

Was ist "MVC"?

MVC steht für Model, View, Controller. Eine kurze Antwort auf die Frage, was unter den drei Begriffen verstanden wird, könnte wie folgt lauten:

Nach obenNach oben

Und warum "MVC"?

Eine Typische PHP-Webanwendung, wie sie vor allem von Anfängern programmiert wird, besteht oft aus vielen einzelnen PHP-Dateien. Bei einer kleinen Anwendung, die nur aus einer PHP-Datei besteht, wie z.B. einem Mini-Gästebuch Skript, ist das noch kein Problem, wächst eine Anwendung aber und besteht plötzlich aus 10 oder 20 PHP Seiten, wird sie schnell unübersichtlich und schwer wartbar.

Hier kommt MVC ins Spiel. Unser Ziel ist es, eine übersichtliche Anwendung, die gut wartbar ist zu entwickeln. Wenn unsere MVC- Anwendung fertig ist, wird der Einstiegspunkt nur aus einer Datei bestehen, die wie folgt aussehen wird:

// unsere Klassen einbinden
include('classes/controller.php');
include('classes/model.php');
include('classes/view.php');

// $_GET und $_POST zusammenfasen
$request = array_merge($_GET, $_POST);
// Controller erstellen
$controller = new Controller($request);
// Inhalt der Webanwendung ausgeben.
echo $controller->display();

Das ganze Tutorial gibts natürlich auch zum Download.

Nach obenNach oben

Voraussetzungen

Es gibt da ein paar Dinge, die du beherrschen solltest, wenn du alles in diesem Tutorial verstehen willst:

Wenn du allerdings etwas nicht verstehst, kannst du hier nachfragen. Keine Angst, es gibt keine Blöden Fragen oder so... ;)

Nach obenNach oben

MVC umsetzen

Grundstruktur

Am Anfang erstellen wir mal eine grundlegende Ordnerstruktur. Diese ist nicht gottgegeben, sondern von mir ganz einfach für dieses Projekt so gewählt:

ordnerstruktur

Wenn wir das ganze im Browser aufrufen, rufen wir immer nur die Datei index.html auf und übergeben ihr bestimmte Parameter, die dann bestimmen, welche Seite erscheint.

In die Dateien controller.php, model.php und view.php kommen dann die Klassen für Model, View und Controller, die wir uns im folgenden ansehen.

Nach obenNach oben

Model

Das Model repräsentiert, wie schon gesagt, das Datenmodell, das unserer MVC-Anwendung zugrunde liegt. Meistens werden das wohl eine oder mehrere Datenbanktabellen, XML- oder Textfiles sein.

Unsere Model-Klasse (in einer realen, größeren Anwendung können das auch mehrere Klassen sein, z.B. für jede Datenbanktabelle eine) ist also dazu da, uns Daten zu beschaffen. In unserem Fall bestehen die Daten entweder aus einem einzelnen Blog-Eintrag oder ausmehreren Einträgen für die Übersicht.

<?php
/**
 * Klasse für den Datenzugriff
 */
class Model{

	//Einträge eines Blogs als zweidimensionales Array
	private static $entries = array(
		array("title"=>"Eintrag 1", "content"=>"Ich bin der erste Eintrag."),
		array("title"=>"Eintrag 2", "content"=>"Ich bin der ewige Zweite!"),
		array("title"=>"Eintrag 3", "content"=>"Na dann bin ich die Nummer drei.")
	);

	/**
	 * Gibt alle Einträge des Blogs zurück.
	 *
	 * @return Array Array von Blogeinträgen.
	 */
	public static function getEntries(){
		return self::$entries;
	}

	/**
	 * Gibt einen bestimmten Eintrag zurück.
	 *
	 * @param int $id Id des gesuchten Eintrags
	 * @return Array Array, dass einen Eintrag repräsentiert, bzw. 
	 * 					wenn dieser nicht vorhanden ist, null.
	 */
	public static function getEntry($id){
		if(array_key_exists($id, $this->entries)){
			return self::$entries[$id];
		}else{
			return null;
		}
	}
}
?>

Unser Model ist also eine einfache Klasse mit zwei statischen Methoden, die uns entweder einen oder mehrere Einträge zurückgeben. Eine reale Anwendung mit Datenbank hätte wohl auch Funktionen zum speichern, ändern und löschen im Model, aber wir wollen die ganze Sache ja einfach halten.

Nach obenNach oben

View

Die Aufgabe der View, also der Präsentationsschicht ist es, die Daten, welche sie vom Model bzw. Controller weitergereicht bekommt, aufzubereiten und in HTML-Code zu betten.

Dazu benötigen wir Templates (Vorlagen) für jede View (Ansicht). Die View-Klasse lädt dann das vom Controller vorgegebene Template und gibt es aus.

class View{

	// Pfad zum Template
	private $path = 'templates';
	// Name des Templates, in dem Fall das Standardtemplate.
	private $template = 'default';

	/**
	 * Enthält die Variablen, die in das Template eingebetet 
	 * werden sollen.
	 */
	private $_ = array();

	/**
	 * Ordnet eine Variable einem bestimmten Schl&uuml;ssel zu.
	 *
	 * @param String $key Schlüssel
	 * @param String $value Variable
	 */
	public function assign($key, $value){
		$this->_[$key] = $value;
	}


	/**
	 * Setzt den Namen des Templates.
	 *
	 * @param String $template Name des Templates.
	 */
	public function setTemplate($template = 'default'){
		$this->template = $template;
	}

	/**
	 * Das Template-File laden und zurückgeben
	 *
	 * @param string $tpl Der Name des Template-Files (falls es nicht vorher 
	 * 						über steTemplate() zugewiesen wurde).
	 * @return string Der Output des Templates.
	 */
	public function loadTemplate(){
		$tpl = $this->template;
		// Pfad zum Template erstellen & überprüfen ob das Template existiert.
		$file = $this->path . DIRECTORY_SEPARATOR . $tpl . '.php';
		$exists = file_exists($file);

		if ($exists){
			// Der Output des Scripts wird n einen Buffer gespeichert, d.h.
			// nicht gleich ausgegeben.
			ob_start();
				
			// Das Template-File wird eingebunden und dessen Ausgabe in 
			// $output gespeichert.
			include $file;
			$output = ob_get_contents();
			ob_end_clean();
			
			// Output zurückgeben.
			return $output;
		}
		else {
			// Template-File existiert nicht-> Fehlermeldung.
			return 'could not find template';
		}
	}
}

Ok, das ist jetzt ein Haufen Code auf einmal. Also das ganze nochmal Schritt für Schritt:

Die Templates selbst sehen so aus:

<h1>Willkommen im MVC-Blog!</h1>

<?php
foreach($this->_['entries'] as $entry){
?>

	<h2><a href="?view=entry&id=<?php echo $entry['id'] ?>"><?php echo $entry['title']; ?></a></h2>
	<p><?php echo $entry['content']; ?></p>

<?php
}
?>

Das Template, das alle Einträge enthält (default.php). Im Controller werden wir sehen, dass das Array von Einträgen (aus dem Model) mittels 'assign('entries', $entries)' zugewiesen wird. Die Variable $entries enthält alle Einträge, die im Template in einer Foreach-Schleife durchlaufen werden.

<h1>Willkommen im MVC-Blog!</h1>
<h2><?php echo $this->_['title']; ?></h2>
<p><?php echo $this->_['content']; ?></p>
<a href="?view=deafult">Zur&uuml;ck zur &Uuml;bersicht</a>

Dieses Template ist für einen Einzelnen Eintrag (entry.php). Es enthält zwei Variablen, die im Controller mittels assign zugewiesen werden.

Anmerkung: statt unserer eigenen View-Klasse kann man natürlich auch eine Template-Engine wie z.B. Smarty verwenden.

Nach obenNach oben

Controller

Auch wenn es vielleicht eine etwas stumpfe Übersetzung ist, kann man wohl sagen, der Controller kontrolliert die Anwendung, was angezeigt wird und was mit ihr passiert.

class Controller{

	private $request = null;
	private $template = '';

	/**
	 * Konstruktor, erstellet den Controller.
	 *
	 * @param Array $request Array aus $_GET & $_POST.
	 */
	public function __construct($request){
		$this->request = $request;
		$this->template = !empty($request['view']) ? $request['view'] : 'default';
	}

	/**
	 * Methode zum anzeigen des Contents.
	 *
	 * @return String Content der Applikation.
	 */
	public function display(){
		$view = new View();
		switch($this->template){
			case 'entry':
				$view->setTemplate('entry');
				$entryid = $this->request['id'];
				$entry = Model::getEntry($entryid);
				$view->assign('title', $entry['title']);
				$view->assign('content', $entry['content']);
				break;
				
			case 'default':
			default:
				$entries = Model::getEntries();
				$view->setTemplate('default');
				$view->assign('entries', $entries);
		}
		return $view->loadTemplate();
	}
}

Die Klasse ist eigentlich recht übersichtlich und besteht nur aus 2 Methoden: dem Konstruktor und der Methode zum anzeigen des Inhalts.

Dem Konstruktor werden die Daten, die der Seite mittels GET und POST übergeben wurden, als Array $request übergeben. Wenn dieses Array einen Eintrag $request['view'] enthält, wird dieser Eintrag als neues Template übernommen.

Die Funktion display() erstellt die View und entscheidet dann je nach Template(das im Konstruktor bestimmt wurde) welches Template geladen wird, welche Daten aus dem Model geladen und der View zugewiesen werden. Zu guter letzt wird der Inhalt der View mit der Methode loadTemplate() (siehe oben) zurückgegeben.

Schlussendlich brauchen wir auch noch einen Startpunkt, von dem wir unsere kleine Webapplikation starten und betreten:

Nach obenNach oben

Der Einstiegspunkt

// unsere Klassen einbinden
include('classes/controller.php');
include('classes/model.php');
include('classes/view.php');

// $_GET und $_POST zusammenfasen
$request = array_merge($_GET, $_POST);
// Controller erstellen
$controller = new Controller($request);
// Inhalt der Webanwendung ausgeben.
echo $controller->display();

Als erstes werden alle benötigten Dateien geladen. Dann werden $_GET und $_POST zu einem Array zusammengefasst und dieses dem neuen Controller übergeben. Zum Schluss wird dann der Inhalt ausgegeben.

Nach obenNach oben

Optimierung

Einen kleinen Schönheitsfehler hat unser kleiner MVC-Blog aber noch: Wenn wir z.B. den Titel in 'Willkommen im Blog' ändern wollen, müssen wir alle Templates bearbeiten, soch das ist das was wir uns doch ersparen wollten! Um dieses Problem zu lösen, gibt es jetzt 2 Wege:

Die so lala Lösung

Wir verwenden Templates, die dynamisch Inhalt laden können. Warum also nicht den Titel dynamisch generieren? Das horcht sich logisch an und einmal alle Templates ändern ist auch nicht soooo viel Arbeit, danach kann ich den Blogtitel ändern, sooft ich will. Unsre geänderten Templates sehen jetzt also wie folgt aus:

<h1><?php echo $this->_['blog_title']; ?></h1>

<?php
foreach($this->_['entries'] as $entry){
?>

	<h2><a href="?view=entry&id=<?php echo $entry['id'] ?>"><?php echo $entry['title']; ?></a></h2>
	<p><?php echo $entry['content']; ?></p>

<?php
}
?>

für default.php und für entry.php:

<h1><?php echo $this->_['blog_title']; ?></h1>
<h2><?php echo $this->_['title']; ?></h2>
<p><?php echo $this->_['content']; ?></p>
<a href="?view=deafult">Zur&uuml;ck zur &Uuml;bersicht</a>

Nun ändern wir noch die Methode display() in unserem Controller, indem wir der View einfach noch die Variable 'blog_title' zuweisen:

/**
 * Methode zum anzeigen des Contents.
 *
 * @return String Content der Applikation.
 */
public function display(){
	$view = new View();
	$view->assign('blog_title', "Willkommen im Blog");
	switch($this->template){
		case 'entry':
			$view->setTemplate('entry');
			$entryid = $this->request['id'];
			$entry = Model::getEntry($entryid);
			$view->assign('title', $entry['title']);
			$view->assign('content', $entry['content']);
			break;
			
		case 'default':
		default:
			$entries = Model::getEntries();
			$view->setTemplate('default');
			$view->assign('entries', $entries);
	}
	return $view->loadTemplate();
}

Sodala, fertig. Aber schön wärs schon noch, wenn wir noch eine Fußzeile hinzufügen könnten, oder? Also, wieder alle Templates bearbeiten, wieder den Controller bearbeiten, usw. Wir sehen schon, das ist nicht wirklich das Wahre, also versuchen wir es mit einer anderen Lösung, nennen wir sie

Nach obenNach oben

Die gute Lösung

Eine bessere Lösung ist es, die Templates selbst in einem äußeren Template, dass Titel, Fusszeile, usw. enthält, einzufügen. Dazu brauchen wir zuerst mal ein äußeres Template, von den inneren Templates muss der Titel entfernt werden.

Das äußere Template (theblog.php):

<h1><?php echo $this->_['blog_title']; ?></h1>
<?php echo $this->_['blog_content']; ?>
<hr />
<?php echo $this->_['blog_footer']; ?>

Das Übersichts-Template (default.php):

<?php
foreach($this->_['entries'] as $entry){
?>

	<h2><a href="?view=entry&id=<?php echo $entry['id'] ?>"><?php echo $entry['title']; ?></a></h2>
	<p><?php echo $entry['content']; ?></p>

<?php
}
?>

Das Einzelansicht-Template ( entry.php):

<h2><?php echo $this->_['title']; ?></h2>
<p><?php echo $this->_['content']; ?></p>
<a href="?view=deafult">Zur&uuml;ck zur &Uuml;bersicht</a>

Jetzt müssen wir noch dem Controller die neue View für das äußere Template hinzufügen. Dazu bauen wir den Controller wie folgt um:

class Controller{

	private $request = null;
	private $template = '';
	private $view = null;

	/**
	 * Konstruktor, erstellet den Controller.
	 *
	 * @param Array $request Array aus $_GET & $_POST.
	 */
	public function __construct($request){
		$this->view = new View();
		$this->request = $request;
		$this->template = !empty($request['view']) ? $request['view'] : 'default';
	}

	/**
	 * Methode zum anzeigen des Contents.
	 *
	 * @return String Content der Applikation.
	 */
	public function display(){
		$innerView = new View();
		switch($this->template){
			case 'entry':
				$innerView->setTemplate('entry');
				$entryid = $this->request['id'];
				$entry = Model::getEntry($entryid);
				$innerView->assign('title', $entry['title']);
				$innerView->assign('content', $entry['content']);
				break;
				
			case 'default':
			default:
				$entries = Model::getEntries();
				$innerView->setTemplate('default');
				$innerView->assign('entries', $entries);
		}
		$this->view->setTemplate('theblog');
		$this->view->assign('blog_title', 'Der Titel des Blogs');
		$this->view->assign('blog_footer', 'Ein Blog von und mit MVC');
		$this->view->assign('blog_content', $innerView->loadTemplate());
		return $this->view->loadTemplate();
	}
}

Die Änderungen des neuen Controllers im Detail:

Jetzt haben wir eine einfache und saubere Implementierung des MVC-Patterns.

Nach obenNach oben

Feedback, Download, Quellen und Links

Feedback

Genauso wie bei meinem Scriptaculous-Tutorial gibts in meinem Blog web/code die Möglichkeit Feedback zu hinterlassen, Fragen zu stellen und zu diskutieren.

Nach obenNach oben

Download

Den vollständigen Quellcode des Tutorials gibt in allen 3 Versionen zum Download:

Auch das gesamte Tutorial gibts als .zip und .rar zum Download.

Nach obenNach oben

Quellen und Links

Inspiriert hat mich vor allem das MVC Framework von Joomla1.5.

Weiters gibt es einige interessante Blog-Beiträge und Artikel:

Nach obenNach oben

(C)Copyright Thomas Lemmé. Feedback und Fragen auf meinem Blog web/code.

Übersicht auf tutorials.lemme.at.