« En route vers un framework MVC ... » Le 7 octobre 2007 Développement Web

Introduction

Il y a peu, j’ai commencé à m’intéresser au développement avec Ruby on Rails et, pour être franc, je suis tombé de haut, dans le bon sens du terme. En effet, le principe du framework Rails est d’utiliser le motif de conception Modèle-Vue-Contrôleur, quelque chose qui était totalement nouveau à mes yeux à ce moment-là.

Après avoir un peu digéré mes découvertes et m’être pas mal interrogé sur la logique et la qualité de mes développements en PHP, je me suis dit qu’il ne serait pas inutile d’essayer modestement de créer un framework personnel que je pourrais utiliser facilement pour programmer en PHP.

Ce genre de frameworks existent bien sûr depuis bien longtemps pour PHP, le mien n’apportera donc pas grand chose aux ténors du genre que sont cakePHP, symphony, qcodo, phpontrax, et tant d’autres. Il s’agit avant tout d’un projet d’étude que je compte mener le plus loin possible afin de mieux comprendre la logique du motif de conception MVC.

Le motif Modèle-Vue-Contrôleur

PNG - 39 ko

L’idée qui sous-tend le motif MVC est de permettre de séparer au mieux les 3 parties fondamentales qui constituent un site web, j’ai nommé le modèle de données de la base de données (c’est le modèle), la présentation de l’ensemble (la vue) et le traitement des données (le contrôleur). Cette séparation permet notamment de respecter au mieux le principe du DRY (don’t repeat yourself) et du faible couplage des classes.

Je n’ai volontairement pas réinventé la roue dans mon framework, mais ai préféré assembler des éléments éprouvés et testés pour bénéficier rapidement de leur apport. J’ai essentiellement écrit la classe controleur qui va créer une association entre ces éléments.

Tour d’horizon

Modèle

La classe de gestion du modèle de données permet de s’abstraire de la base de données relationnelle au maximum pour éviter d’avoir des requêtes à saisir et conserver une approche tout objet en manipulant toujours des concepts métier. Pour fonctionner, elle a besoin que les tables de la base de données respectent une syntaxe standardisée. Cela peut devenir un problème s’il s’agit de réemployer d’anciennes tables dont on ne peut pas changer la nomenclature.

Concrètement, voilà un exemplé tiré d’une petite application que je suis en train de développer en utilisant le framework :


// On cherche l'article dont l'id est $id
$article = MyActiveRecord::FindById("article", $id);

// On cherche l'utilisateur qui a rédigé l'article (liaison un à plusieurs)
$utilisateur = $article->find_parent('auteur');

// On cherche enfin les catégories auxquelles appartient l'article (liaison plusieurs à plusieurs) :
// Elles seront placées dans le tableau $categories qu'il suffira de parcourir.
$categories = $article->find_attached('categorie');

Dans mon exemple, j’ai trois concepts métiers qui correspondent à quatre tables dans ma base. J’ai simplement eu à déclarer les objets en étendant la classe que j’ai employée pour pouvoir ensuite faire totalement abstraction de la base.


class article extends MyActiveRecord
{
   
}

class auteur extends MyActiveRecord
{
   
}

class categorie extends MyActiveRecord
{
   
}

Voilà à quoi se limitait un paramétrage préliminaire : ces quelques déclarations de classes et une ligne pour indiquer à MyActiveRecord comment se connecter à la base de données.


include('config/bdd.php');
define('MYACTIVERECORD_CONNECTION_STR', "mysql://$utilisateur:@$hote/$base");

Vous imaginez maintenant mieux les possibilités offertes par ce système en termes d’ajouts à la base, de tests avant insertion (il suffit de surcharger une méthode de la classe MyActiveRecord dans un modèle de données x) etc.

Trouver cette fameuse classe MyActiveRecord n’a pas été une chose très complexe : elle se trouve notamment sur phpclasses.org et a été écrite par Jake Grimley.

Vue

