Les livres d'or font partie des applications dynamiques les plus populaires sur Internet. Ils permettent à aux visiteurs d'un site de déposer une trace écrite de leur passage dans le but d'aider le webmaster à améliorer son site. Nous allons présenter dans ce tutoriel, une manière simple et efficace de développer un livre d'or fonctionnel et sécurisé. Bien entendu, ce dernier s'appuiera sur une base de donnée de type MySQL et son moteur de stockage MyISAM. Dans une optique de structuration de l'application, nous nous appuierons sur le modèle MVC.
Avant de rentrer dans le vif du sujet, nous considérons que vous êtes déjà à l'aise avec :
Vous devrez également vous assurer que votre serveur web fonctionne avec la version 5 de PHP et que l'extension PDO et le driver PDO MySQL sont installés.
Sachez également que ce tutoriel n'a pas l'unique but de vous présenter une solution technique fonctionnelle pour réaliser un livre d'or. Il a également l'ambition (voire même la prétention) de vous apporter des connaissances approfondies de développement PHP5 et de bonnes pratiques.
Un livre d'or est une application dynamique qui permet aux utilisateurs d'un site Internet de déposer des messages d'appréciation. C'est en quelque sorte le recueil des humeurs des visiteurs à l'encontre du site web. Vous l'aurez compris, l'intérêt pour le webmaster est double :
L'application que nous allons développer devra répondre aux contraintes suivantes :
Détaillons sommairement ce dernier point.
MVC est l'abréviation de «Modèle, Vue, Contrôleur ». C'est est une architecture et une méthode de conception (design pattern) pour le développement d'applications logicielles qui sépare le modèle de données, l'interface utilisateur et la logique de contrôle.
Dans une application web, la couche du modèle est représentée par la base de données, les librairies de fonctions, les classes, les fichiers, les structures de données... Ce sont en fait tous les composants qui permettent de stocker et de manipuler les données.
La vue est la couche logicielle qui assure l'affichage des données à l'utilisateur et l'interface Homme / Machine. Cette couche récupère donc les données brutes du modèle et les formate correctement pour l'utilisateur. Par exemple, le nombre 1 254,67 est ici issu de la vue. Il est en effet formaté pour un site français. Pour un site américain, nous l'aurions écrit 1 254.67. En revanche, dans les deux cas, cette valeur s'écrit 1254.67 dans le modèle (dans une variable de type flottant par exemple).
La dernière couche est le contrôleur. Il s'agit du moteur principal de l'application. Il fait la liaison entre le modèle et la vue. Le contrôleur a la tâche d'analyser la requête de l'utilisateur, d'appeler le modèle adéquat et de retourner la valeur de ce dernier à la vue qui prendra en charge son affichage.
Quel sont les avantages d'utiliser une telle architecture ? Le premier intérêt concerne avant tout la maintenance. En séparant le problème en 3 couches distinctes, l'application deviendra plus facile à maintenir où à faire évoluer. Le second avantage implique la vue. En effet, cet éclatement en 3 couches permet de remplacer la vue aisément sans avoir à toucher au modèle ou bien au contrôleur. Par exemple : changer le wedesign d'un site, proposer différents format d'affichage d'un contenu (XML, XHTML, PDF, image...).
Nous avons défini globalement à quoi correspond le modèle MVC. Arrêtons nous à présent sur la structuration générale de notre livre d'or. De quoi aurons-nous besoin ?
Nous visualisons vaguement deux couches du modèles MVC . Le modèle représenté par la base de données et le formulaire et la page de listing qui feront partie de la vue. Nous construirons le contrôleur petit à petit. Passons désormais à la création de la base de données.
Nous utiliserons ici une table MySQL avec un moteur de stockage de type MyISAM. C'est le moteur par défaut de MySQL. Il est rapide, performant et supporte la recherche en texte intégral (fulltext). Néanmoins, ce moteur a le principal défaut de ne pas être conforme à la norme ACID (Atomicité, Cohérence, Isolement, Durabilité) des bases de données. Il ne supporte pas les transactions contrairement au moteur de stockage InnoDB. Malgré tout, nous décidons d'utiliser MyISAM car les enjeux d'intégrité et de cohérence des données ne justifient pas l'emploi d'un moteur de stockage transactionnel. Un livre d'or n'est pas une application critique, contrairement à une application manipulant des données bancaires ou des salaires par exemple.
La table MySQL de notre livre d'or accueillera les messages des utilisateurs. Notre formulaire sera composé de 3 champs (pseudo, message et note). Il faut donc au minimum que ces informations soient stockées dans la base de données. Seulement, ces informations ne suffisent pas. Il nous manque tout d'abord la clé primaire de la table. Nous opterons naturellement pour un identifiant unique auto-incrémenté pour chaque nouvel enregistrement. Enfin, nous ajouterons un champ recevant la date d'enregistrement des message afin de pouvoir les ordonner du plus récent au plus ancien à l'affichage.
CREATE TABLE IF NOT EXISTS guestbook ( id INT(7) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, pseudo VARCHAR(20) NOT NULL, message TEXT NOT NULL, note TINYINT(2) NOT NULL DEFAULT 5, creation DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00');
Commencez par exécuter ce code SQL dans votre outil d'administration de votre base de données (PHPMyAdmin, MySQL Query Browser...). Cela aura pour effet immédiat de créer la table "guestbook" dans votre base de données. Nous pouvons à présent nous tourner vers le développement PHP.
PHP5 propose plusieurs connecteurs capables de se connecter à une base de données MySQL. Le premier et le plus courant reste le driver MySQL de base. C'est le plus simple à utiliser et donc le plus populaire. Il existe également l'extension MySQLI qui n'est autre qu'un driver amélioré de l'extension MySQL de base et qui a la particularité d'être orientée objet. Avec MySQLI, le développeur manipule directement un objet de type MySQLI alors qu'avec le driver MySQL de base il doit fonctionner par appels de procédures et fonctions.
Enfin le dernier connecteur est l'extension PDO qui signifie PHP Data Objects. Cette extension permet de se connecter à de multiples bases de données à condition que les pilotes pour chaque système d'information soit installé sur le serveur Web. PDO a été intégré avec PHP5 et a le principal avantage de faire bénéficier le développeur de certaines fonctionnalités de la base de données en fonction de son pilote. Au même titre que MySQLI, PDO est une interface orientée objet, ce qui est beaucoup plus pratique à utiliser lorsque l'on est à l'aise avec de la programmation orientée objet.
C'est donc le connecteur que nous choisirons pour nous connecter sur la base de données et la manipuler. Notez que nous aurions pu également sélectionner le driver de base ou bien le connecteur MySQLI pour notre projet. Nous avons volontairement choisi PDO comme connecteur pour les raisons évoquées ci-après :
Nous sommes donc prêts à mettre la main à la pâte et commencer à produire nos premières lignes de code PHP.
Notre application se composera de 5 fichiers PHP dont un sera le front-controller ou programme principal qui contiendra toute l'application. Nous appellerons ce fichier guestbook.php. D'un point de vue du code PHP, nous verrons qu'il est relativement limité car il ne contiendra que des appels aux autres fichiers.
Nous développerons également un fichier de configuration (guestbook-config.inc.php) qui contiendra uniquement les informations de configuration de l'application. Il s'agit en fait simplement de constantes définissant les paramètres de connexion sur la base de données ainsi que le nombre de messages à afficher par page.
Puis nous créerons un fichier stockant des procédures / fonctions (ou helpers) utiles à l'application et potentiellement utilisables dans d'autres projets. Ce fichier se nommera guestbook-model.inc.php.
Ensuite nous développerons le coeur même de l'application, c'est-à-dire le contrôleur. Ce fichier PHP contiendra le code PHP qui vérifie et enregistre les données du formulaire en base de données; et récupère une liste de messages en fonction de la pagination. Nous appellerons ce fichier guestbook-controller.inc.php.
Enfin le dernier fichier contiendra le code générant la vue. Il s'agira majoritairement de code XHTML et de quelques appels à des fonctions élémentaires de PHP ainsi qu'à des fonctions utilisateurs du fichier guestbook-model.inc.php. Vous vous en doutez peut-être, ce fichier portera le nom guestbook-view.inc.php.
<?php /* - Constantes d'accès à la base de données - et de configuration du livre d'or */ // Adresse du serveur de base de données define('DB_SERVEUR', 'localhost'); // Login define('DB_LOGIN','root'); // Mot de passe define('DB_PASSWORD','root'); // Nom de la base de données define('DB_NOM','APTutoriels'); // Nom de la table du livre d'or define('DB_GUESTBOOK_TABLE','guestbook'); // Driver correspondant à la BDD utilisée define('DB_DSN','mysql:host='. DB_SERVEUR .';dbname='. DB_NOM); // Nombre de messages à afficher par page define('MAX_MESSAGES_PAR_PAGE', 1); ?>
<?php /* Ce fichier contient toutes les fonctions utiles à l'application */ /* Fonction de connexion sur la BDD Cette fonction utilise l'extension PDO de PHP5 - @param string driver de connexion sur la BDD - @param string login d'accès à la bdd - @param string mot de passe d'accès à la base de données - @return PDO objet de connexion sur la BDD */ function PDOConnect($sDbDsn, $sDbLogin, $sDbPassword) { $oPDO = new PDO($sDbDsn, $sDbLogin, $sDbPassword); $oPDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return $oPDO; } /* Convertit la date au format américain en format français - @param string la date au format US - @return string la date au format FR */ function convertirDate($sDateUs) { return strftime('%d/%m/%Y à %H:%M', strtotime($sDateUs)); } /* Fonction de pagination des résultats Retourne le code HTML des liens de pagination - @param integer nombre de résultats total - @param integer nombre de résultats par page - @param integer numéro de la page courante - @param integer nombre de pages avant la page courante - @param integer nombre de pages après la page courante - @param integer afficher le lien vers la première page (1=oui / 0=non) - @param integer afficher le lien vers la dernière page (1=oui / 0=non) - @return string code html des liens de pagination */ function paginer($nb_results, $nb_results_p_page, $numero_page_courante, $nb_avant, $nb_apres, $premiere, $derniere) { // Initialisation de la variable a retourner $resultat = ''; // nombre total de pages $nb_pages = ceil($nb_results / $nb_results_p_page); // nombre de pages avant $avant = $numero_page_courante > ($nb_avant + 1) ? $nb_avant : $numero_page_courante - 1; // nombre de pages après $apres = $numero_page_courante <= $nb_pages - $nb_apres ? $nb_apres : $nb_pages - $numero_page_courante; // première page if($premiere && $numero_page_courante - $avant > 1) { $resultat .= '<a href="'. htmlspecialchars($_SERVER['PHP_SELF']) .'?numeroPage='. $numero_page_courante .'" title="Première page">««</a> '; } // page précédente if($numero_page_courante > 1) { $resultat .= '<a href="'. htmlspecialchars($_SERVER['PHP_SELF']) .'?numeroPage='. ($numero_page_courante - 1) .'" title="Page précédente '. ($numero_page_courante - 1) . '">«</a> '; } // affichage des numéros de page for($i = $numero_page_courante - $avant; $i <= $numero_page_courante + $apres; $i++) { // page courante if($i == $numero_page_courante) { $resultat .= ' [<strong>' . $i . '</strong>] '; } else { $resultat .= ' [<a href="'. htmlspecialchars($_SERVER['PHP_SELF']) .'?numeroPage='. $i .'" title="Consulter la page '. $i . '">' . $i . '</a>] '; } } // page suivante if($numero_page_courante < $nb_pages) { $resultat .= '<a href="'. htmlspecialchars($_SERVER['PHP_SELF']) .'?numeroPage='. ($numero_page_courante + 1) .'" title="Consulter la page '. ($numero_page_courante + 1) . ' !">»</a> '; } // dernière page if($derniere && ($numero_page_courante + $apres) < $nb_pages) { $resultat .= '<a href="'. htmlspecialchars($_SERVER['PHP_SELF']) .'?numeroPage='. $nb_pages .'" title="Dernière page">»»</a> '; } // On retourne le résultat return $resultat; } ?>
<?php /* Contrôleur de l'application. Ce fichier - traite le formulaire - Enregistre les informations en base de données - Affiche une liste paginée de résultats */ /* ----- Déclaration des variables globales ----*/ // Objets de connexion et de manipulation de la BDD $oPDO = null; $oPDOStatement = null; // Tableau stockant les informations du livre d'or $aInfosGuestbook = array(); // Tableau stockant les messages récupérés de la BDD $aListeMessages = array(); // Tableau stockant les erreurs générées $aListeErreurs = array(); // Nombre de messages enregistrés dans la BDD $iNombreDeMessages = 0; // Numéro de la page courante $iNumeroDePageCourante = 1; // Offset à partir duquel on récupère les messages dans la BDD $iOffsetSelection = 0; // Note moyenne du site $fNoteMoyenne = 0; /* -----Contrôle de la pagination ---- */ if(!empty($_GET['numeroPage' * ) && is_numeric($_GET['numeroPage' * ) && ($_GET['numeroPage' * >1)) { $iNumeroDePageCourante = intval($_GET['numeroPage']); $iOffsetSelection = ($iNumeroDePageCourante - 1) * MAX_MESSAGES_PAR_PAGE; } /* ----- Initialisation de la connexion avec la base de données ---- */ $oPDO = PDOConnect(DB_DSN, DB_LOGIN, DB_PASSWORD); /* ----- Contrôle du formulaire ---- */ if(!empty($_POST)) { // Récupération et nettoyage des données $_POST['pseudo'] = trim($_POST['pseudo']); $_POST['message'] = trim($_POST['message']); $_POST['note'] = trim($_POST['note']); // Le pseudo est-il rempli ? if(empty($_POST['pseudo'])) { $aListeErreurs[] = 'Veuillez indiquer votre pseudo'; } else { // Le pseudo est-il compris entre 2 et 20 caractères ? if(strlen($_POST['pseudo']) < 2) { $aListeErreurs[] = 'Votre pseudo est trop court'; } if(strlen($_POST['pseudo']) > 20) { $aListeErreurs[] = 'Votre peudo est trop long'; } } // Le message est-il rempli ? if(empty($_POST['message'])) { $aListeErreurs[] = 'Veuillez indiquer votre message'; } // La note est-elle correcte ? if(empty($_POST['note']) || !is_numeric($_POST['note']) || ($_POST['note'] < 1) || ($_POST['note']>10)) { $aListeErreurs[] = 'Veuillez choisir une note'; } // Si aucune erreur n'a été générée // On enregistre le message dans la BDD if(0 === sizeof($aListeErreurs)) { try { // Création d'une requête préparée $oPDOStatement = $oPDO->prepare('INSERT INTO '. DB_GUESTBOOK_TABLE .' (pseudo, message, note, creation) VALUES(:pseudo, :message, :note, NOW())'); // Ajout de chaque paramètre à la requête // Les paramètres sont automatiquement protégés par l'objet PDO $oPDOStatement->bindParam(':pseudo', $_POST['pseudo'], PDO::PARAM_STR); $oPDOStatement->bindParam(':message', $_POST['message'], PDO::PARAM_STR); $oPDOStatement->bindParam(':note', $_POST['note'], PDO::PARAM_INT); // Execution de la requête préparée $oPDOStatement->execute(); } catch(PDOException $oPdoException) { $aListeErreurs[] = 'Une erreur est survenue et a empêché l\'enregistrement de votre message'; } } } /* ----- Comptage du nombre de messages en base de données et calcul de la note moyenne ----*/ $oPDOStatement = $oPDO->query('SELECT COUNT(1) AS nombreMessages, SUM(note) AS noteTotale FROM '. DB_GUESTBOOK_TABLE); $oPDOStatement->setFetchMode(PDO::FETCH_ASSOC); $aInfosGuestbook = $oPDOStatement->fetch(); $iNombreDeMessages = intval($aInfosGuestbook['nombreMessages']); // Calcul de la note moyenne if($iNombreDeMessages > 0) { $fNoteMoyenne = round(intval($aInfosGuestbook['noteTotale']) / $iNombreDeMessages, 2); } $oPDOStatement = null; /** ----- Récupération des messages en fonction de la pagination ---- */ if(sizeof($iNombreDeMessages)>0) { $oPDOStatement = $oPDO->prepare('SELECT pseudo, message, creation FROM '. DB_GUESTBOOK_TABLE .' ORDER BY creation DESC LIMIT :offset, '. MAX_MESSAGES_PAR_PAGE); $oPDOStatement->bindParam(':offset', $iOffsetSelection, PDO::PARAM_INT); $oPDOStatement->execute(); // Récupération des résultats sélectionnés dans le tableau $aListeMessages $aListeMessages = $oPDOStatement->fetchAll(PDO::FETCH_OBJ); } // Fermeture de la connexion SQL $oPDOStatement = null; $oPDO = null; ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr"> <head> <title>Mon livre d'or</title> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> </head> <body> <h1>Mon livre d'or</h1> <h2>Poster un message</h2> <?php /** Affichage des erreurs générées **/ ?> <?php if(sizeof($aListeErreurs) > 0) : ?> <ul> <?php foreach($aListeErreurs as $sErreur) : ?> <li><?php echo htmlspecialchars($sErreur); ?> <?php endforeach; ?> </ul> <?php endif; ?> <form action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>" method="post"> <div> <label for="pseudo">Pseudo :</label> <input type="text" name="pseudo" id="pseudo" value="<?php if(!empty($_POST['pseudo'])) : echo htmlspecialchars($_POST['pseudo']); endif; ?>" /> </div> <div> <label for="message">Message :</label> <textarea name="message" id="message" rows="10" cols="45"><?php if(!empty($_POST['message'])) : echo htmlspecialchars($_POST['message']); endif; ?></textarea> </div> <div> <label for="note">Note :</label> <select name="note" id="note"> <?php for($i=1; $i<11; $i++) : ?> <option value="<?php echo $i; ?>"<?php if(!empty($_POST['note']) && ($_POST['note'] == $i)) : echo ' selected="selected"'; endif; ?>><?php echo $i; ?></option> <?php endfor; ?> </select> </div> <div> <input type="submit" name="envoyer" value="Soumettre" /> </div> </form> <h2>Liste des messages</h2> <?php /** Affichage des messages **/ ?> <?php if($iNombreDeMessages > 0) : ?> <ul> <li><?php echo $iNombreDeMessages; ?> message<?php if($iNombreDeMessages > 1) : ?>s<?php endif; ?></li> <li>Note moyenne : <?php echo $fNoteMoyenne; ?> / 10 </ul> <?php foreach($aListeMessages as $oMessage) : ?> <div> <p> Par <?php echo htmlspecialchars($oMessage->pseudo); ?>, le <?php echo convertirDate($oMessage->creation); ?> </p> <blockquote> <p> <?php echo nl2br(htmlspecialchars($oMessage->message)); ?> </p> </blockquote> </div> <hr/> <?php endforeach; ?> <?php /** Affichage de la pagination si nécessaire **/ ?> <?php if($iNombreDeMessages > MAX_MESSAGES_PAR_PAGE) : ?> <div class="pagination"> <?php echo paginer($iNombreDeMessages, MAX_MESSAGES_PAR_PAGE, $iNumeroDePageCourante, 4, 4, 1, 1); ?> </div> <?php endif; ?> <?php else : ?> <p> Aucun message enregistré </p> <?php endif; ?> </body> </html>
<?php /* Programme principal Construit la page à partir de tous les fichiers*/ require(dirname(__FILE__).'/guestbook-config.inc.php'); require(dirname(__FILE__).'/guestbook-model.inc.php'); require(dirname(__FILE__).'/guestbook-controller.inc.php'); require(dirname(__FILE__).'/guestbook-view.inc.php'); ?>
Pour tester le livre d'or, il vous suffit simplement de placer tous les fichiers dans le même répertoire et d'appeller le script principal dans votre navigateur.
Par exemple : http://monsite.com/guestbook/guestbook.php
Les fichiers sources du programme sont disponibles en téléchargement libre. Vous pouvez les modifier et les commercialiser librement.
http://www.apprendre-php.com/downloads/Guestbook/APGuestbook.zip
Ce livre d'or reste malgré tout sommaire et il ne tient qu'à vous à présent de l'améliorer. Voici quelques idées pour agrémenter ce programme de nouvelles fonctionnalités :
...
Du fait de l'implémentation de l'objet PDO, cette application devient plus aisément portable sur un autre système de bases de données relationnelles. Si vous comptez utiliser ce livre d'or sur un système Oracle par exemple, il ne vous suffira qu'à changer la constante DB_DSN du fichier de configuration, et modifier les requête SQL du contrôleur en conséquence. Vous n'aurez nul besoin de toucher aux autres fichiers. C'est là tout l'avantage du modèle MVC comme nous l'avons expliqué plus haut.
Nous sommes arrivés au terme de ce tutoriel. Nous avons pu découvir progressivement comment réaliser une application Web PHP5 structurée et sécurisée s'appuyant sur le modèle MVC. Bien entendu ce n'est pas la seule et unique façon de procéder. Il en existe beaucoup d'autres mais cela vous donnera probablement de nouvelles idées pour vos prochains développements...