Vous êtes sur la page 1sur 7

Fiche technique

Le motif MVC
Contrôleurs et URLs

Une très large majorité des frameworks PHP exploitent le motif MVC. Ce motif
simple en apparence, peut être appliqué de bien des façons. Nous allons
explorer ensemble la couche contrôleur, en examinant des possibilités concrètes
d'implémentation.

nécessitait pas de modifier la logique métier


Cet article explique : Ce qu'il faut savoir : de l'application.
• Différentes possibilités d'implémentation de la • Notions de base de l'orienté objet en PHP. Le MVC divise donc le code d'une applica-
couche contrôleur, en insistant sur la relation tion selon 3 responsabilités :
avec les URLs correspondantes.
• Modèle : encapsule la logique métier, l'ac-
cès et la manipulation des sources de don-
nées;
fier complètement la présentation (le HTML), • Vue : présente à l'utilisateur les données
ou au contraire modifier l'accès aux données obtenues à partir du modèle;
Niveau de difficulté (en changeant de SGBD par exemple) sans • Contrôleur : interprète les requêtes de
toucher au reste du code. Sans le savoir, nous l'utilisateur, interagit avec le modèle et sé-
avons appliqué le modèle MVC. Vous remar- lectionne la vue à utiliser.
querez que nous n'avons pas utilisé l'orienté

S
'il est un point commun aux très nom- objet : il est tout à fait possible d'appliquer ce Détaillons maintenant les 2 responsabilités les
breux frameworks web disponibles en modèle en conservant un code uniquement plus faciles à appréhender. La vue tout d'abord,
PHP, c'est bien l'emploi du motif MVC fonctionnel. représente la réponse de l'application à une re-
ou Modèle-Vue-Contrôleur. De Symfony au Comme vous le constaterez assez vite, il n'est quête. C'est une représentation de l'état du
Zend Framework en passant par CakePHP pas toujours facile d'appliquer les principes du modèle à un instant. Deux stratégies existent
et CodeIgniter, tous revendiquent leur affi- MVC avec succès : cerner les différentes res- pour implémenter la vue. Le motif Template
liation au MVC. Nous allons voir que si ces ponsabilités du code afin de bien le comparti- View est bien sûr le plus courant : soit on in-
frameworks diffèrent de façon importante menter est loin d'être une tâche aisée. Toutefois, tègre du PHP à une page HTML, soit on utili-
dans leur implémentation des couches Vue l'expérience aidant, vous apprécierez très vite de se un des nombreux moteurs de template dis-
ou Modèle, leur approche du Contrôleur est travailler dans ce cadre là et vous en constaterez ponibles, comme Smarty ou Savant pour ne ci-
assez semblable, et tire profit de plusieurs les bienfaits sur la lisibilité et la facilité de main- ter que les plus connus. L'autre possibilité est
années d'expérimentations diverses dans ce tenance de votre code. d'utiliser le motif Transform View, ce qui se fait
domaine. N'espérez d'ailleurs pas trouver dans cet ar- le plus souvent en faisant générer des données
ticle une méthode idéale d'implémentation du sous forme de XML par le modèle, et en ap-
Le MVC par l'exemple MVC : cela n'existe pas. On ne peut pas relier pliquant une feuille de style XSLT à ces don-
Nous avons tous débuté en PHP par des pages le MVC à un ensemble fini de classes de base, nées. On peut également utiliser le motif Cus-
ressemblant à l'exemple 1. Après tout, à l'ori- comme dans la plupart des design patterns. Plus tom Tag, dans lequel votre moteur de rendu
gine PHP était fait pour ça : intégrer de la lo- qu'un motif de conception, le MVC est un mo- HTML s'appuie sur des tags XML spécifiques
gique dans le HTML des pages pour les rendre dèle d'architecture, un ensemble de principes à placés dans le template pour intégrer des mor-
dynamiques. Cette approche peut fonctionner suivre, dans lequel un grand nombre de motifs ceaux de HTML dynamiques.
pour des sites très simples, mais dès lors que le de conception peuvent s'appliquer. Ce qu'il est important de comprendre est
nombre de pages s'accroit, la maintenance de- que la vue ne doit en aucun cas contenir de la
vient vite un enfer. Il est pourtant très simple Principes du MVC logique métier, mais uniquement de la logique
de refactoriser ce code d'une façon plus pro- Le motif MVC est en fait apparu dans un de présentation (et vice-versa). Si par exemple
pre, comme l'illustre l'exemple 2 : un fichier framework développé par Trygve Reenskaug nous souhaitons afficher certains objets en
contenant la logique d'accès aux données, un pour le langage Smalltalk à la fin des années rouge dans une page (par exemple des comptes
fichier HTML avec un tout petit peu de lo- 70. Le but de ce framewok était d'isoler le co- bancaires avec un solde négatif), le modèle doit
gique de présentation, et un fichier faisant le de régissant l'interface graphique du code de fournir un moyen de déterminer les comptes en
lien entre les 2. Le code devient plus facile à l'application proprement dit. Ainsi, un chan- question, et c'est dans la vue qu'une condition
lire et à maintenir, et on peut tout à fait modi- gement au niveau de l'interface graphique ne permettra de les afficher en rouge.

