Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
FORUM PHP FAQ PHP COURS PHP SOURCES PHP LIVRES PHP SCRIPTS PHP OUTILS PHP COMPARATIFS PHP TV Zend Framework

Le modèle MVC et le contrôleur sous PHP

Date de publication : 28/06/2007 , Date de mise à jour : 26/10/2007

Par Julien Pauli (Tutoriels, article et conférences PHP et developpement web) (Blog)
 

De tous les motifs de conception (Design patterns), le motif MVC est sans doute celui sur lequel il y a le plus à dire.
Je vais cependant rester simple, le but de cet article est de comprendre MVC, et pourquoi MVC ?

I. Introduction
II. Principe du modèle MVC
III. Le frontController ou contrôleur frontal
IV. Motif actionController
V. Conclusions


I. Introduction

Le MVC tout comme l'orientation objet du code, semble être devenu un standard dans le developpement d'applications web, avec la réputation d'être une bonne pratique de conception.
Cependant, trouver une definition exacte et précise du MVC semble impossible, notamment au regard de PHP, langage scripté interprété à chaque requête, à la différence de java avec qui on peut utiliser les threads. De nombreux Frameworks utilisent aujourd'hui MVC, car le but principal de ce motif est de séparer les couches logiques d'une application. Nous allons voir qu'il n'est pas toujours judicieux d'utiliser MVC, ce motif sera plutôt réservé aux sites touffus, dans lesquels beaucoup de redondances de code apparaissent en général, des sites souvent mis à jour, souvent remodelés. Dans le cadre d'une petite application web gentillette, la complexité en terme de code, apportée par MVC, ne sera pas justifiée. En revanche pour tout autre projet web, la séparation en plusieurs couches permet à différentes équipes de bosser chacune sur une couche, indépendamment des autres.
Et c'est aussi un gros avantage lorsqu'une couche doit évoluer, sans que les autres n'en aient besoin.
Voyons déjà un petit code :

<?php
$connect = mysql_connect('myserver', 'mylogin', 'mypassword');
mysql_select_db('myDB');
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
	$news_id = $_POST['news_id'];
	mysql_query("INSERT INTO commentaires SET news_id='$news_id',
                                              auteur='".mysql_escape_string($_POST['auteur'])."',
                                              texte='".mysql_escape_string($_POST['texte'])."',
                                              date=NOW()"
               );
	header("location: ".$_SERVER['PHP_SELF']."?news_id=$news_id");
	exit;
} else {
	$news_id = $_GET['news_id'];
}
?>
<html>
	<head>
		<title>Les news</title>
	</head>
	<body>
	<h1>Les news</h1>
	<div id="news">
		<?php
		$news_req = mysql_query("SELECT * FROM news WHERE id='$news_id'");
		$news = mysql_fetch_array($news_req);
		?>
	<h2><?php echo $news['titre'] ?> postée le <?php echo $news['date'] ?></h2>
	<p><?php echo $news['texte_nouvelle'] ?> </p>
		<?php
		$comment_req = mysql_query("SELECT * FROM commentaires WHERE news_id='$news_id'");
		$nbre_comment = mysql_num_rows($comment_req);
		?>
	<h3><?php echo $nbre_comment ?> commentaires relatifs à cette nouvelle</h3>
		<?php while ($comment = mysql_fetch_array($comment_req)) {?>
			<h3><?php echo $comment['auteur'] ?> a écrit le <?php echo $comment['date'] ?></h3>
			<p><?php echo $comment['texte'] ?></p>
		<?php } ?>
	<form method="POST" action="<?php echo $_SERVER['PHP_SELF'] ?>" name="ajoutcomment">
		<input type="hidden" name="news_id" value="<?php echo $news_id?>">
		<input type="text" name="auteur" value="Votre nom"><br />
		<textarea name="texte" rows="5" cols="10">Saisissez votre commentaire</textarea><br />
		<input type="submit" name="submit" value="Envoyer">
	</form>
	</div>
	</body>
