Exemple d'application PHP et MySQL : Niouzilla

© Christophe Lauer - clauer@linux-france.org
(Relu par Armel Fauveau et John Gallet)

Un exemple concret : un site de publication de nouvelles

Afin d’illustrer le développement d’applications Web dynamiques en PHP, nous allons construire des scripts qui nous permettront d’afficher, de saisir et de gérer des nouvelles, un peu à la façon de Slashdot (http://www.slashdot.org) ou encore de Linuxfr.org (http://linuxfr.org/news/).

Bien entendu, les scripts que nous allons réaliser ne sont qu’un embryon d’application. De nombreuses améliorations pourraient être faites (gestions des erreurs, optimisations diverses, etc.).

Ces quelques scripts ont avant tout un but didactique : ils vont nous permettre d’étudier les points suivants :

Nous avons choisi d'utiliser MySQL comme SGBD, ce dernier étant parmi les plus utilisés avec PHP. Les sources utilisent donc des fonctions qui lui sont spécifiques. Vous devrez adapter ces sources si vous souhaitez interfacer l'ensemble avec un autre SGBD.

But

Nous voulons réaliser une page d’affichage des nouvelles, les présentant par ordre chronologique, et n’affichant que les nouvelles les plus récentes (le nombre de nouvelles affichées sera paramétrable). Nous nous réfèrerons à cette page sous le nom de " page d’accueil " dans la suite de l’article.

Ensuite, nous allons créer une page contenant un formulaire HTML qui permettra aux visiteurs du site de saisir des nouvelles. Dans la suite, nous nous réfèrerons à cette page en la nommant " page de saisie ".

A l'image de tout site d’information qui se respecte, nous ne voulons pas que les nouvelles saisies par les utilisateurs (donc des internautes potentiellement inconnus) soient directement publiées. Nous allons donc passer par une phase intermédiaire de validation : le modérateur (qui peut être le webmaster du site) devra valider les dernières publications ou au contraire les supprimer. En pratique, la page réservée au modérateur devrait être protégée par l’utilisation d’un mot de passe (authentification). Pour des raisons évidentes de simplicité, cette page sera publiquement accessible dans notre exemple. Pour de plus amples informations sur ce point, consultez la documentation de votre serveur Web. Si vous utilisez Apache, il suffit de placer cette page dans un répertoire distinct, et de le protéger par un fichier .htaccess. Nommons cette page la page de " validation ".

Enfin, nous voulons permettre la consultation des messages les plus anciens, ne figurant plus sur la page d’accueil. Pour cela, nous allons réaliser une page qui permettra de visualiser les nouvelles les plus anciennes, et permettra de se déplacer de page en page au moyen des classiques liens du type " Page précedente " et " Page suivante ". Comme cette page fait presque les mêmes traitements que la page d’accueil, nous allons la réaliser au moyen de la page d’accueil. La page d’accueil sera donc chargée d’afficher les dernières nouvelles, et affichera les nouvelles les plus anciennes quand nous le lui demanderons en lui passant un paramètre spécial.

 

Fonctionnalités

Toutes les pages comportent dans leur partie haute les mêmes liens vers les autres pages de l’application. Cet affichage standard est effectué par l’appel à la fonction display_links().

Dans une application réelle, l’ensemble de l’habillage des pages devant être cohérent et rester conforme à la charte graphique, nous utiliserions un système de modèles de pages tel que FastTemplates (se reporter à l’article sur les solutions " prêtes à utiliser " disponibles en PHP).

Page d’accueil

La page d’accueil doit interroger le contenu de la base de données pour y chercher les nouvelles les plus récentes puis afficher ces nouvelles.


Illustration : nouvelles.gif

Page de saisie

La page de saisie comprend un formulaire comportant les zones de saisie suivantes : adresse d’e-mail de l’auteur de la nouvelle, titre de la nouvelle, un liste déroulante présentant les différentes rubriques thématiques. Nous devrons créer dans la base de données une table qui stockera les rubriques thématiques avec leurs identifiants respectifs.

Enfin une zone de saisie de texte, destinée à recevoir le corps du message. Notre exemple est volontairement simplifié, nous ne faisons pas de test de cohérence sur les saisies. D’autre part, le corps du texte est limité en base à 255 caractères.


Illustration : ajout.gif

Script d’insertion de nouvelle

Une fois la saisie effectuée, ce script sera chargé d'enregistrer les valeurs dans la base de données. Suivant le succès ou l’échec de l’enregistrement, un message de remerciement ou d’avertissement sera retourné à l’utilisateur pendant quelques secondes. Puis celui-ci sera automatiquement reconduit vers la page d’accueil.

Page de validation

Cette page réalise deux fonctions. Elle affiche l’ensemble des publications en attente de validation de la part du modérateur. D’un point de vue ergonomique, c’est cette même page qui effectue la requête de changement de statut d’une nouvelle, de sorte que la validation ou la suppression d’une nouvelle ne nécessite qu’un seul clic.


Illustration : validation.gif

Pour faire fonctionner cet exemple, il vous faut une machine sur laquelle un serveur Web, PHP et MySQL sont installés. PHP devra être configuré de façon à offrir les fonctions d’accès à MySQL (sous Unix/Linux, ceci nécessite de recompiler PHP).

Dans MySQL, nous créons une base dédiée à notre application : nous l’appelons naturellement ‘niouzilla’. Cette base niouzilla va contenir deux tables, l’une destinée à contenir les différentes rubriques thématiques, l’autre devant recevoir les nouvelles saisies par les utilisateurs.

Pour créer la base et ses tables associées, vous pouvez soit ouvrir une session interactive MySQL en ligne de commande, soit utiliser l’indispensable outil d’administration de MySQL, écrit en PHP et offrant une interface Web. Cet outil est un logiciel libre, il se nomme " phpMyAdmin " et peut être téléchargé sur Internet à l’adresse suivante : http://phpwizard.net/phpMyAdmin.

Les scripts de création des tables figurent ci-dessous. Ils ne contiennent rien de particulier. Nous soulignons que dans le cas général, tout enregistrement d’une table doit comporter un identifiant unique permettant de caractériser de façon certaine une ligne de la table. Pour répondre à ce besoin de façon simple, MySQL propose un extension au standard SQL. Il s’agit du mot réservé AUTO_INCREMENT. Une colonne de type entier qualifiée au moyen de ce mot réservé verra sa valeur attribuée automatiquement par MySQL au moment de l’instruction SQL d’insertion. En tant que programmeur, vous n’avez pas besoin de fournir de valeur pour cette colonne, passez simplement NULL dans la requête SQL. MySQL se charge du reste.

Table NOUVELLES :

CREATE TABLE nouvelles (

idnouv int(11) NOT NULL auto_increment,

dtsaisi datetime,

dtpubli datetime,

cdstatut char(1),

cdrubriq int(11),

idauteur varchar(50),

titre varchar(50),

texte text,

PRIMARY KEY (idnouv)

);

Table RUBRIQUES :

CREATE TABLE rubriques (

cdrubriq int(11) NOT NULL,

lbrubriq varchar(20),

PRIMARY KEY (cdrubriq)

);

Après avoir créé cette table, nous allons y ajouter quelques rubriques (libre à vous de les adapter selon votre inspiration)

INSERT INTO RUBRIQUES VALUES (1, 'Internet', NULL);

INSERT INTO RUBRIQUES VALUES (2, 'Free software', NULL);

INSERT INTO RUBRIQUES VALUES (3, 'Multimédia', NULL);

INSERT INTO RUBRIQUES VALUES (4, 'Communication', NULL);

INSERT INTO RUBRIQUES VALUES (5, 'Commerce', NULL);

INSERT INTO RUBRIQUES VALUES (6, 'Technologie', NULL);

INSERT INTO RUBRIQUES VALUES (7, 'Loisirs', NULL);

INSERT INTO RUBRIQUES VALUES (8, 'Manifestations', NULL);

 

Revue de code…

Nous avons regroupé plusieurs fonctions dans un fichier distinct, qui est inclus dans l’ensemble des scripts. Parmi ces fonctions, deux sont particulièrement utiles puisqu’elles réalisent la connexion et la déconnexion de la base de données. Que contiennent-elles ?

function db_connect()

{

$servername = 'localhost';

$dbname = 'niouzilla';

$login = '';

$password = '';

$dbh = @mysql_connect($servername, $login, $password) or die ("probleme de connexion");

@mysql_select_db($dbname, $dbh) or die ("probleme dans selection base");

return $dbh ;

} ;

function db_disconnect(&$dbh)

{

mysql_close($dbh);

$dbh=0;

} ;

La connexion à la base s’effectue via la fonction mysql_connect(). Notez que pour des raisons de sécurité, il est recommandé de procéder comme dans l’exemple : utiliser des variables pour fournir le login et le mot de passe, et placer un @ devant l’appel de la fonction, de façon à interdire l’affichage des messages d’erreur. A la place du message d’erreur de PHP, on utilise l’instruction die() et un message d’erreur tout à fait inoffensif.

La fonction fermant la connexion à la base de données est également chargée de placer la variable contenant le lien vers la base ($dbh : database handle) à une valeur nulle. Tout script bien écrit doit tester la valeur du database handle avant de soumettre une requête.

Dans le cas d’une application réelle, qui serait exploitée par plusieurs utilisateurs simultanés, on aurait sans doute intérêt à employer une connexion à MySQL dite " permanente ". Les connexions permanentes permettent à PHP de mutualiser les connexions à MySQL entre plusieurs pages. D’un point de vue pratique, on économise les délais d’ouverture et de fermeture des connexions au niveau de la durée d’exécution des scripts.

Pour effectuer l’affichage des nouvelles, nous devons ouvrir une connexion à la base de données, exécuter une requête SQL de sélection, puis lire les lignes constituant l’ensemble des résultats (on parle de resultset). Détaillons le code correspondant :

$dbh = @db_connect();

if ($dbh <> 0)

{

$sql = "SELECT n.idnouv, n.dtsaisi, r.lbrubriq, n.titre, n.texte, n.idauteur ";

$sql .= "FROM nouvelles n, rubriques r ";

$sql .= "WHERE n.cdstatut = 'A' ";

$sql .= "AND n.cdrubriq = r.cdrubriq ";

$sql .= "ORDER BY n.dtsaisi desc ";

$result=@mysql_query($sql, $dbh);

if($result <> FALSE)

{

while ($niouze=mysql_fetch_object($result))

{

display_news($niouze->idauteur, $niouze->dtsaisi,

$niouze->lbrubriq, $niouze->titre, $niouze->texte);

} ;

} ;

mysql_free_result($result);

db_disconnect($dbh);

} ;

Premièrement, il s’agit d’effectuer une connexion à la base de données, ce qui retourne en cas de succès un handle ($dbh) qui sera ensuite nécessaire pour toute requête. Vous remarquerez qu’il est fortement recommandé de s’assurer systématiquement que les valeurs de retour sont correctes. On construit ensuite la requête SQL. A ce niveau, pas de remarque particulière, si ce n’est qu’on effectue une jointure entre les tables NOUVELLES et RUBRIQUES. La requête est exécutée, par l’appel de la fonction mysql_query(). Cette fonction retourne un descripteur qui permet de récupérer toutes les lignes de résultat. Dans notre exemple, nous avons utilisé la fonction mysql_fetch_object() qui retourne une ligne sous la forme d’objet PHP, dont chacun des attributs membre correspond à une colonne résultat de la requête.

PHP permet de manipuler les données de résultat sous plusieurs formes : un tableau dans lequel on accède aux différentes colonnes par leur position dans la requête SQL, un tableau associatif dans lequel la clé est le nom de la colonne, et enfin, sous la forme d’un objet PHP. Cette dernière forme est celle qui permet l’écriture la plus compacte, et la plus lisible.

A titre d’exemple, si nous avions souhaité manipuler l’ensemble de résultat sous la forme d’un tableau indicé, le code aurait ressemblé à ceci :

$row=0 ;

while (mysql_fetch_row($id))

{

$idnouv = @mysql_result($id, $row, 0);

$dtsaisi = @mysql_result($id, $row, 1);

$lbrubriq = @mysql_result($id, $row, 2);

$titre = @mysql_result($id, $row, 3);

$texte = @mysql_result($id, $row, 4);

$idauteur = @mysql_result($id, $row, 5);

display_news($idauteur, $dtsaisi, $lbrubriq, $titre, $texte);

$row += 1 ;

};

Le principal inconvénient de cette forme est que toute modification de colonnes au niveau de la requête SQL implique de changer le troisième paramètre de mysql_result().

L’instruction mysql_fetch_array() retourne les colonnes de la table sous la forme d’un tableau associatif. Ceci permet dans certains cas précis de faire des traitements itératifs qui s’appliquent à toutes les colonnes, en utilisant l’instruction de parcours de tableau each().

while ($niouze=mysql_fetch_array($result))

{

display_news($niouze["idauteur"], $niouze["dtsaisi"], $niouze["lbrubriq"], $niouze["titre"], $niouze["texte"] ) ;

} ;

Imaginez que vous ayez à manipuler un ensemble de colonnes représentant des valeurs booléennes, stockées tout naturellement en base de données sous la forme d’un entier pouvant prendre les valeurs 0 ou 1. Pour afficher très simplement ‘Oui’ ou ‘Non’ en fonction de la valeur des colonnes, on pourra procéder de la façon suivante :

if($options = mysql_fetch_array($resopt))

{

// Remplacement des 0 en 'Non' et des 1 en 'Oui'

reset($options);

while(list($key, $value) = each($options))

{

if (strlen($options[$key])==1)

{

if ($options[$key]==0)

$options[$key] = 'Non'

else

$options[$key] = 'Oui';

} ;

} ;

} ;

La dernière chose intéressante à regarder dans la page d’accueil est qu’elle a deux comportements : elle affiche soit les toutes dernières nouvelles, soit les archives des nouvelles plus anciennes. Cet artifice est obtenu en passant un paramètre à la page, qui lui signale si on souhaite obtenir un comportement de page d’accueil ou d’archive. De façon très simple, pour lui indiquer que l’on souhaite avoir l’affichage des archives, on lui passe le paramètre " mode " qui prend la valeur " archive ". En outre, on lui indique également le numéro de la première nouvelle à afficher. De cette façon, on peut très simplement afficher l’ensemble des nouvelles archivées par tranche.

// Affichage des liens 'Page Préc.' et 'Page Suivante'

if ((isset($mode)) && ($mode=='archive'))

{

if (!isset($first))

{

$first=$rowlimit ; // $rowlimit est une constante qui définit le nbre de lignes à afficher

} ;

echo "<p align='center'><FONT FACE='Courier'>\n";

if ($first >= $rowlimit)

{

echo "<a href='index.php3?mode=archive&first=".($first-$rowlimit)."'> &lt;&lt;&nbsp;Page précédente</a>&nbsp; &nbsp;\n";

};

echo "<a href='index.php3?mode=archive&first=".($first+$rowlimit)."'> Page suivante&nbsp;&gt;&gt;</a>\n</font></p>";

} ;

On voit donc que l’affichage en mode archive va ajouter les deux liens classiques " Page précédente " et " Page suivante ". Ces liens sont construits de façon à rappeler la même page en lui passant une valeur différente pour le numéro de départ de la première nouvelle à afficher. C’est le paramètre $first, qui évolue par tranches de $rowlimit de longueur.

Maintenant que l’on connaît le numéro d’ordre de la première nouvelle à afficher, nous allons construire la requête SQL permettant d’interroger la base de données. En fait, le code que nous avions vu précédemment pour expliquer la connexion à la base de données n’était pas complet, nous avions volontairement éliminé l’affichage par tranche, pour conserver à cet exemple toute sa clarté. Voici la partie manquante :

$dbh = @db_connect();

if ($dbh <> 0)

{

$sql = "SELECT n.idnouv, n.dtsaisi, r.lbrubriq, n.titre, n.texte, n.idauteur ";

$sql .= "FROM nouvelles n, rubriques r ";

$sql .= "WHERE n.cdstatut = 'A' ";

$sql .= "AND n.cdrubriq = r.cdrubriq ";

$sql .= "ORDER BY n.dtsaisi desc ";

if (isset($first))

{

$sql .= "LIMIT $first, " . $rowlimit;

}

else

{

$sql .= "LIMIT " . $rowlimit;

} ;

$result=@mysql_query($sql, $dbh);

La requête standard est construite, puis on lui ajoute le mot clé LIMIT, qui est une extension au SQL que l’on ne trouve que dans MySQL et maintenant dans PostgreSQL en version 6.5 ou plus. Ce mot clé permet de limiter l’ensemble de résultat au N premières lignes à partir de la M-ième. Prenons un exemple. Une table MaTable comporte 1000 lignes, seulement 200 lignes ont une valeur non nulle dans la colonne TypeData.

 

SELECT ColA, ColB, ColC from MaTable where TypeData IS NOT NULL LIMIT 50, 10

 

Si la requête précédente, sans le mot réservé LIMIT nous rapportait les 200 lignes pour lesquelles TypeData n’est pas NULL, la requête précédente nous retournerait les 10 lignes à partir de la 50ème ligne de l’ensemble de résultat. Il faut bien voir que le mot clé LIMIT porte sur l’ensemble de résultat de la requête et non pas sur l’ensemble des lignes de la table. Pour plus d’explications, vous vous reporterez au manuel de MySQL. Ce dernier est également disponible en Français (voir l’adresse en fin d’article).

Etudions à présent l’enregistrement des nouvelles dans notre base de données. Deux scripts sont chargés de faire cet enregistrement. Le premier ( submit.php ) contient simplement un formulaire, et un morceau de code PHP qui interroge la table des rubriques afin de remplir une liste déroulante. Nous avons ajouté un exemple de vérification de saisie en JavaScript dans ce formulaire. Rappelons que le JavaScript est un langage embarqué dans le HTML et qui est destiné à être interprété et exécuté par le navigateur. Du point de vue de PHP, qu’il s’agisse de générer du HTML pur ou de générer du HTML et du JavaScript, cela ne fait aucune différence. Gardez en mémoire qu’il n’y a, a priori, aucun lien entre le code PHP et le code JavaScript. Ce formulaire envoie ensuite les données saisies à un autre script ( recordcontrib.php ) qui est chargé de réaliser l’enregistrement dans la table " Nouvelles ".

Pour pouvoir tester si une adresse d’e-mail a été saisie dans le formulaire, il faut pouvoir se référer au champ en question dans le formulaire. Pour cela, il faut donner un nom au formulaire : nous le nommons " myform ". Le champ contenant l’adresse e-mail, qui porte le nom " nomauteur " est donc document.myform.nomauteur et sa valeur est document.myform.nomauteur.value. L’événement OnSubmit du formulaire appelle la fonction JavaScript f_email_valid() qui est chargée de retourner TRUE ou FALSE, ce qui conditionne le déclenchement de l’action (appel du script recordcontrib.php) et l’envoi des valeurs contenues dans le formulaire.

<SCRIPT LANGUAGE="JavaScript">

<!-- Vérification de Saisie de l'adresse d'e-mail de l’auteur

function f_email_valid() {

// Champ non rempli

if (document.myform.nomauteur.value == "") {

alert("Veuillez saisir votre adresse d'e-mail.");

document.myform.nomauteur.focus();

return false;

}

// Ok

return true;

}

// -->

</SCRIPT>

Notez qu’on place généralement la définition des fonctions JavaScript dans la section HEAD de la page HTML. De cette façon, on s’assure que le code JavaScript sera chargé et donc potentiellement utilisable depuis n’importe quel endroit de la page HTML. Le code source complet du script submit.php est donné en fin de l’article.

Dans cette page de formulaire, les valeurs de la liste déroulante sont affichées dynamiquement, en fonction des rubriques lues dans la base de données. Une simple portion de code PHP au niveau de la définition du formulaire permet de réaliser ceci.

<SELECT NAME="cdrubriq">

<?php

echo "<option value=\"0\">[Aucune]</option>\n" ;

$dbh = db_connect();

if ($dbh <> 0)

{

$sql = "SELECT cdrubriq, lbrubriq ";

$sql .= "FROM rubriques ";

$sql .= "ORDER BY cdrubriq";

$result = @mysql_query($sql, $dbh) or die ("problème dans requête select");

while ($rub = mysql_fetch_object($result))

{

echo "<option value=\"$rub->cdrubriq\"> $rub->lbrubriq </option>\n" ;

}

@mysql_free_result($result);

db_disconnect($dbh) ;

}

?>

</SELECT>

Notez que la valeur [Aucune] n’est pas enregistrée en base de données, car elle ne constitue pas réellement une rubrique. Cette valeur est ajoutée " en dur ". Ceci produit le code HTML suivant, dans lequel on retrouve bien nos rubriques et leur codes associés.

<SELECT NAME="cdrubriq">

<option value="0">[Aucune]</option>

<option value="1">Internet</option>

<option value="2">Free software</option>

<option value="3">Multimédia</option>

<option value="4">Communication</option>

<option value="5">Commerce</option>

<option value="6">Technologie</option>

<option value="7">Loisirs</option>

<option value="8">Manifestations</option>

</SELECT>

Au niveau de l’enregistrement dans la table " Nouvelles ", il n’y a rien de particulier à signaler. On retrouve les valeurs saisies dans le formulaire en accédant au contenu du tableau associatif HTTP_POST_VARS, car le formulaire était déclaré avec cette méthode. Les dernières recommandations en la matière sont de préférer la méthode POST à la méthode GET pour les formulaires HTML.

 

Dans PHP, le tableau HTTP_POST_VARS ne contiendra vos valeurs que si la directive track_vars est à " On " dans le fichier de configuration. Sur les plateformes Unix/Linux, il faut recompiler PHP en lui donnant la directive --enable-track-vars. Vérifiez au moyen de phpinfo() si cette option est activée dans votre environnement. Si tel n'est pas le cas, vous pouvez l'activer dynamiquement en plaçant un <?php php_track_vars?> en début de script.

La seule subtilité au niveau du script d’enregistrement des nouvelles dans la table réside dans le fait que, pour effectuer la redirection automatique au bout de 5 secondes, nous plaçons dans la section HEAD de la page HTML un tag META spécial :

 

<META http-equiv="Refresh" content="5; URL=index.php3">

 

Grâce à ce tag, au bout de cinq secondes d’affichage du message, l’utilisateur sera automatiquement reconduit vers la page " index.php3 ".

Terminons cette revue de code par la page de validation des nouvelles. Cette page est chargée d’afficher la liste des nouvelles en attente de validation de la part du modérateur du site, ainsi que de mettre à jour le statut de la nouvelle. Le statut d’une nouvelle peut prendre dans notre exemple trois valeurs :

Grâce à l’utilisation de fonctions, ce script est très compact, et donc très lisible. Vous remarquez que les liens " Valider " et " Supprimer " pour chaque nouvelle en attente pointent également vers le script de validation, en fournissant deux paramètres qui sont le numéro de la nouvelle et le statut à lui attribuer. Si le script reçoit en paramètre un numéro de nouvelle, il commence par mettre à jour le statut de cette nouvelle. De ce fait, cette nouvelle n'a plus un statut à NULL. Elle disparaît donc de la liste des nouvelles en attente de validation. Le script affiche ensuite normalement la liste des nouvelles restant à valider. De ce fait, l’acceptation ou le refus d’une nouvelle ne nécessite qu’un seul clic de la part du modérateur. Ainsi, il lui est très facile de traiter une grande quantité de messages.

Ce script ne comporte aucune difficulté. Il contient une requête de mise à jour et une requête de sélection. Maintenant, sa compréhension ne devrait pas vous poser de problème.

 

En guise de conclusion

Grâce à PHP, vous disposez gratuitement d’un outil très puissant, et de qualité professionnelle. Mais créer des application Web avec PHP ne présente pas seulement l’intérêt d’être simple et efficace, c’est également ludique et très formatteur. Nous espérons que cet exemple aura su vous convaincre, et vous donnera envie de créer à votre tour toutes les applications dont vous rêvez depuis toujours.

Si vous disposez d’une connexion à Internet, sachez que vous pouvez tester directement ces exemples depuis l’adresse suivante : http://clauer.free.fr/niouzilla/

La documentation officielle de PHP est disponible à cette adresse : http://www.php.net/manual ou sur le site miroir Français http://fr.php.net/manual

La documentation de PHP traduite en Français se trouve à cette adresse :

http://www.nexen.fr/fr/aide/PHP/francais/manual.html

La documentation de MySQL officielle est disponible à cette adresse : http://www.mysql.com/Manual_chapter/manual_toc.html

La documentation de MySQL traduite en Français est disponible à cette adresse : http://www.nexen.fr/fr/aide/MySQL/index.html

 

© Christophe Lauer - clauer@linux-france.org
(Relu par Armel Fauveau et John Gallet)