1 3/2007 (21)
MVC

Le modèle quand à lui est le coeur même de Le motif PageController en extraire les informations nécessaires à
l'application. C'est lui qui a un accès direct aux Dans le Listing 2, nous avons appliqué le mo- l'exécution de l'action demandeé (dans no-
sources de données, et les manipule en appli- tif PageController : un contrôleur par page de tre exemple, il s'agit de déterminer si l'appel
quant les règles métier. Il doit toujours rester notre application. Ses responsabilités sont les de la page s'est fait par la méthode POST ou
indépendant du contrôleur et de la vue, car suivantes : analyser la requête, c'est à dire non), mettre à jour le modèle si nécessaire,
c'est à cette seule condition qu'il pourra être
réutilisable dans d'autres contextes, comme
Listing 1. Exemple de ce qu'il ne faut pas faire ;)
par exemple si l'application doit subir une re-
fonte graphique, ou que l'ordre de visualisation <?php
des pages doit changer. Cela permet également $connexion = mysql_connect ("localhost", "root", "password");
aux développeurs en charge de cette partie de mysql_select_db("article_mvc");
l'application de ne se préoccuper que de la logi- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
que métier, et de plus le modèle est de ce fait $post_id = $_POST['post_id'];
beaucoup plus facile à tester. Du point de vue mysql_query("INSERT INTO comments SET post_id = '$post_id', author = '{$_
de l'implémentation, de nombreux motifs de POST['author']}',
conception et outils d'ORM (Object Relational content = '{$_POST['content']}', created_on = NOW()");
Mapping) sont dédiés à la couche modèle, mais header("location: {$_SERVER['PHP_SELF']}?post_id=$post_id");
cela sort du cadre de cet article. Vous pouvez die();
vous référer à mon article du précédent nu- } else {
méro, Implémentation du motif ActiveRecord $post_id = $_GET['post_id'];
en PHP5, pour avoir un aperçu de quelques uns }
de ces motifs. ?>
Arrêtons nous un moment sur les relations <html>
possibles entre la vue et le modèle. Nous avons <head>
dit que le modèle doit être indépendant du con- <title>Mon blog</title>
trôleur et de la vue, mais ces derniers dépen- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
dent malgré tout du modèle, puisqu'ils doivent </head>
pouvoir y accéder pour récupérer des données. <body>
Deux conceptions s'affrontent alors : une pre- <div id="conteneur">
mière stratégie est que le contrôleur demande <div id="header">
les données à afficher au modèle et les fait pas- <h1>Mon blog</h1>
ser à la vue. Dans ce cas, la vue n'a pas de relation </div>
directe avec le modèle : c'est une approche push, <div id="contenu">
les données sont poussées dans le template par <?php
le contrôleur. L'autre stratégie, l'approche pull, $rs = mysql_query("SELECT * FROM posts WHERE id = '$post_id'");
consiste à laisser la vue faire appel directement $post = mysql_fetch_array($rs);
au modèle pour récupérer les données dont elle ?>
a besoin. Bien sûr, elle ne doit pas appeler de <h2><?php echo $post['title']; ?></h2>
méthodes du modèle pouvant modifier l'état <strong>Le <?php echo $post['created_on']; ?></strong>
de ce dernier, car tel est le rôle du contrôleur. <p><?php echo $post['content']; ?></p>
Les 2 stratégies se valent, mais attention car l'ap- <?php
proche pull a tendance à recoupler la vue et le $rs = mysql_query("SELECT * FROM comments WHERE post_id = '$post_id'");
modèle, et le développeur a vite fait de déraper $comments_count = mysql_num_rows($rs);
et de décharger le contrôleur de ses responsabi- ?>
lités. De plus, un changement d'API du modèle <h3><?php echo $comments_count; ?> commentaire(s)</h3>
peut casser des vues existantes. On lui préfèrera <?php while ($comment = mysql_fetch_array($rs)) { ?>
donc l'approche push ou le découplage est total. <h5><?php echo $comment['author']; ?>, le <?php echo $comment['created_on'];
Vous voici donc maintenant confrontés à ?></h5>
la vraie problématique de cet article : le con- <p><?php echo $comment['content']; ?></p>
trôleur. Il est en effet le plus difficile à cerner, <?php } ?>
principalement parce que ce terme de contrô- <form name="add-comment" method="post" action="
leur englobe différentes significations dans plu- <?php echo $_SERVER['PHP_SELF']; ?>">
sieurs motifs. Dans un premier temps, nous dé- <input type="hidden" name="post_id" value="<?php echo $post['id'];?>"/>
finirons donc simplement le rôle du contrôleur, <input type="text" name="author" size="40" maxlength="50"
quelle que soit sa forme, de la façon suivante : value="Votre nom" /><br />
le contrôleur reçoit et analyse les requêtes de <textarea name="content" rows="5">Vos commentaires</textarea><br />
l'utilisateur, a la responsabilité d'appeler les mé- <input name="comment_submit" type="submit" value="Envoyer" />
thodes du modèle susceptibles de modifier son <input type="reset" value="Effacer" />
état, et sélectionne la vue à afficher. </form>
Martin Fowler, dans son ouvrage de référence </div>
Patterns of Enterprise Application Architecture, </div>
présente plusieurs motifs de conception relatifs </body>
à la couche contrôleur ; nous allons les examiner </html>
en détail.