Le principe de la couche présentation est le suivant : il s’agit de détacher le traitement des données (réalisé par le contrôleur) de la présentation en elle-même en se contentant de recevoir et de mettre en forme des variables transmises par le contrôleur.

GIF - 3 ko

Smarty est un langage de templates pour PHP qui permettait de créer exactement ce dont j’avais besoin, c’est donc lui qui s’occupera de la partie présentation de mon framework.

Pour rendre tangible le principe de cette séparation du fond et de la forme, voilà un nouvel exemple, tiré du code de ma petite application en cours de développement.

Après avoir cherché un article, son auteur et ses catégories, nous allons afficher tout cela sur l’interface de l’utilisateur : il faut donc explicitement passer ces quelques variables du contrôleur vers la vue. Voilà les lignes à ajouter à la suite des premières :


$this->Vue->assign('article', $articles);
$this->Vue->assign('utilisateur', $utilisateur);
$this->Vue->assign('categories', $categories);

// On affiche la template avec smarty :
// il s'agit d'un fichier afficherMessage.tpl placé dans le controleur admin
$this->Vue->afficher("admin/afficherMessage");

Ensuite, dans la template smarty correspondante, on peut utiliser ces informations facilement avec des boucles, des traitements basiques etc. Je vous renvoie à la documentation de Smarty qui existe en Français pour en découvrir toutes les possibilités.


<h1>{$article->titre}, par {$utilisateur->prenom} {$utilisateur->nom}</h1>
(...)

Contrôleur

La seule partie que j’ai réellement développée moi-même pour l’instant est le contrôleur. Dans le modèle MVC, le contrôleur va se charger de réaliser les opérations de traitement, d’interface entre l’utilisateur et la base de données. Il est ainsi tout naturellement placé au centre de l’échiquier.

Concrètement, le contrôleur regroupe le traitement d’un ensemble de vues et contient un certain nombre d’actions, des méthodes de la classe contrôleur, qui vont être appelées éventuellement avec un ou plusieurs paramètres.

Un exemple vaut mieux qu’un long discours, aussi voilà une url tirée de mon application d’exemple :


http://127.0.0.1/myFramework/?admin/afficher/article/1

Il faut à ce moment-là lire la chose suivante :

  • contrôleur : "admin"
  • action : "afficher"
  • paramètres : "article" et "1"

D’autres frameworks permettent de paramétrer totalement ce chemin (route), comme Rails, mais celui-ci ne permet que cette syntaxe là pour le moment. Elle devrait suffire pour une utilisation courante.

Concrètement, les classes contrôleur (adminControleur en l’occurence) héritent simplement d’une classe Controleur générique. Leur nom est important, puisque le script que je présenterai après recompose le nom de la classe à partir du nom de base (nom + Controleur).

Voilà un exemple de contrôleur, avec une action :


class adminControleur extends Controleur
{
   // Cette fonction est appelée si on n'a pas spécifié d'action a effectuer
   // elle devrait être toujours redéfinie.
   // elle correspond à une url comme http://monsite.com?admin/
   function __index()
   {
       // utilisation du modèle de données
       $articles = MyActiveRecord::FindAll("article");
       
       // assignation de la variable à la vue
       $this->Vue->assign('articles', $articles);
       
       // appel de la vue à la fin de la méthode
       $this->Vue->affiche("admin/liste");
   }
   
   // elle correspond à une url comme http://monsite.com?admin/article[/param]
   function article($param = null)
   {
       // (...)
   }  
}

La classe Controleur en elle-même ne contient pour l’instant qu’une fonction permettant de tester si une action d’un nom particulier a bien été déclarée, et s’occupe d’inclure les objets modèles de données et d’instancier la vue.

Transformation de l’URL

PNG - 10.9 ko

Cette partie ne rentre pas vraiment dans le modèle MVC, mais elle est importante tout de même. Elle s’occupe de décomposer l’URL fournie au navigateur pour en tirer le nom du contrôleur, de l’action et des paramètres éventuels avant d’instancier le bon contrôleur et d’appeler l’action. Si une erreur se produit, elle lance une exception en donnant quelques détails.

