MVC pattern uitgelegd
Inleiding
Het MVC design pattern
Models
Views
Controllers
Router
Register
Andere 'hulp classes' voor de werking
Conclusie en bronnen
Inleiding
In deze tutorial wil ik graag in gaan op het design pattern genaamd MVC. MVC is de afkorting van Model - View - Controller.
Deze tutorial is allereerst bedoeld voor Sitemasters.be. Deze tutorial kan dus niet zonder toestemming van de auteur overgenomen worden.
Het niveau van deze tutorial is moeilijk. Maar zodra je dit een beetje doorhebt dan ben je goed bezig in PHP land.
top
Het MVC design pattern
Zoals in de inleiding al genoemd is de afkorting MVC afgeleid van de drie sleutelwoorden binnen deze pattern: Model - View - Controller. Deze manier van programmeren werd
voor het eerst toegpast in de applicatie SmallTalk. Het MVC biedt u de volgende voordelen:
- Scheiding tussen datamodel, applicatielogica en de applicatie presentatie.
- Hergebruik van code wat tijdbesparing opleverd.
- Gemakkelijk nieuwe applicatie presentaties maken
In deze tutorial gaan we dus kennismaken met het mvc pattern. Maar wat doet het nou precies zal u zich afvragen? Naast het scheiden van de verschillende onderdelen werkt het pattern als volgt.
Het mvc pattern weerspiegelt een wisselwerking tussen de verschillende lagen in uw applicatie aan de hand van een vaste opbouw. Het komt er dus in feite op neer dat
iedere view dat aangepast moet worden dit aan de controller doorgeeft. De controller registreert deze wijziging vervolgens zodat er later makkelijker en sneller een update kan plaatsvinden van de view.
top
Models
De models binnen het mvc pattern vertegenwoordigen het datamodel. Het datamodel is de laag tussen de PHP en de database. Binnen het model komen dus voornamelijk de functies die iets uit een database halen en er weer inzetten,
Het voordeel van dit is dat je makkelijk kan overstappen naar een ander soort database. Het is natuurlijk niet alleen bestemd voor de link tussen de database en de php. Je kan er ook de zogenaamde 'rekenfuncties' inzetten. Maar liever houden we dit voor de database handelingen.
Een voorbeeld van een model kan als volgt zijn:
<?php
/**
* Media Class voor MVC tutorial www.sitemasters.be
* @author Marten van Urk
*
*/
class media {
public $iMedia_ID; //Media id
public $sMedia_Naam; //Media title
public $sMedia_DoP; //Media Date of Publicity
public $sMedia_Genre; //Media Genre
/**
* Construct
* @author Marten van Urk
*
*/
public function __construct() {}
/**
* Get a media object searching on media title
*
* @param string $media_title
* @param Resource $connection
*/
public function getMedia($media_title, $connection) {
$aData = array();
$sQuery = "SELECT * FROM media WHERE media_naam = '" .mysql_real_escape_string($media_title). "'";
/**
* Vanaf dit punt gaan we zelf de foutafhandeling oppakken
*/
$rResult = @mysql_query($sQuery, $connection);
/**
* Foutafhandeling ===> Query Fout vangen
*/
if ($rResult === false) {
throw new Exception('Query (' .$sQuery. ') mislukt: ' . mysql_error());
}
/**
* Foutafhandeling ===> Lege result Array terug gekregen
*/
if (@mysql_num_rows($rResult) == 0) {
throw new Exception('Geen media objecten gevonden met de naam: ' . $media_title, NO_MEDIA);
}
/**
* Als de parser op dit punt komt zijn er geen onvolkomendheden gevonden en kunnen we verder met het script
*/
$aData = @mysql_fetch_assoc($rResult);
$this->iMedia_ID = $aData['media_id'];
$this->sMedia_Naam = $aData['media_title'];
$this->sMedia_DoP = $aData['media_dop'];
$this->sMedia_Genre = $aData['media_genre'];
}
}
?>
Ik gebruik voor iedere database tabel een aparte model. Bovenstaande code zou dus de tabel media kunnen vertegenwoordigen. Ik wil hiermee natuurlijk niet zeggen dat mijn methode het beste is maar wel efficient zoals u later in het voorbeeld zal zien.
top
Views
De view binnen het MVC pattern vertegenwoordigd de presentatie van de applicatie. Dit is dus vergelijkbaar met de User Interface van bijvoorbeeld Microsoft Windows.
Het gebruik van de view valt en staat met het register. De register zal later in deze tutorial behandeld worden. Persoonlijk gebruik ik de view als 'tussenoplossing' voor TemplatePower.
Zo kan ik mijn php en mijn html code gescheiden houden. Een voorbeeld van een view zoals ik die gebruik kan zijn:
<?php
/**
* Include the TemplatePower Class for striping (x)HTML and PHP
*/
include_once('library/templates.php');
/**
* Define a new object of the templatepower class
**/
$Tpl = new TemplatePower('templates/tpl/index.tpl');
/**
* Prepare the page
**/
$Tpl->Prepare();
$Tpl->newBlock('completelist');
$Tpl->assign('media_id', $media_title);
$Tpl->assign('media_name', $media_name);
$Tpl->assign('media_genre', $media_genre);
/**
* Print it to screen
**/
$Tpl->printToScreen();
?>
Als je een beetje op de hoogte bent van TemplatePower zal je onderstaande code niet nodig hebben. Maar voor de mensen die nog niet zo bekend zijn met TemplatePower hieronder de tpl file die in de map templates/tpl/ komt te staan.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Voorbeeld Sitemasters tutorial</title>
</head>
<body>
<div id="outer">
<div id="menulist">
<div class="listitem">{media_name} staat onder het genre {media_genre} in de database onder het id {media_id}</div>
</div>
</div>
</body>
</html>
top
Controllers
Een belangrijk onderdeel in het zogenaamde Web 2.0 is de SEO. Iedereen wil bovenaan de zoeklijsten komen. Een belangrijke stap daartoe is het gebruik van
vriendelijke url's. Een belangrijk hulpmiddel daarbij binnen het MVC kan de controller zijn. Wanneer je de controller samen met de router, welke later zal worden behandeld,
gebruikt kan je zeer simpel de url www.mijndomein.nl/index.php?route=media&media_id=345 omzetten naar www.mijndomein.nl/media/345/.
Een simpele controller 'stuurt' de gebruiker naar de juiste models. Een simpele controller word ook weleens vergeleken met de switch die de meeste programmeurs gebruiken voor
de pagina navigatie. Een simpele controller code kan dus als volgt zijn:
<?php
class controller_Media extends Controller_Base {
function index() {
/**
* Hier voer je een aantal handelingen uit zoals het berekenen van bepaalde waardes.
* Bij ons media database zou het dus als volgt kunnen zijn.
* Let ook op de combinatie met ons model genaamd Media en op de koppeling met de datbase door middel van een sessie!
*/
include_once('models/media.model.php');
$media = new MediaModel();
if (isset($_GET['media_id'])) {
if (is_numeric($_GET['media_id'])) {
/**
* Haal de waardes uit de database. Het model zet deze in de klasse variabelen
* dus kunnen we ze aanroepen met $object_naam->klassevariabelenaam;
*/
$getMedia = $media->getMedia('tiesto', $_SESSION['db']);
$media_naam = $media->sMedia_Naam;
$media_dop = $media->sMedia_DoP;
$media_genre = $media->sMedia_Genre;
$media_ID = $media->sMedia_ID;
/**
* Zet de verschillende waardes in het register zodat we deze in de view kunnen gebruiken
*/
$this->registry['template']->set('media_naam', $media_naam);
$this->registry['template']->set('media_genre', $media_genre);
$this->registry['template']->set('media_id', $media_ID);
$this->registry['template']->set('media_dop', $media_dop);
/**
* Verwijzing naar de view.
* Dit laat de view met de naam wat tussen de haken van show staat.
* Hier is het dus de view media. Deze staat in de map templates en
* heeft de naam <viewnaam>.php
*/
$this->registry['template']->show('index');
}
} else {
/**
* Er is geen media id meegegeven in de url dus toon gewoon de view.
* Hier kan je natuurlijk ook een model functie aanroepen die alle categorien
* laat zien zoals we dat in het voorbeeld gaan doen.
*/
$this->registry['template']->show('index');
}
}
}
?>
top
De router
De router is binnen het MVC pattern niet geheel onbelangrijk. En zeker niet in de toepassing van dit pattern zoals ik deze gebruik. Het belangrijkste wat de router doet binnen de webapplicatie is aan de hand van de url de juiste controller inladen en vervolgens deze controller uitvoeren. Een voorwaarde van dit alles is wel dat alle requests binnen komen op één punt namelijk de index. Op dit punt kan de mod_rewrite module van Apache erg handig zijn. Zie het volgende voorbeeld:
- http://www.mijndomein.nl/media/add/ --> Deze gaat naar de controller Media en de methode add binnen die controller.
Wanneer je simpelweg geen rewrite kan gebruiken binnen je websites ben je op dit moment niet uitgeschakeld. Door het volgende bereik je hetzelfde resultaat:
- http://www.mijndomein.nl/?route=media/add --> Deze gaat naar de controller Media en de methode add binnen die controller.
of
- http://www.mijndomein.nl/index.php?route=media/add --> Deze gaat naar de controller Media en de methode add binnen die controller.
We gaan nu eerst kijken naar het bestand waar alle requests op binnen komen. Namelijk de index.php die in de root van je website staat. Omdat dit bestand bij elk request word aangeroepen is het erg zinvol om hier helemaal bovenaan je sessies te starten door de functie session_start(); aan te roepen. Zo heb je altijd je sessies gestart en heb je je geen zorgen te maken over de sessies. Verder zou je je configuratie file met de database verbinding ook kunnen includen hier. Wat er in ieder geval moet gebeuren hier is het aanmaken van een object van het register class. Deze heb ik standaard in mijn config file staan.
Wat bevat dit bestand nou verder? We maken hier een object aan van een tweetal classes. We beginnen met het aanmaken van een object van de template class. Het is nodig dat deze voor het aanmaken van het andere object komt omdat je deze gebruikt binnen die class. We geven in deze classes het object van de register class mee. Het is dus goed om deze vooraf aan te maken zoals eerder gezegd. Verder geven we hier ook het path naar de controller map aan. Dit door een functie binnen de router class uit te voeren. Een voorbeeld hoe dit bestand er uit zou kunnen zien is als volgt:
<?php
session_start();
/**
* Include het configuratie bestand
*
* Hier word het object voor de register class aangemaakt!
* Verder wordt er een functie aangemaakt om automatisch een class in te laden.
* ***************************************************************************
* define('DIRSEP', DIRECTORY_SEPARATOR);
*
* Functie om classes te laden
*
* @author Marten van Urk
* @param String $class_name
* @return including file of false
* function __autoload($class_name) {
* $filename = strtolower($class_name) . '.php';
* $file = site_path . 'classes' . DIRSEP . $filename;
*
* if (file_exists($file) === false) {
* return false;
* }
*
* include($file);
* }
*
* $registry = new Registry;
* ***************************************************************************
*/
include_once('includes/config.inc.php');
/**
* Laad de template in
*/
$template = new Template($registry);
$registry->set ('template', $template);
/**
* Laad de router in
*/
$router = new Router($registry);
$registry->set('router', $router);
$router->setPath('controllers');
$router->delegate();
?>
Waar alles om draait zijn dus een aantal classes. Al deze classes zullen hierna behandeld worden. De class router wil ik hier wel behandelen. Hierna volgt het voorbeeld van deze class. Lees vooral het commentaar. Deze legt namelijk hoe en wat er gebeurd.
<?php
Class Router {
private $registry;
private $path;
private $args = array();
/**
* Constructor
* Deze zet het register in een klasse variabele
*
* @param Class object $registry
*/
function __construct($registry) {
$this->registry = $registry;
}
/**
* Deze functie zet het controller path
* Hij controleert eerst of de directory bereikbaar is.
* Wanneer dit zo is word het in een klasse variabele gezet voor later gebruik.
*
* @param String $path
*/
function setPath($path) {
$path = trim($path, '/\');
$path .= DIRSEP;
if (is_dir($path) == false) {
throw new Exception ('Invalid controller path: `' . $path . '`');
}
$this->path = $path;
}
/**
* Deze functie word aan het eind aangeroepen.
* Deze analyseert de route, kijkt of het bestand aanwezig is en wanneer dit zo is
* zal er een object van die class aangemaakt worden. Als dat zo is zal word de action
* van de controller aangeroepen. Dit is een methode binnen de controller.
* Dus www.mijndomein.nl/media/add/ is de action add van de controller media
*
*/
function delegate() {
// Analyze route
$this->getController($file, $controller, $action, $args);
// File available?
if (is_readable($file) == false) {
die ('404 Not Found');
}
// Include the file
include ($file);
// Initiate the class
$class = 'Controller_' . $controller;
$controller = new $class($this->registry);
// Action available?
if (is_callable(array($controller, $action)) == false) {
die ('404 Not Found');
}
// Run action
$controller->$action();
}
private function getController(&$file, &$controller, &$action, &$args) {
$route = (empty($_GET['route'])) ? '' : $_GET['route'];
if (empty($route)) { $route = 'index'; }
// Get separate parts
$route = trim($route, '/\');
$parts = explode('/', $route);
// Find right controller
$cmd_path = $this->path;
foreach ($parts as $part) {
$fullpath = $cmd_path . $part;
// Is there a dir with this path?
if (is_dir($fullpath)) {
$cmd_path .= $part . DIRSEP;
array_shift($parts);
continue;
}
// Find the file
if (is_file($fullpath . '.php')) {
$controller = $part;
array_shift($parts);
break;
}
}
if (empty($controller)) { $controller = 'index'; };
// Get action
$action = array_shift($parts);
if (empty($action)) { $action = 'index'; }
$file = $cmd_path . $controller . '.php';
$args = $parts;
}
}
?>
top
Het register
Zoals eerder aangegeven is het register een belangrijk onderdeel. Het register zorgt ervoor dat de juiste view aangeroepen word en dat de waardes die in de controller gezet zijn ook in de view beschikbaar zijn. Een erg belangrijk onderdeel dat nodig is, en op de meeste hosts normaal aanstaat, is de Reflection extensie van PHP. Dit bevat namelijk een aantal functies die het register gebruikt.
We zullen eerst de register class weergeven. Het commentaar in die class bevat de verdere uitleg.
<?php
class Registry implements ArrayAccess {
private $vars = array();
/**
* Zet een variabele
*
* @param String $key
* @param String $var
* @return boolean
*/
function set($key, $var) {
if (isset($this->vars[$key]) == true) {
throw new Exception('Unable to set var `' . $key . '`. Already set.');
}
$this->vars[$key] = $var;
return true;
}
/**
* Haal een waatde op uit het register
*
* @param unknown_type $key
* @return unknown
*/
function get($key) {
if (isset($this->vars[$key]) == false) {
return null;
}
return $this->vars[$key];
}
/**
* Verwijder een variabele uit het register
*
* @param unknown_type $var
*/
function remove($var) {
unset($this->vars[$var]);
}
/**
* Hieronder vind u enkele functies die ArrayAccess mogelijk maakt.
* ArrayAccess zit in de SPL dat staat voor Standaard PHP Library.
*
* Meer info op http://www.php.net/~helly/php/ext/spl/
*
* @param unknown_type $offset
* @return unknown
*/
function offsetExists($offset) {
return isset($this->vars[$offset]);
}
function offsetGet($offset) {
return $this->get($offset);
}
function offsetSet($offset, $value) {
$this->set($offset, $value);
}
function offsetUnset($offset) {
unset($this->vars[$offset]);
}
}
?>
top
Andere 'hulp classes' voor de werking
Nu we de meeste onderdelen hebben behandeld missen we nog een aantal classes die vereist zijn.
Deze classes zullen we hieronder weergeven.
Controller Base bevat de functie index. Deze functie is Abstract. Dit houd in dat deze functie altijd terug dient te komen in een class die deze class 'extend'. Dit komt er op neer dat elke controller die je aanmaakt een method index moet bevatten om fouten te voorkomen.
<?php
abstract class Controller_Base {
protected $registry;
function __construct($registry) {
$this->registry = $registry;
}
abstract function index();
}
?>
Naast deze hebben we ook de class template.php waarvan een object word gemaakt in de config file. Deze class bevat een aantal functions die in de code uitgelegd word.
<?php
Class Template {
private $registry;
private $vars = array();
function __construct($registry) {
$this->registry = $registry;
}
function set($varname, $value, $overwrite=false) {
if (isset($this->vars[$varname]) == true AND $overwrite == false) {
trigger_error ('Unable to set var `' . $varname . '`. Already set, and overwrite not allowed.',
E_USER_NOTICE);
return false;
}
$this->vars[$varname] = $value;
return true;
}
function remove($varname) {
unset($this->vars[$varname]);
return true;
}
/**
* Toon de juiste view.
* Het path is in $path aan te passen. Hier staat het op site_path/templates/
*
* @param String $name
* @return boolean
*/
function show($name) {
$path = site_path . 'templates' . DIRSEP . $name . '.php';
if (file_exists($path) == false) {
trigger_error ('Template `' . $name . '` does not exist.', E_USER_NOTICE);
return false;
}
// Load variables
foreach ($this->vars as $key => $value) {
$$key = $value;
}
include ($path);
}
}
?>
top
Conclusie
Ik hoop met deze tutorial een aanzet te leveren tot het gebruik van MVC. Dit pattern is een goede basis voor elke website en zal op den duur tijd besparend, dus kosten bespared, zijn. Voor vragen en/of opmerkingen kan je hier altijd een reactie plaatsen of een prive bericht sturen.
|