www.phpsolmag.org 2
Fiche technique

en lui fournissant les données issues de la le MVC modèle 1. Ce motif fonctionne très le plus souvent un fichier index.php chargé
requête (ici en appelant la fonction insert_ bien dans les cas les plus simples, mais dès d'instancier le FrontController. L'action à réa-
comment() avec les données dans $_POST), que l'application devient plus complexe, des liser (ou la page à afficher) sera donc fourni
et déterminer la vue à afficher, en lui faisant redondances commencent à apparaître dans en paramètre dans l'url. Par exemple, l'url de
passer les données issues du modèle. Les Pa- le code. notre exemple 2 deviendrait : http://localhost/
gesControllers regroupent donc l'ensemble des index.php?action=show_post&post_id=n
responsibilités de la couche contrôleur, ce Le motif FrontController ou bien, si nous souhaitons mieux struc-
qui en fait le motif le plus simple à mettre en Pour éviter ce problème de redondances, turer notre application : http://localhost/inde
oeuvre dans le cadre du MVC : un contrôleur l'étape suivante selon Martin Fowler est de x.php?module=blog&action=show_post&post_
par page, appellé directement dans l'url. Nous confier la responsabilité de la prise en charge id=n.
n'avons même pas besoin d'utiliser l'orienté et de l'analyse de la requête à un composant Ce motif s'associe normalement au motif
objet pour cela ! Nous pouvons d'ailleurs re- spécialisé : le FrontController. Ce composant Command, qui utilise des objets pour re-
marquer que le code du contrôleur pourrait sera donc l'unique point d'entrée de notre présenter des action. Une grande partie de
tout à fait être placé dans le même fichier que application, et décidera de la suite des évè- la logique de nos PageControllers se retrouve
la vue, ce qui dans le monde Java est appellé nements. En pratique, ce point d'entrée sera encapsulée dans des sous-classes de la classe