</html>
C'est volontairement simple et simplifié, peu sécurisé, ceci pour demeurer concis et clair, constat : tout le monde connaît un code comme celui-ci, et ça ne choquera personne. PHP est mélangé au sein du HTML, après tout c'est son but premier : langage préprocesseur.
A la lecture du code, on comprend immédiatement ce à quoi il est destiné, c'est l'avantage. On appelle ça un traitement procédural. L'inconvénient, c'est que les couches sont mélangées. Dans le traitement d'informations sur le web, généralement on distingue 3 couches : le modèle qui gère la manière d'accéder aux données, la vue qui se charge de présenter des données, et le contrôle, c'est l'étape intermédiaire de sélection et fusion des données. On peut donc factoriser le code pour bien séparer ces 3 couches, voyez plutôt :
mymodel.php

<?php
function dbconnect()
{
static $connect = null;
if ($connect === null) {
	$connect = mysql_connect('myserver', 'mylogin', 'mypassword');
	mysql_select_db('myDB');
}
return $connect;
}

function get_news($id)
{
$news_req = mysql_query("SELECT * FROM news WHERE id='$news_id'",dbconnect());
return mysql_fetch_array($news_req);
}

function get_comment($news_id)
{
$comment_req = mysql_query("SELECT * FROM commentaires WHERE news_id='$news_id'",dbconnect());
$result = array();
while ($comment = mysql_fetch_array($comment_req)) {
	$result[] = $comment;
}
return $result;
}

function insert_comment($comment)
{
mysql_query("INSERT INTO commentaires SET news_id='{$comment['news_id']}',
                                          auteur='".mysql_real_escape_string($comment['auteur'])."',
                                          texte='".mysql_real_escape_string($comment['texte'])."',
                                          date=NOW()"
            ,dbconnect() );
}
myview.php

<html>
	<head>
		<title>Les news</title>
	</head>
	<body>
	<h1>Les news</h1>
	<div id="news">
	<h2><?php echo $news['titre'] ?> postée le <?php echo $news['date'] ?></h2>
	<p><?php echo $news['texte_nouvelle'] ?> </p>
	<h3><?php echo $nbre_comment ?> commentaires relatifs à cette nouvelle</h3>
		<?php foreach ($comments AS $comment) {?>
			<h3><?php echo $comment['auteur'] ?> a écrit le <?php echo $comment['date'] ?></h3>
			<p><?php echo $comment['texte'] ?></p>
		<?php } ?>
	<form method="POST" action="<?php echo $_SERVER['PHP_SELF'] ?>" name="ajoutcomment">
		<input type="hidden" name="news_id" value="<?php echo $news_id?>">
		<input type="text" name="auteur" value="Votre nom"><br />
		<textarea name="texte" rows="5" cols="10">Saisissez votre commentaire</textarea><br />
		<input type="submit" name="submit" value="Envoyer">
	</form>
	</div>
	</body>
</html>
mycontroller.php

<?php
require ('mymodel.php');
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
	insert_comment($_POST);
	header("HTTP/1.1 301 Moved Permanently"); 
	header("location: {$_SERVER['PHP_SELF']}?news_id={$_POST['news_id']}");
	exit;
} else {
	$news = get_news($_GET['news_id']);
	$comments = get_comments($_GET['news_id']);
	require ('myview.php');
}
?>
Et voilà, on a appliqué le modèle MVC ;-) On a bien séparé le code en 3 couches distinctes, qui se connaissent l'une l'autre. Dans ce code-là, on peut facilement changer la manière d'accéder aux données (changer de SGBD, par exemple), sans pour autant se soucier le moins du monde de la présentation, ou de la manipulation future de ces données. Et nous n'avons pas utilisé la POO ;-).
Voilà l'intérêt principal d'un modèle MVC : séparer les couches de conception, de manière à ce que plusieurs groupes de personnes puissent bosser chacun sur leur couche, sans même qu'ils connaissent les personnes qui bossent la couche du dessous (ou du dessus, ou d'à coté).

