Architecture : Le Design Pattern MVC en PHP
Par François Lasselin le mardi, octobre 20 2009, 22:23 - Génie Logiciel - Lien permanent
Dans un projet, une démarche de génie logiciel pousse à définir l'architecture d'une application dans le respect des design pattern. L'architecture MVC cherche à séparer trois choses :
- la façon d'accéder aux données
- l'interface homme/machine: l'habillage, le design
- les traitements liés au métier/domaine de l'application
Soit le Modèle, les Vues et les Contrôleurs. Les contrôleurs permettent de répondre aux actions de l'utilisateur. Chaque contrôle est associé à une vue : cette dernière permet de présenter l'information retournée à l'utilisateur. Les données sont issues du modèle (la logique métier).
Les patterns ont une histoire et ne sont pas figés. Ainsi, le pattern MVC a évolué vers le MVC2. Dans l'architecture MVC 2, il n'existe plus qu'un seul et unique contrôleur réceptionnant toutes les requêtes clientes.
Le contrôleur unique devient le point d'entrée exclusif de l'application. Il devient alors très aisé de centraliser la gestion des accès, des droits, des statistiques ou de toute autre fonctionnalité transverse.
Concrètement, en quoi cela consiste t'il ? L'expression la plus simple d'une page affichant un listing de prix pourrait être la suivante.
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en_US" xml:lang=
"en_US">
<head>
<title>
Exemple
</title>
</head>
<body>
<?php
$db=mysql_connect($dbhost,$dbuser,$dbpass);
mysql_select_db($dbname,$db);
$query="SELECT product.name, product.price FROM product where product.type=42";
$req=mysql_query($query)or die('Erreur SQL!<br>'.$sql.'<br>'.mysql_error());
while ($row = mysql_fetch_array($req)) {
echo "Nom :".$row[0]." Prix :".$row[1];
}
mysql_close();
?>
</body>
</html>
Découpons le code. (Les sources sont téléchargeables sur la page des ressources)
D'abord, en construisant un aiguillage à requête.
Point d'entré unique : index.php:
if ($_GET['do']=="")
{
require ("default.html");
}
else
{
if($_GET['do']=="affichage")
{
require("action/class_example.php");
new Example();
}
}
?>
Controleur : /action/class_example.php:
require ("dao/dao_example_dao.php");
class Example
{
public function example()
{
$exampleDao= new ExampleDao();
$data = $exampleDao->get_products();
require("web/affichage.phtml");
}
}
?>
L'accès aux données est isolé dans une classe dao (Data access Object, autre design pattern)
Modèle : dao_example_dao.php
require ("class_connect_bd.php");
class ExampleDao
{
public function get_products()
{
$sql = "SELECT product.name, product.price FROM product where product.type=42";
$query = mysql_query($sql) or die('Erreur SQL !<br>'.mysql_error());
$all = mysql_fetch_all($query);
return $all;
}
}
?>
Le lecteur averti notera que la fonction mysql_fetch_all() n'est pas une fonction native de php. Il convient de l'implémenter. Ici dans le class_connect_bd.php:
$db = mysql_connect('localhost', 'user', 'password');
mysql_select_db('test',$db);
function mysql_fetch_all($query, $kind = 'assoc') {
$result = array();
$kind = $kind === 'assoc' ? $kind : 'row';
eval('while(@$r = mysql_fetch_'.$kind.'($query)) array_push($result, $r);');
return $result;
}
?>
Enfin, l'affichage:
Vue : /web/affichage.phtml:
<head>
<title>
Exemple
</title>
</head>
<body>
<?
foreach ($data AS $row )
{
?>
Nom :
<? echo $row["name"]; ?>
Prix :
<? echo $row["price"]; ?> <br/>
<?
}
?>
</body>
</html>
Attention: L'exemple se lance en passant en passant le paramètre do dans l'url :
index.php?do=affichage
Évidement sur un exemple court, l'explosion du nombre de lignes du code n'est pas à l'avantage du pattern. Toutefois, sur le développement d'une application, le rangement opéré sur le code est nécessaire. En effet, le MVC apporte un niveau d'industrialisation du code.
Cette architecture est particulièrement préconisée dans la mise en œuvre d'une application Web pour de nombreuses raisons.
Le point d'entré unique permet la mise en place de traitement centralisé. Typiquement, le contrôle d'accès. Plutôt que de copier coller (et oublier...) un test sur chaque page pour vérifier que l'utilisateur est identifié. On réalise ce test une fois et ce code est maintenu à un seul endroit.
Le code est également beaucoup plus facile à maintenir.
- L'enchainement des écrans est défini par le point d'entré unique , une modification se fera à cet endroit (et pas en partant à la recherche des 90 liens perdus dans une montagne de code)
- Si demain le produit n'est plus à chercher en base de données mais dans un service web, il est facile de ne modifier que le dao correspondant sans impacter la logique métier.
- Et si le design évolue, seul les fichiers de la vue sont concernés.
Les bénéfices d'une bonne structuration d'un code sont nombreux. C'est pour cela que le MVC est devenu incontournable et qu'il est utilisé dans la quasi totalité des framework PHP.
La discussion continue ailleurs
URL de rétrolien : http://blog.nalis.fr/index.php?trackback/60
Commentaires
Merci pour ce billet simple mais clair qui présente de manière concise et précise ce qu'il faut savoir à ce sujet.
Le meilleur article pour débutant que j'ai pu trouvé à ce sujet.
Le principe MVC est très bien expliqué.
Merci !
Merci, c'est bien expliqué.
J'ai une question,
des fonctions du type "mysql_fetch_array" ne devrait pas figurer dans le code de vue.. ou je me trompe?
merci
@linka
C'est une bonne question.
Non, les fonctions du type "mysql_fetch_array" ne doivent pas être dans la vue. Comme son nom l'indique, "mysql_fetch_array" est une fonction spécifique au SGBD MySQL. Il faut donc l'isoler dans le modèle car cela est spécifique à la façon dont les données sont stockées.
C'est pourtant bel et bien le cas dans l'exemple :s
En effet, c'est une erreur. J'ai corrigé.
Il y a une explication à ça. Il n'y a pas de fonction fetch_all en mysql. Du coup, il est tentant de parcourir les résultats au moment de l'affichage.
Mais il n'y avait donc pas isolation du traitement des données.
Pour rectifier le tir, j'ai inclus une fonction mysql_fetch_all().
Les sources du script sont maintenant téléchargeables.
"action/class_example.php/" fait partit du modèle ou du contrôleur? Si il fait partit du modèle, "require("web/affichage.phtml");" ne devrais pas plutôt être dans le contrôleur juste après "new Example();" pour que se soit le contrôleur qui s'occupe de choisir la vue et non le modèle?
Bonjour Guillaume,
Non, class_example.php c'est le contrôleur. L'appel à la vue est donc bien justifié ici.
Pour plus de clarté, J'ai ajouté "Modele", "Vue", "Controleur" et "Point d'entré unique" avant haque fichier correspondant.
Hum... mais le point d'entré unique me semble être un contrôleur dans ton cas... puisque en théorie c'est le contrôleur qui reçoit la requête et choisi le modèle et la vue qui sera utilisé (L'aiguillage fait partie intégral du contrôleur). Hors, dans ton point d'entré, tu effectue un choix entre affiché default.html (donc afficher une vue) ou de créé un "exemple" qui va appeler un modèle et afficher une autre vue. C'est, il me semble, clairement la fonction d'un contrôleur. Ton index.php correspond au contrôleur du pattern MVC2. Ton /action/class_example.php est une action donc possiblement un espèce de sous-contrôleur OU, en déplaçant l'appel à la vue dans index.php, faire partit du modèle. J'avoue qu'il y a pour moi une partie flou entre où doit finir le contrôleur et commencer le modèle. Par contre, je suis certains que ton index.php EST un contrôleur. À bientôt!
Le point d'entrée unique est ce que l'on appel un Front Controller, il fait le premier aiguillage vers les différents modules de ton application. Ensuite tu a tes Back Controller classiques. Le premier appelant les seconds. Tu rencontre assez souvent cette architecture.
Votre explication du principe MVC est très compréhensible ce qui est assez rare pour le souligner. Ce design pattern est important pour créer des applications propres, lisibles et donc plus facilement maintenable (ce que certains développeurs ont tendance à oublier). A mon sens, c'est un principe fondamentale pour un développeur.
Bravo pour cet article clair
Enfin un moyen simple et efficace de faire comprendre le fonctionnement du MVC. Tout simplement merci et bravo a Francois Lasselin pour se billet.
/clap
Bonjour,
L'exemple est très compréhensible, cependant à l'affichage je n'ai aucune erreur mysql et je n'ai pas de données qui s'affichent.
Bonjour Bastien,
J'ai tout réinstallé à partir des sources du site. Je soupçonne que ta configuration de php n'affiche pas les warning.
As tu bien appelé : index.php?do=affichage ?
Cordialement,
J'appelle bien cette même page cependant je n'ai aucun affichage de la BDD, seulement: "Nom : Prix : ".
De plus au début du script j'ai demandé l'affichage des erreurs (error_reporting(E_ALL);) mais rien n'y fait je n'ai pas d'erreurs.
Est-ce que la base de données est créée ?
Y'a t'il bien des données dans la table ?
Est-ce que tu as modifié le fichier de connexion pour y mettre les paramètres de connexion à ta base ?
Bien sur :D tout ceci est fait !
J'accède aux données à partir d'autres scripts
Merci.
Un tuto simple et bien fait très pédagogique qui ne nous perd pas en disgressions!
J'ai le même souci que Kishin : Je n'affiche aucune donnée
Bonjour Vincent,
Peux tu m'envoyer tes coordonnées par le formulaire de contact ?
(j'ai encore tout retesté en réinstallant les sources du blog, ... ça marche)
Comme ça, on se contacte directement et on trouve ce qui bloque.
++
Bonjour François et les autres visiteurs,
J'ai bien aimé ta manière de présenter MVC...
Je me suis penché sur le problème précité. Le problème qu'il pourrait y avoir pour les personnes concernées doit venir de l'accès à la base de donnée. Si aucune erreur n'est reportée, ce doit être grâce au (à cause du...) @ qu'il y a dans ta boucle WHILE évaluée dans ta fonction "mysql_fetch_all". Je n'ai vu aucune autre possibilité d'erreur ailleurs dans le code. Kishin et Vincent devraient essayer le script en retirant cet "opérateur de contrôle d'erreur", comme on l'appelle... ils obtiendraient peut-être la réponse à leur problème vu que les possibles messages d'erreurs de cette expression ne seraient plus filtrés.
PS : Je me suis permis de retoucher le code, juste histoire d'avoir une sortie valide XHTML1.0 Strict et 2-3 autres choses au niveau du PHP ( http://hq.ariworld.eu/mvc )
Très bien expliqué, droit au but. Merci !
Pour ceux qui n'arrivent pas à voir les données, cela vient probablement du nom d'une variable.
Le problème du pattern Bordel / Vue / Controleur c'est que très très rapidement on ne sait plus d'ou viennent les variables.
En l'occurence ici c'est une variable appelée $all [ nom pas très explicite, malheureusement ] qui devient $data dans la vue.
Le code ne peut donc pas fonctionner ; et je suis effaré par le nombre d'exemples qu'on peut trouver sur le web qui ne fonctionnent pas ; et je suis aussi étonné par les commentaires.
A croire que la seule démonstration suffit ; que ça puisse fonctionner est secondaire. J'espère que vous n'achetez pas votre voiture sur le même principe !
Sinon ; merci pour ce billet ; et un immense merci à Arimbourg qui propose un source QUI FONCTIONNE parfaitement et qui améliore le procédé.
Bon code à tous.
Coucou Tartiflette,
Le nom d'une variable interne à une fonction (en l'occurrence: return $all;) n'a pas besoin d'être réutilisé ailleurs pour le résultat de cette même fonction ($data = $exampleDao->get_products();). Donc, j'ai du mal à suivre ton raisonnement.
Une lecture plus attentive du code (ou le téléchargement des sources complètes) te permettrait de voir que l'exemple fonctionne (cette implémentation fait tourner plusieurs sites actuellement en prod...). C'est un peu injuste de se défouler gratuitement sur cet article.
Cordialement,
bonjour j'ai bien télécharger les sources, et malheureusement ca ne fonctionne pas ;
- il manque le fichier "default.html"
- Undefined index: do
@efoenix : Oui, c'est normal. L'exemple se lance en complétant l'url : index.php?do=affichage
Petit article sympa et très clair.
Toutefois, je voudrais suggérer deux-trois points pour étendre un peu le modèle et gérer les problèmes usuels, notamment la détermination du contexte (e.g. dans le cas de la redoutable utilisation du bouton "back").
Je m'explique avec un exemple : j'ai deux listes - des articles et des clients - dont chaque ligne est munie d'un bouton "Afficher" (?do=affichage), ce qui permet d'accéder, respectivement, à la fiche d'un article ou à celle d'un client.
Le bouton a une signification bien différente selon le contexte (avec éventuellement une gestion de droits : je n'ai peut-être pas le droit de voir les clients au même titre que les articles).
Donc, il est nécessaire de déterminer le contexte dont il est question :
[...]
else {
if ($_GET['contexte']=="liste_clients") {
if($_GET['do']=="affichage") {
[...]
}
}
else if ($_GET['contexte']=="liste_articles") {
if($_GET['do']=="affichage") {
[...]
}
}
[...]
Bon, mon code est super lourdingue et devrait être remplacé par un fichier de paramétrage, avec un appel dynamique à la fonction qui va bien ;-)
L'avantage de gérer les choses de cette manière, c'est que même en jouant sur le bouton "back", le contexte est toujours explicitement disponible (il est donc difficile de ruser avec le système : en appliquant la gestion de droits sur le contexte, dans le Front Controller, il y a peu de place pour contourner le contrôle).
Sinon, on pourrait aussi résoudre le problème en appelant les actions différemment (disons, "?do=affichage_articles" et "?do=affichage_clients") mais ceci oblige à une conserver l'unicité des désignations dans toute l'application (!) et je ne trouve pas ça très élégant.
Qu'en dites-vous ?
OK, pour ceux qui ont des problèmes du genre "rien ne s'affiche", il suffit de remplacer tous les "<?" par "<?php" dans le fichier "affichage.phtml" ! (suffisait de regarder les sources dans un navigateur pour se rendre compte que le code php n'était pas interprété).
Sinon, un grand merci pour ce petit exemple !
Ouai, ben c'est trés bien si on développe un système d’exploitation, mais pour un site web c'est lourdingue, ce qui est important c’est de séparer au maximum les traitements des données de l’affichage de façon à ce que comme dans le modèle MVC, la modification du traitement n’influe pas sur la vue, et qu’il n’y ai qu’un seul point d’entrée.
De plus avec l’intégration en plus des requetes ajax, la séparation strict devient à mon avis une source de complexité, voir pose des problèmes insurmontables puisque parfois le controleur est de fait écrit en javascript est donc intégré de fait à la vue. Fort de ce constat, il faut garder les principes de base, et ne pas s’accrocher à une application stricte contre productive.
Bonjour,
merci pour cet exemple de modèle.
Cependant, j'ai du mal à comprendre la couche DAO :
il me semblait qu'il fallait séparer la DAL de la DAO (la config de la BDD et les requêtes SQL étant exprimées dans la DAL, la description des objets avec leurs GET/SET dans la DAO) ?
Merci pour vos réponses :-)
> il me semblait qu'il fallait séparer la DAL de la DAO (la config de la BDD et les requêtes SQL étant exprimées dans la DAL, la description des objets avec
> leurs GET/SET dans la DAO) ?
Le découpage DAL / DAO serait pertinent dans le cas où on utiliserait un ORM (Object Relational Mapping ) dans lequel on retrouverait les 4 instructions CRUD. L'implémentation proposée dans le billet ne met pas en oeuvre d'ORM c'est pourquoi on ne trouve pas de get/set. L'ORM n'est pas une obligation, il serait même à déconseiller pour son impact négatif sur les performances.
Cordialement,
François
Bonjour Lolo,
>pour un site web c'est lourdingue, ce qui est important c’est de séparer au maximum les traitements des données de l’affichage de façon à ce que
>comme dans le modèle MVC, la modification du traitement n’influe pas sur la vue, et qu’il n’y ai qu’un seul point d’entrée.
D'abord, c'est une démarche saine de commencer par s'interroger sur ce qui est nécessaire ou pas et tu as raison : "ne pas s’accrocher à une application stricte contre productive." on ne fait pas un MVC pour un script de 200 lignes. J'aurai tendance à prescrire le MVC pour des applications web (en opposition avec un site web mais la frontière est maigre). Mais il faut adapter ces outils à l'ouvrage à réaliser.
Sinon, séparer les traitement + point d'entrée unique > l'implémentation proposée dans ce billet ne propose rien de plus (avec une mise en œuvre objet sommaire...)
> De plus avec l’intégration en plus des requêtes ajax, la séparation strict devient à mon avis une source de complexité, voir pose des problèmes
> insurmontables puisque parfois le contrôleur est de fait écrit en javascript est donc intégré de fait à la vue. Fort de ce constat, il faut garder les
>principes de base, et ne pas s’accrocher à une application stricte contre productive.
Heu ... constat erroné: Aucun problème avec l'ajax, implémenté avec succès dans plusieurs projets basés sur l'implémentation décrite ici.
Après il y a différentes religions : ceux qui pense que le code xml fait partie de la vue et ceux qui pense que çe fait partie du modèle.
>le contrôleur est de fait écrit en javascript est donc intégré de fait à la vue
Ici, on présente un pattern MVC poru le PHP. pas pour le javascript.
Cordialement,
François
Bonjour,
Surement une question de sémantique, mais pour moi la DAL est la couche d'accès aux données. C'est un terme architectural, alors que DAO est un patron de conception.
In extenso, cela signifie que les DAO sont inclus dans la DAL.
La conf. de l'accès à la couche de persistance étant quant à elle implémenté dans les fabriques abstraites qui créent les fameux DAO.
JP
WHAT IS THAT???
.
.
$kind = $kind === 'assoc' ? $kind : 'row';
eval('while(@$r = mysql_fetch_'.$kind.'($query)) array_push($result, $r);');
.
.
Un ternaire et un eval() ???
.
.
if($kind === 'assoc')
{
____while($r = mysql_fetch_assoc($query))
________array_push($result, $r);
}
else
{
____while($r = mysql_fetch_row($query))
________array_push($result, $r);
}