Listing 2. L'exemple 1 à la sauce MVC

URL : http://localhost/show_post.php?post_id=n <div id="header">


// fichier blog_model.php <h1>Mon blog</h1>
function db_connection() { </div>
static $conn = null; <div id="contenu">
if ($conn === null) { <h2><?php echo $post['title']; ?></h2>
$conn = mysql_connect ("localhost", "root", "password"); <strong>Le <?php echo $post['created_on']; ?></
mysql_select_db("article_mvc"); strong>
} <p><?php echo $post['content']; ?></p>
return $conn; <h3><?php echo count($comments); ?>
} commentaire(s)</h3>
function get_post($id) { <?php foreach ($comments as $comment) { ?>
$rs = mysql_query("SELECT * FROM posts WHERE id = '$id'", <h5><?php echo $comment['author']; ?>, le <?php
db_connection()); echo $comment['created_on']; ?></h5>
return mysql_fetch_array($rs); <p><?php echo $comment['content']; ?></p>
} <?php } ?>
function get_comments($post_id) { <form name="add-comment" method="post"
$rs = mysql_query("SELECT * FROM comments WHERE post_id = action="<?php echo $_SERVER['PHP_SELF'];
'$post_id'", db_connection()); ?>">
$comments = array(); <input type="hidden" name="post_id" value="<?php
while ($row = mysql_fetch_array($rs)) { echo $post['id']; ?>" />
$comments[] = $row; <input type="text" name="author" size="40"
} maxlength="50" value="Votre nom" /><br
return $comments; />
} <textarea name="content" rows="5">Vos
function insert_comment($comment) { commentaires</textarea><br />
// oui je sais qu'on doit utiliser mysql_real_escape_string <input name="comment_submit" type="submit"
;) value="Envoyer" />
$sql = "INSERT INTO comments SET post_id = '".mysql_escape_ <input type="reset" value="Effacer" />
string($comment['post_id'])."', </form>
author = '".mysql_escape_string($comment['author']) </div>
."', </div>
content = '".mysql_escape_string($comment['content'] </body>
)."', </html>
created_on = NOW()"; // fichier show_post.php
mysql_query($sql, db_connection()); include('blog_model.php');
} if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// fichier show_post_view.php insert_comment($_POST);
<html> header("location: {$_SERVER['PHP_SELF']}?post_id={$_
<head> POST['post_id']}");
<title>Mon blog</title> die();
<meta http-equiv="Content-Type" content="text/html; } else {
charset=utf-8" /> $post = get_post($_GET['post_id']);
</head> $comments = get_comments($_GET['post_id']);
<body> include('show_post_view.php');
<div id="conteneur"> }

3 3/2007 (21)
MVC

Command. Les Listings 3 et 4 vous fournissent La classe Request analyse l'url fournie par dant à la commande, instancie la sous-classe
un exemple d'implémentation. Le modèle uti- l'utilisateur, ce qui nous permet de détermi- de Command, et appelle sa méthode execute().
lisé n'est pas détaillé, car c'est le contrôleur ner le module et la commande souhaitée. Le Une fois l'action effectuée, la commande
qui nous intéresse ! FrontController appelle le fichier correspon- appelle le rendu d'une vue (via la méthode

Listing 3. Implémentation d'un FrontController en PHP (et classes associées)