Il n'est cependant pas toujours facile de bien distinguer les couches, et le motif MVC reste sans aucun doute un motif complexe, car, nous allons le voir, on peut fortement le complexifier, avec MVC2, mais les développeurs habitués à ce modèle apprécient particulièrement la lisibilité du code.
3 couches sont un minimum, MVC2 en implémente minimum 4, et on peut même en distinguer jusqu'à 5 voire 6 ou plus sur des gros projets. Mais comme toujours, la séparation des entités logiques, réalisable grace à la programmation orientée objet, simplifie et clarifie la conception d'un logiciel (et dans notre cas d'une application web). Il est plus facile de dire de quoi est constituée une vinaigrette, lorsque les ingrédients sont tous posés sur la table, plutot que d'essayer d'analyser leur mélange au fond du saladier. Il n'y a pas à proprement parler de nombre de classes défini, MVC est un design pattern architectural, c'est un ensemble de principes à suivre à l'intérieur duquel un grand nombre d'autres motifs de conception interviennent.
On peut ainsi créer "plein de" motifs MVCs, chacun répondra à un problème particulier. Cependant la plupart du temps, les developpeurs utiliseront des modèles MVC déja écrits, dans des frameworks ayant fait leurs preuves.


II. Principe du modèle MVC

Le but de MVC est de séparer les couches d'une application (en 3 couches distinctes, au minimum, et le plus souvent). On va donc distinguer :

  1. Le modèle qui encapsule la logique métier et la manipulation des sources de données. Ici, des designs patterns peuvent intervenir : TableDataGateway, ActiveRecord ....
  2. La vue : présente les données à l'utilisateur. Ici aussi on peut introduire des motifs : Factory , Composite, TemplateView; par exemple
  3. Le contrôleur : c'est tout le reste, c'est lui qui analyse la requête client, accède aux données, formate le tout, et balance le tout à la partie vue, qui va afficher ça.
On va détailler un peu, la vue d'abord, c'est un TemplateView. On utilise PHP lui-même pour écrire nos variables dans du HTML, ou alors un moteur de template.
Le TransformView peut aussi être utilisé pour sortir en transformant les données renvoyées par les modèles, qui sont en XML, grâce à XSLT et les feuilles de style XSL. Une sortie en PDF peut aussi être utilisée.
Il est très important de noter que la vue ne fait que présenter des données, c'est le modèle qui va fournir toutes les méthodes nécessaires à la présentation. Si par exemple on doit afficher en gras les commentaires datant de moins de 3 jours, le modèle fournira une méthode d'accès à ces commentaires, et la vue comportera une condition de vérification.

Du coté du modèle, il est très important que celui-ci ne connaisse ni les contrôleurs, ni les vues. Les liens vont des contrôleurs vers le modèle, de temps en temps, des vues vers le modèle (on va voir ça), mais jamais dans le sens inverse.
Le modèle étant totalement indépendant du reste, il pourra faire l'objet d'une réflexion et d'un développement à part du reste. Il pourra de même être utilisé par d'autres composants, tels que des webservices.
Il existe plein de méthodes d'accès aux données. Dans la majeure partie des cas, la source de données est une base de données, et le mapping objet relationnel (ORM) pourra être utilisé, ou l'ActiveRecord, ce qui ne fait pas l'objet de cet article.
Il est possible que la vue accède au modèle. C'est un choix architectural qui devra être bien réfléchi, car il impose un plus fort couplage et un mélange des couches. On appelle ça l'approche pull. C'est souvent réservé aux opérations légères, comme par exemple afficher une liste de catégories d'éléments quelconques ; et aux opérations de lecture uniquement.
L'autre approche, à l'inverse, appelée push, interdit à la vue d'accéder au modèle. En général, on utilise les 2 choix en même temps : une instruction de lecture simple (récupérer la liste des membres) sera demandée directement par la vue, ou un de ses sous-composants.
En revanche une opération de modification du modèle, sera demandée par un contrôleur.

Modèle MVC global :

MVC général

Diagramme de séquence d'un MVC simple :

MVC details

Et on arrive enfin au contrôleur, c'est un peu l'objet de cet article, car c'est lui le composant principal du modèle MVC. Il agit avec le modèle, la vue, et d'autres contrôleurs, à la fois.
Quel qu'il soit, le contrôleur reçoit et analyse la requête du client, accède au modèle, et présente la vue.


III. Le frontController ou contrôleur frontal

Il existe plusieurs motifs associés au contrôleur. Le PageController impose un contrôleur par page. C'est le schéma 'logique' qui nous vient à l'esprit, l'exemple montré plus haut implémente cette idée.
De manière simple nous avons séparé les 3 couches, et chaque action à réaliser va se trouver dans un fichier distinct, par exemple post_comment.php , read_new.php ; etc...

Le contrôleur doit donc :
  1. Analyser la requête, soit extraire les informations dans l'URL
  2. Faire une mise à jour du modèle si nécessaire, en lui passant des paramètres
  3. Déterminer la vue à afficher et demander son affichage
Chaque contrôleur regroupe l'ensemble des responsabilités dans un seul fichier, appelé dans l'URL, c'est le motif le plus clair et le plus facile à mettre en œuvre, la POO n'étant pas nécessaire.
Cependant dans des applications qui gagnent un peu en complexité, de la factorisation s'impose, afin de séparer certains rôles :

Le FrontController, ou contrôleur frontal, est un motif qui est destiné à prendre en charge l'analyse de la requête client. Il est donc le seul et unique point d'entrée de l'application, et va orchestrer toute la suite. C'est un singleton et généralement on l'appelle index.php et on lui fournit les actions demandées dans l'URL :
http://monsite.com/index.php?action=show_livres&categorie=x ou pour faire plus joli http://monsite.com/index.php/action?categorie=x
Le site étant en général plus complexe que ça, l'url intègrera aussi ce qu'on appelle un module :
http://monsite.com/index.php?module=bibliotheque&action=show_livres&categorie=x ou pour faire plus joli http://monsite.com/index.php/bibliotheque/show_livres?categorie=x

FrontController est associée au design pattern Command qui va utiliser des objets $request et $response pour représenter les actions. La classe request est en charge de l'analyse de l'URL fournie, elle va déterminer un module, une action, et des paramètres. Le frontController appelle alors le fichier correspondant à l'action (motif Command), et va appeler sa méthode launch(). L'action va être exécutée. A la fin, l'action demande le rendu d'une vue via render(), puis son affichage via printOut(), mais elle peut aussi demander une redirection grâce à redirect(), ou passer la main à une autre action avec forward().
classe Request

<?php
class Request 
{
	public function getParam($key)
	{
		return filter_var($this->getTaintedParam($key), FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
	}
	
	public function getTaintedParam($key)
	{
		if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST[$key])){
			return $_POST[$key];
		}else{
			return $_GET[$key];
		}
	}
	
	public function route()
	{
		$requestUri = substr($_SERVER['REQUEST_URI'],
							 strpos($_SERVER['REQUEST_URI'],'/'.basename(__FILE__)) + 
							 strlen('/'.basename(__FILE__))
							);
		if (empty($requestUri)) return array();
		
		$path = parse_url($requestUri,PHP_URL_PATH);
		preg_match('#^(/(?P<module>\w+))(/(?P<action>\w+)/?)?$#', $path, $matches);
		
		$args = explode('&', parse_url($requestUri,PHP_URL_QUERY));
		
		return $matches;
	}

}
classe Response