On est ici quasiment dans la méta-programmation, c’est à dire l’écriture de code autour du langage en lui même : il s’agit assez prosaïquement de tester si telle classe de contrôleur a bien été définie et le cas échéant, si elle contient bien l’action que l’on veut exécuter.


// on récupère les arguments passés en paramètre à la page
$arguments_page = array_keys($_GET);
$arguments_page = explode("/",$arguments_page[0]);
$arguments_page = array_clean($arguments_page, "");

// on gère les exceptions en utilisant une classe Erreur qui encapsule Exception.
try
{
   // ---------------------------------------------- Controleur
   
   // Il faut au moins proposer un contrôleur
   // Cela pourrait change à l'avenir.
   if(count($arguments_page) < 1) throw new Erreur("Veuillez préciser un contrôleur.");
       
   // Le contrôleur est le premier paramètre
   $controleur = $arguments_page[0];

   // Le fichier du même nom doit exister
   if(!is_file("app/controleurs/".$controleur."Controleur.php"))
       throw new Erreur("Ce contrôleur n'existe pas. ");
   
   // On instancie le controleur qui porte le nom : nomControleur
   include("app/controleurs/".$controleur."Controleur.php");
   eval('$iControleur = new '.$controleur.'Controleur();');
   
   // ---------------------------------------------- Action
   
   // L'action est le deuxième argument
   $action = $arguments_page[1];
   
   // Si on a une action
   if($action)
   {
       // et quelle existe dans le contrôleur ...
       eval('$existe_action = $iControleur->existeAction("'.$action.'");');
       if(!$existe_action) throw new Erreur("Vous n'avez aucune action '$action' dans le contrôleur '$controleur'.");
       
       // ---------------------------------------------- Paramètres
       
       // si elle existe bien, on regarde s'il y a des paramètres
       $cParametres = "";
       if(count($arguments_page[2]) > 0)
       {
           $parametres = array_slice($arguments_page, 2);
           
           // traitement des paramètres pour mettre les strings entre guillemets
           $parametres = array_map("traiterParam", $parametres);
           
           // on sépare les paramètres avec des virgules pour respecter la syntaxe
           // d'un appel de fonction.
           $cParametres = implode(",", $parametres);
       }
       
       // puis on exécute l'action en joignant les éventuels paramètres.
       eval('$iControleur->'.$action.'('.$cParametres.');');
   }
   // Sinon, on appelle l'action standard
   else
   {
       $iControleur->__index();
   }
}

// Si une erreur s'est produite.
catch(Erreur $e)
{
   echo $e->afficher();
}

Conclusion

Je publierai la source complète dans quelques temps, mais je veux tout d’abord créer une petite application de test me permettant d’affiner un peu tout ça. Quoi qu’il en soit, si vous avez des suggestions ou des remarques, n’hésitez pas à m’en faire part en laissant un commentaire ;)

Dandelionmood.com a déménagé !

Votez pour cet article sur

Vos réactions

Nuxwin, le 25 février 2008

Bonjour ;

Je serais intéressé par ton framework mvc. En effet, je suis actuellement entrain d’en développer un pour une application spécifique et je serais curieux de voir comment tu as ordonnancé le tous. Cela pourrait notamment me donner de nouvelles idées.

D’avance merci.

Pierre Quillery, le 25 février 2008

Bonjour Nuxwin,

j’aimerais beaucoup t’aider, mais malheureusement, à la suite d’un fausse manipulation, j’ai perdu la quasi totalité de mes sources web les plus récentes, y compris celles du framework :(.

Toutefois, si ça peut t’être utile, je ne peux que te renvoyer vers le framework Ruby on Rails sur lequel je me suis basé pour créer la structure des fichiers. Voilà un bon exemple.

Laissez un message !

Qui êtes-vous ?

Un message, un commentaire ?