class Request { "{$url}\">redirected</a>.</body></


public function getParam($key) { html>";
return filter_var($this->getTaintedParam($key), FILTER_ }
SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_ public function out() {
QUOTES); foreach($this->headers as $key => $value) {
} header($key.': '.$value);
public function getTaintedParam($key) { }
if ($this->getRequestMethod() == 'POST' && isset($_ echo $this->body;
POST[$key])) { }
return $_POST[$key]; }
} else { class FrontController {
return $_GET[$key]; private $defaults = array('module' => 'home', 'action' =>
} 'index');
} private $request;
public function getMethod() { private $response;
return $_SERVER['REQUEST_METHOD']; public function __construct() {
} $this->request = new Request();
public function parseUri() { $this->response = new Response();
$requestUri = substr($_SERVER['REQUEST_URI'], }
strlen(str_replace('/index.php', '/', public function dispatch($defaults = null) {
$_SERVER['SCRIPT_NAME']))); $recognized = $this->request->parseUri();
if (empty($requestUri)) return array(); $recognized = array_merge($this->defaults, $recognized);
if (strpos($requestUri, '?') !== false) { $this->forward($recognized['module'],
list($path, $queryString) = explode('?', $requestUri); $recognized['action']);
} else { }
list($path, $queryString) = array($requestUri, ''); public function forward($module, $action) {
} $command = $this->getCommand($module, $action);
if (substr($path, -1) == '/') $path = substr($path, 0, $command->execute($this->request, $this->response);
-1); }
preg_match('#^(?P<module>\w+)(/(?P<command>\w+))?(/ public function render($file) {
?(?P<id>\w+))?$#', $path, $matches); $view = new View();
if (isset($matches['id'])) $_GET['id'] = $matches['id']; $this->response->setBody($view-> render($file, $this-
return $matches; >response-> getVars()));
} }
} public function redirect($url) {
class Response { $this->response->redirect($url);
private $assigns = array(); }
private $headers = array(); public function getResponse() {
private $body; return $this->response;
public function addVar($key, $value) { }
$this->assigns[$key] = $value; private function getCommand($module, $action) {
} if (!file_exists($path = "$module/$action.php")) {
public function getVars() { return new UnknownCommand($this);
return $this->assigns; }
} require($path);
public function setBody($body) { $class = $action.'Command';
$this->body = $body; return new $class($this);
} }
public function redirect($url, $permanently = false) { }
if ($permanently) { class View {
$this->headers['Status'] = '301 Moved Permanently'; public function render($file, $assigns = array()) {
} else { extract($assigns);
$this->headers['Status'] = '302 Found'; ob_start();
} include ($file);
$this->headers['location'] = $url; $str = ob_get_contents();
$this->body = "<html><body>You are being <a href=\ ob_end_clean();

www.phpsolmag.org 4
Fiche technique

render()), demande une redirection HTTP Response. D'autre part, la classe Request nous ne peut totalement apporter à l'aide de solu-
(via redirect()), ou fait suivre à une autre permet d'utiliser des URLs plus jolies, du type : tions basées sur mod_rewrite.
commande (via forward()). http://localhost/module/command/id Pour en revenir à Martin Fowler, celui-ci dis-
Vous remarquerez immédiatement, je Toutefois, l'implémentation que je vous pro- tingue un autre intérêt possible à l'implémenta-
l'espère, que l'approche n'est plus du tout la pose ne va pas encore assez loin pour justifier tion d'un FrontController : en lui associant le mo-
même que dans nos précédents exemples : on la complexité qu'elle apporte. Le support des tif InterceptingFilter, on peut plus facilement gérer
passe d'un code procédural à une approche URLs propres par exemple, pourrait assez aisé- le problème de l'authentification ou du logging :
orientée objet, et la complexité du code uti- ment être ajouté grâce au mod_rewrite d'Apa- puisque nous avons du code qui doit être exécuté
lisé croit d'une manière significative. Et c'est che, et avec des performances bien supérieures à chaque requête, cela fait du FrontController un
là le vrai problème du motif FrontController en ! En pratique, je pense que qu'il faut avoir à bon candidat pour intégrer ce code.
PHP : dans de nombreux cas le jeu n'en vaut sa disposition un vrai composant de routage,
pas la chandelle et on a tendance à réinventer comme celui que propose le Zend Framework, interface Filter {
la roue en s'efforçant d'appliquer ce motif tel pour commencer à trouver un intérêt à centra- public function preFilter();
qu'il a été décrit dans la littérature. En effet, liser ainsi la logique associée au traitement des public function postFilter();
ce motif a été appliqué initialement dans le requêtes : }
monde Java, où les contraintes sont totalement class FrontController {
différentes. Examinons l'interface d'une classe $router = new Zend_Controller_ ...
Command en Java : Router_Rewrite(); private $filtersChain = array();
$router->addRoute('user', new ...
class Command... Zend_Controller_Router_ public function addFilter($filter) {
public void init(ServletContext Route(':controller/:action')); $this->filtersChain[] = $filter;
context, HttpServletRequest $router->addRoute('user', new }
request, HttpServletResponse Zend_Controller_Router_ public function dispatch($defaults =
response)... Route('user/:username')); null) {
public void process()... $router->addRoute('archive', ...
new Zend_Controller_Router_ foreach ($this->filtersChain as
Pour initialiser une sous-classe de Command , on Route('archive/:year', array('year' $filter) {
doit notamment lui passer en argument une => 2006), array('year' => '\d+'))); $filter->preFilter();
instance de la classe HttpServletRequest . }
La raison en est qu'en Java, plusieurs requê- Par ce type de code, la classe Zend _ $this->forward($recognized[
tes peuvent être servies par une même ins- Controller _ Router nous permet non seu- 'module'], $recognized
tance de notre sous-classe de Command : cha- lement de faire de la reconnaissance d'URLs ['action']);
que requête a son propre thread, mais tous complexes, mais également de générer auto- foreach (array_reverse($this->
les threads partagent le même espace mémoi- matiquement les URLs de notre application filtersChain) as $filter) {
re, et donc nos classes doivent être thread-sa- dans les templates, ce qui nous permet de mo- $filter->postFilter();
fe. On utilise donc les paramètres et les re- difier l'architecture de nos URLs sans avoir à }
tours de méthodes pour passer l'information aller modifier de nombreux templates. Nous }
à la commande et l'en faire sortir. D'ailleurs avons donc une vraie valeur ajoutée, que l'on }
HttpServletRequest fait partie intégrante de
J2EE et non d'un framework quelconque.
Listing 4. Implémentation des sous-classes de Command relatives à notre exemple précédent
Le fonctionnement de PHP est totalement
différent, puisqu'il recrée son environnement class ShowPostCommand extends Command {
à chaque requête ! Et c'est pour cela que PHP public function getRequestMethod() {
peut nous fournir les variables super-globales return METHOD_GET;
$_GET, $_POST ou $_SESSION. Une implémenta- }
tion trop stricte de ce motif, comme l'ont fait les public function execute($request, $response) {
premiers frameworks PHP comme php.MVC, $response->setVar('post', PostDAO::findById($request->getParameter('id')));
Phrame ou Eocene, largement inspirés du fra- $response->setVar('comments', CommentDAO::findByPostId($request-
mework Java Struts, n'a donc pas toujours de >getParameter('id')));
sens en PHP. En fait, on peut même arguer que $this->render('show_posts.php');
le FrontController est déjà fourni par Apache et }
le moteur de PHP ! }
Pour autant, ce motif peut tout de même class AddCommentCommand extends Command {
avoir un intérêt : pour cela, il faut que l'implé- public function getRequestMethod() {
mentation des classes Request et Response par return METHOD_POST;
exemple, présente des fonctionnalités à valeur }
ajoutée : c'est ce que j'ai voulu vous montrer en public function execute($request, $response) {
implémentant d'un côté l'assainissement des CommentDAO::insert($request->getParameter('post_id'),
paramètres de la requête directement dans la $request->getParameter('author'),
classe Request, en utilisant l'extension PECL $request->getParameter('content'));
filter (vous remarquerez que l'on dispose tout $controller->redirect('/blog/showPost/'.$request->getParameter('post_id'));
de même de la méthode getTaintedParam }
pour récupérer les paramètres non filtrés), }
et la gestion des en-têtes HTTP dans la classe

5 3/2007 (21)
MVC

Dans cet exemple d'implémentation, on dé- pour ne pas montrer des messages d'erreur rendu ou redirection n'a été demandé par
finit une interface Filter que nos filtres de- critiques à l'utilisateur. l'action elle-même. Notez enfin que nous uti-
vront donc implémenter, avec 2 méthodes Nous avons ajouté à la classe lisons l'API de Réflexion de PHP pour vérifier
preFilter et postFilter, à appeler respecti- ActionController la possibilité de deman- que l'action demandée dans l'URL existe bien
vement avant et après l'exécution de la com- der automatiquement le rendu d'une vue dans le contrôleur, et surtout qu'elle est bien
mande. portant le même nom que l'action si aucun publique.
Mais attention : là encore PHP nous fournit
des solutions simples pour faire exactement Listing 5. L'exemple 1 avec Symfony
la même chose : en utilisant les directives du
php.ini auto_prepend_file et auto_append_ class blogActions extends sfActions {
file, on peut charger des scripts automatique- public function executeIndex() {
ment avant et après l'inclusion de notre Page- // affichage des derniers billets
Controller, réglant notre problème du même }
coup ! public function executeShow() {
Pour terminer cette analyse critique du $this->post = PostPeer::retrieveByPk($this->getRequestParameter('id'));
motif FrontController, il convient de signaler $this->forward404Unless($this->post);
que le motif Command, tout comme le Page- }
Controller, devient vite gênant lorsque l'ap- public function executeAddComment() {
plication et en particulier les relations entre if ($this->getRequest()->getMethod() == sfRequest::POST) {
les différentes pages devient plus complexe. $post_id = $this->getRequestParameter('post_id');
Avoir une image claire du fonctionnement $comment = new Comment();
d'un ensemble de pages devient difficile, et là $comment->setPostId($post_id);
encore, des redondances dans le code peuvent $comment->setAuthor($this->getRequestParameter('author'));
apparaître. $comment->setContent($this->getRequestParameter('content'));
$comment->save();
Les classes ActionController return $this->redirect('blog/show?id='.$post_id);
Pour répondre à ce problème, les déve- }
loppeurs des récents frameworks Symfony }
ou Zend Framework ont appliqué la même re- }
cette que RubyOnRails : regrouper les actions // fichier showSuccess.php
concernant un même type d'entités dans une <html>
seule classe. Vous trouverez dans l'exemple 5 <head>
un exemple d'implémentation de notre exem- <title>Mon blog</title>
ple initial avec le framework Symfony. Là en- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
core, le modèle utilisé n'est pas détaillé car </head>
hors-sujet. <body>
Vous remarquerez que les actions sont main- <div id="conteneur">
tenant des méthodes publiques dont le nom est <div id="header">
précédé de execute. On ainsi une meilleure vi- <h1>Mon blog</h1>
sibilité du code applicatif de chaque partie de </div>
son application. D'autre part, vous remarque- <div id="contenu">
rez que dans la méthode executeShow(), nous <h2><?php echo $post->getTitle(); ?></h2>
instancions une propriété $this->post non <strong>Le <?php echo $post->getCreatedOn(); ?></strong>
déclarée. <p><?php echo $post->getContent(); ?></p>
En effet, c'est ainsi que l'on assigne des va- <h3><?php echo count($post->getComments()); ?> commentaire(s)</h3>
riables au template avec Symfony. Le contenu <?php foreach ($post->getComments() as $comment) : ?>
de la propriété $this->post se retrouve acces- <h5><?php echo $comment->getAuthor(); ?>, le <?php echo $comment-
sible automatiquement dans le template sous >getCreatedOn(); ?></h5>
la forme de la variable $post. Ce principe a <p><?php echo $comment->getContent(); ?></p>
été conservé dans l'exemple d'implémentation <?php endforeach; ?>
d'une super-classe ActionController présenté <?php echo form_tag('blog/addComment') ?>
dans le Listing 6. <input type="hidden" name="post_id" value="<?php echo $post->getId();
Nous utilisons pour cela les méthodes magi- ?>" />
ques __get() et __set(). Notez bien que nous <input type="text" name="author" size="40" maxlength="50" value="Votre
réutilisons les classes Request, Response et nom" /><br />
View vues dans le Listing 4. <textarea name="content" rows="5">Vos commentaires</textarea><br />
Certaines responsabilités du Front- <input name="comment_submit" type="submit" value="Envoyer" />
Controller se retrouvent intégrées <input type="reset" value="Effacer" />
dans la classe ActionController, et le </form>
FrontController se trouve finalement ré- </div>
duit à un rôle d'encapsulation de l'exécu- </div>
tion du code : le bloc try/catch nous permet </body>
d'attraper les exceptions éventuelles, et de </html>
demander le rendu d'une page 404 ou 500

www.phpsolmag.org 6
Fiche technique

Listing 6. Implémentation d'un ActionController


class FrontController { $this->$action();
public function dispatch() { // lots of stuff after...
try { if (!$this->performed) {
$request = new Request(); $this->render($this->request->getParam('action').'.p
$response = new Response(); hp');
ActionController::factory($request, $response)->out(); }
} catch (Exception $e) { return $this->response;
ActionController::rescue($request, $response, $e)- }
>out(); public function processWithException($e) {
} if (in_array(get_class($e), array('UnknownControllerExcep
} tion', 'UnknownActionException'))) {
} $this->render('404.php');
class ActionController { } else {
private $request; $this->render('500.php');
private $response; }
private $performed; return $this->response;
public static function factory($request, $response) { }
if (!file_exists($path = 'controllers/'.$request->getPara public function render($file) {
m('controller'))) { if ($this->performed) {
throw new UnknownControllerException(); throw Exception('Un rendu ou une redirection a déjà
} été effectué');
require_once($path); }
$className = $request->getParam('controller').'Control $view = new View();
ler'; $this->response->setBody($view->render($file, $this-
$controller = new $className($request, $response); >response->getVars()));
return $controller->process(); $this->performed = true;
} }
public static function rescue($request, $response, $e) { public function redirect($url) {
$controller = new ActionController($request, $reponse); if ($this->performed) {
return $controller->processWithException($e); throw Exception('Un rendu ou une redirection a déjà
} été effectué');
public function __construct($request, $reponse) { }
$this->request = $request; $this->response->redirect($url);
$this->response = $response; $this->performed = true;
$this->performed = false; }
} private function actionExists($action) {
public function __get($name) { try {
return $this->response->getVar($name); $method = new ReflectionMethod(get_class($this),
} $action);
public function __set($name, $value) { return ($method->isPublic() && !$method-
$this->response->setVar($name, $value); >isConstructor());
} }
public function process() { catch (ReflectionException $e) {
$action = $this->request->getParam('action'); return false;
if (!$this->actionExists($action)) { }
throw new UnknownActionException(); }
} }
// lots of stuff before...

Conclusion 3 couches du MVC, seule la couche modèle mé- deviennent par la suite un problème, il sera tou-
Nous venons de survoler ensemble quelques mo- rite vraiment dans de nombreux cas une appro- jours temps d'exploiter les ressources de PHP et
tifs de conception basiques pour l'implémenta- che orientée objet, ce que Martin Fowler appelle d'Apache pour améliorer la situation.
tion d'une couche contrôleur. Bien souvent, les un Rich Domain Model. Toutefois, l'optimisation
développeurs fraîchement convertis à l'orienté prématurée est souvent source de problèmes par
objet ont tendance à ne plus voir que des solu- la suite, aussi si votre application est réellement RAPHAËL ROUGERON
tions objets à tous les problèmes. Gardez-vous complexe, vous gagnerez certainement à la déve- Raphaël Rougeron est développeur web à la Cham-
pourtant de trop vite opter pour des solutions lopper ou tout du moins à la prototyper à l'aide bre de Commerce et d'Industrie de Paris, pour la-
comme le FrontController, car PHP et Apache d'un framework moderne comme le Zend Fra- quelle il a réalisé différentes applications métiers
permettent déjà de faire beaucoup de choses, mework, Symfony ou CakePHP. L'essentiel est en PHP. Dans le cadre de son travail, il a créé le fra-
et avec des performances bien supérieures. On que l'utilisation de ce framework vous permette mework Stato, publié sous licence MIT.
peut d'ailleurs se risquer à avancer que dans les d'écrire du code plus clair. Si les performances Pour contacter l'auteur : goldoraf@gmail.com

7 3/2007 (21)

Vous aimerez peut-être aussi