<?php
class Response
{
	private $_vars = array();
	private $_headers = array();
	private $_body;
	
	public function addVar($key, $value)
	{
		$this->_vars[$key] = $value;
	}
	
	public function getVar($key)
	{
		return $this->_vars[$key];
	}
	
	public function setVar($key, $value)
	{
	    $this->_vars[$key] = $value;
	}
	
	public function getVars()
	{
		return $this->_vars;
	}
	
	public function setBody($value)
	{
		$this->_body = $value;
	}
	
	public function redirect($url, $permanent = false)
	{
		if ($permanent){
			$this->_headers['Status'] = '301 Moved Permanently';
		}else{
			$this->_headers['Status'] = '302 Found';
		}
		$this->_headers['location'] = $url;
	}
	
	public function printOut()
	{
		foreach ($this->_headers as $key => $value) {
			header($key. ':' . $value);
		}
		echo $this->_body;
	}
}
classe frontController

class frontController
{
	private $_defaults = array('module' => 'index', 'action' => 'index');
	private $_request;
	private $_response;
	private static $_instance = null;
	
	private function __construct()
	{
		$this->_request = new Request();
		$this->_response = new Response();
	}
	
	public static function getInstance()
	{
		if (is_null(self::$_instance)){
			self::$_instance = new self();
		}
		return self::$_instance;
	}
	
	public function dispatch($defaults = null)
	{
		$parsed = $this->_request->route();
		$parsed = array_merge($this->_defaults, $parsed);
		$this->forward($parsed['module'], $parsed['action']);
	}
	
	public function forward($module, $action)
	{
		$command = $this->_getCommand($module, $action);
		$command->launch($this->_request, $this->_response);
	}
	
	private function _getCommand($module, $action)
	{
		if(!file_exists($path = "$module/$action.php")){
			throw new Exception("Commande inconnue $module/$action.php");
		}
		require($path);
		$class = $action.'Action';
		return new $class($this);
	}
	
	public function getResponse()
	{
		return $this->_response;
	}
	
	public function redirect($url)
	{
		$this->_response->redirect($url);
	}
	
	public function render($file)
	{
		$view = new View();
		$this->_response->setBody($view->render($file,$this->_response->getVars()));
	}
}
classe View

class View
{
	public function render($file, $assigns = array())
	{
		extract($assigns);
		ob_start();
		require($file);
		$str = ob_get_contents();
		ob_end_clean();
		return $str;
	}
}
classe Action

abstract class Action
{
	protected $_controller;
	
	public function __construct($controller)
	{
		$this->_controller = $controller;
	}
	
	abstract public function launch(Request $request, Response $response);
	
	public function render($file)
	{
		$this->_controller->render($file);
	}
	
	public function printOut()
	{
		$this->_controller->getResponse()->printOut();
	}
	
	protected function _forward($module, $action)
	{
		$this->_controller->forward($module, $action);
	}
	
	protected function _redirect($url)
	{
		$this->_controller->redirect($url);
	}
}
index (main)

$front = frontController::getInstance()->dispatch();
Le code est déjà plus complexe, on est passé à de l'objet. On le pilotera de cette manière :
une commande quelconque

<?php
	public function launch(Request $request, Response $response)
	{
		$variable = 'essai de variable';
		$response->addVar('unevariable',$variable);
		$this->render(dirname(__FILE__).'/show.php');
		$this->printOut();
	}
}
show.php

<?php
echo $unevariable
?>
C'est rapide, mais c'est destiné à vous faire intégrer ce concept de séparation des rôles, c'est ça qui est important dans MVC.
Il ne sert à rien d'avoir une structure telle que celle-ci, car en fait apache peut aiguiller lui-même les requêtes, grâce au mod_rewrite, et tout le code peut être écrit de manière procédurale.
En fait, le code objet écrit plus haut montre globalement le principe utilisé dans la plupart des frameworks, ceux-ci s'efforcent e