Un petit script de vote en Ajax et MySQL

Explications et prérequis

Explications

Le but de cet article est de faire un vote ultra simple qui n'a que 2 choix possibles : "oui" et "non". Une barre de progression montrera l'état des votes en cours ...

Pour faire plus "actuel", on utilisera dans cet article de l'AJAX (Asynchronous Javascript And XML), c'est une sorte d'interface entre le PHP et le Javascript, via de l'XML. Pourquoi ?

PHP est côté serveur (et c'est PHP qui va lire/écrire dans la base de données) et HTML/Javascript sont du coté client : pour actualiser les données, il n'y a donc pas d'autre alternative, à première vue, que de recharger toute la page ... Sauf si, dans un coin de page, un module dynamique (Javascript est, après tout, le côté dynamique chez le "client") permet d'aller à la volée lire des informations et les mettre à jour ...

Ce script présente donc un inconvénient (soyons clairs) c'est que sans javascript, il ne fonctionnera pas. Pour cela, on peut prévoir une variante ("accessibilité" oblige) qui ne nécessite pas le Javascript (mais dans ce cas, il y a rechargement complet de la page).

Prérequis

On a besoin :

  • du langage PHP (ou un équivalent côté serveur, cet article est écrit avec du PHP mais il est traductible en ASP ...)
  • d'une base de données MySQL avec une (ou des) table(s), dont la structure est donnée plus loin.
  • d'une page en HTML censée présenter les données
  • d'une image (rouge.png dans mon exemple) qui représentera la barre de progression
  • d'un peu de temps ...

Le code source

Structure de la base de données

Commençons par la base de données. Je suppose ici qu'on veut évaluer des articles qui ont un identifiant (ID) : savoir si vos visiteurs ont apprécié cet article. Il y a donc une table 'demo_ajax_articles_liste' comme suit :

CREATE TABLE demo_ajax_articles_liste(
	id            INT(5) NOT NULL auto_increment,

	fichier       VARCHAR(255) NOT NULL,
	titre         VARCHAR(255) NOT NULL, -- ceux qui auront voté (+)

	PRIMARY KEY(id)
) Engine = MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;

INSERT INTO demo_ajax_articles_liste(fichier, titre) VALUES
('article1.html', 'L''ornithorynque polyglotte'),
('faq_newslettux.html', 'Questions des utilisateurs NewsletTux'),
('Tuto_NewsletTux2.html', 'Installation de NewsletTux de A à Z');


-- ce qui donne cet aperçu :

id            fichier                 titre
-----------------------------------------------------------------------------
1             article1.html           L''ornithorynque polyglotte
2             faq_newslettux.html     Questions des utilisateurs NewsletTux
3             Tuto_NewsletTux2.html   Installation de NewsletTux de A à Z

Bien entendu, le principe peut être adapté à n'importe quoi... L'idée maintenant est de mettre une sorte de notation pour chaque ID de cette table. Par souci de simplicité (et de clarté) dans ce script, on ne bloque pas à 1 vote par visiteur et par jour.

Voici la table qu'il faut créer pour stocker les votes (je mets arbitrairement la popularité de départ à 50%) :

CREATE TABLE demo_ajax_articles_votes(
	id              INT(5) NOT NULL auto_increment,
	id_article      INT(5) NOT NULL, -- ID de l article auquel ce vote se réfère

	num_votes       INT(5) NOT NULL DEFAULT 0, -- le nombre de votants
	votes_plus      INT(5) NOT NULL DEFAULT 0, -- ceux qui auront voté (+)
	votes_moins     INT(5) NOT NULL DEFAULT 0, -- ceux qui auront voté (-)
	pourcent_depart INT(3) NOT NULL DEFAULT 0, -- le % de la barre au départ

	PRIMARY KEY(id)
) Engine = MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;

INSERT INTO demo_ajax_articles_votes(id_article, pourcent_depart) VALUES
(1, 50),
(2, 50),
(3, 50);

A chaque vote, on rajoutera +1 au compteur correspondant (+ ou -) et on affichera, en fonction du nombre de départ, l'état de la popularité...

Code source du moteur de vote

On supposera dans cet article que la connexion à la base de données est réalisée dans un fichier indépendant : "mysql.php". Voici le contenu de ce fichier, à titre d'exemple. Vous pouvez prendre votre propre système de connexion bien sûr.

<?php

	$mysql_host = '127.0.0.1';
	$mysql_port = 3306;
	$mysql_name = 'base_vote_ajax'; // nom de la BDD
	$mysql_uname = 'compte_sql';
	$mysql_upasswd = 'mot_de_passe';

	// connexion MySQL
	try
	{
		$connexion = new PDO('mysql:host='.$mysql_host.';port='.$mysql_port.';dbname='.$mysql_name, $mysql_uname, $mysql_upasswd,array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
		$connexion->SetAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
	}
	catch(Exception $e)
	{
		echo 'Erreur : '.$e->getMessage().'<br />';
		echo 'N° : '.$e->getCode();
		exit();
	}

?>

On va maintenant créer les fonctions qui vont réaliser les votes. Ce seront des fonctions, parce qu'on pourra les appeler plus facilement. Ces fonctions sont à placer dans un fichier d'extension .php, ce fichier devra être inclus dans la page des votes via un include (ou un require). Nommons par exemple ce fichier "ajax_vote.php" :

<?php
	/*
	 * Cette fonction enregistre un nouveau vote pour l'article
	 * entrée : $id_article {int}
	 * entrée : $vote, ('plus' || 'moins') {string}
	 * entrée : $verbose, pour renvoyer le résultat {bool}
	 *
	 * sortie {string}
	 */
	function AddNewVote($id_article, $vote, $verbose = true)
	{
		// on suppose que la connexion base de données est faite dans un fichier à part
		require('mysql.php');



		$query = 'UPDATE
					demo_ajax_articles_votes
				SET
					votes_{TYPE} = (votes_{TYPE} + 1),
					num_votes = (num_votes + 1)
				WHERE
					id_article = :id_article;';
		// replace vote
		$query = str_replace('{TYPE}', $vote, $query);
		$requete_prepare_1=$connexion->prepare($query);
		$resultat = $requete_prepare_1->execute(array(':id_article' => $id_article));


		if ($resultat)
		{
			// read and return new status
			return ReadVoteStatus($id_article, $verbose);
		}
		else
		{
			die($query.'<br />'); // à supprimer en production et à remplacer par votre système d'erreurs
		};
	}




	/*
	 * Cette fonction lit l'état d'un vote pour l'article
	 * entrée : $id_article {int}
	 * entrée : $verbose pour renvoyer le résultat {bool}
	 *
	 * sortie : {string}
	 */
	function ReadVoteStatus($id_article, $verbose)
	{
		// on suppose que la connexion base de données est faite dans un fichier à part
		require('mysql.php');

		$query = 'SELECT
					num_votes,
					votes_plus,
					votes_moins
				FROM
					demo_ajax_articles_votes
				WHERE
					id_article = :id_article;';
		$requete_prepare_1=$connexion->prepare($query);
		$requete_prepare_1->execute(array(':id_article' => $id_article));
		$lignes=$requete_prepare_1->fetch(PDO::FETCH_ASSOC);
		$nb_lignes = $requete_prepare_1->rowCount();




		$answer = '';
		if ($nb_lignes == 1)
		{
			// on retourne un texte arbitrairement sous la forme : nb de votes | nb + | nb -
			$answer = $lignes['num_votes'].'|'.$lignes['votes_plus'].'|'.$lignes['votes_moins'];
		};

		if ($verbose)
			return $answer;
	}





	// maintenant on appelle ces fonctions
	$dont_exec = (isset($dont_exec)) ? $dont_exec : false;
	if (!$dont_exec) // evite la double exécution sans Javascript
	{
		$id_article = (isset($_GET['id_article'])) ? abs(intval($_GET['id_article'])) : 0;
		$vote = (isset($_GET['vote'])) ? $_GET['vote'] : 'plus';
		if (($vote != 'plus') && ($vote != 'moins')) { $vote = 'plus'; }

		if ($id_article != 0)
		{
			echo AddNewVote($id_article, $vote, true); // répond par le statut du vote
		};
	}

?>

2 fonctions sont ici décortiquées (et nommées explicitement) : une fonction pour réaliser un vote (en plus ou en moins) et une fonction pour lire l'état des votes pour un article. Une fois qu'un vote est enregistré, on lit le nouvel état des votes de l'article : soit l'ID de l'article est complètement fantaisiste auquel cas on ne renvoie rien (une chaine vide, plus exactement), soit l'ID est bien réel et on renvoie 3 nombres, séparés par des pipes (AltGr + 6) qui sont respectivement le nouveau nombre de votes, le nouveau nombre de plus et le nouveau nombre de moins.

Il reste maintenant à faire 3 choses : créer les liens "plus" et "moins", créer les fonctions AJAX qui les activent (et les liens HTML en cas de Javascript indisponible) et afficher la barre de progression ;o)

Code source HTML : présentation des données

Cet exemple, simple, n'a pas la prétention de faire joli. On aura recours aux styles CSS pour embellir la présentation qui, je l'avoue, est très sommaire ici. Je suppose que par une requête on lit tous les articles, et pour chacun, on crée une ligne de paragraphe.

Ceci est la page articles.php, d'extension .php, qui présente les articles (et permet le vote d'ailleurs).

<!DOCTYPE html>
<!--[if IE 8 ]><html class="ie ie8" lang="fr"> <![endif]-->
<!--[if (gte IE 9)|!(IE)]><!--><html lang="fr"> <!--<![endif]-->
<head>
	<meta charset="utf-8">
	<title>Un petit script de vote en Ajax et MySQL</title>
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
	<script src="ajax_vote.js"></script>
	<style type="text/css">
	table { border:1px solid #000; width:80%; }
	thead tr th { border:1px solid #000; padding:5px; background-color:#3272ad; color:#FFF; font-weight:bold; }
	tbody tr td { border:1px solid #000; padding:5px; }
	</style>
</head>

<body>
	<h1>PHP-Astux, le site des scripts PHP !</h1>
	<p>Cette page est celle qui liste mes articles pour lesquels j'organise un vote.
	Il y a 3 articles dans la table de test et la popularité commence arbitrairement à 50%.</p>
<?php

	// on suppose que la connexion base de données est faite dans un fichier à part
	require('mysql.php');

	// fonction pour afficher 1 ligne de tableau par article
	function DisplayArticle($article)
	{
		// preparation du score de pourcentage (popularité de l'article)
		/* barre de progression : un <div> de largeur fixée, avec une couleur de fond et une image de largeur variable
			calcul du pourcentage (largeur de la barre rouge)

			on part du chiffre initial ($article[\'pourcent_depart\']) et on lui ajoute les votes + et - :
			La note va de 0 à 100% et on part d\'un nombre initial : X
			depuis ce nombre, il y a eu A votes (+) et B votes (-)
			on met tout le monde à la même échelle, et on recalcule la note...
		*/

		// les votes des gens
		$total_votes_gens = $article['num_votes']; // par exemple : 200 votes : 170 + et 30 -
		$total_votes_gens_plus = $article['votes_plus']; // 170
		$total_votes_gens_moins = $article['votes_moins']; // 30

		// calcul de la popularité
		$popularite = ($total_votes_gens == 0 ) ? $article['pourcent_depart'] : intval(($total_votes_gens_plus / $total_votes_gens) * 100);



		// maintenant on affiche la ligne de l'article
		echo '
		<tr>
			<td><strong>'.$article['titre'].'</strong></td> <!-- titre de cet article -->
			<td>[ <a href="http://monsite/'.$article['fichier'].'">Lien</a> ]</td> <!-- lien -->
			<td>Nombre de votes : <span id="numvotes_'.$article['id'].'">'.$article['num_votes'].'</span> </td> <!-- Nb de votes. Laissez bien un espace aprus le /span -->
			<td><span id="show_'.$article['id'].'" style="display:none;"> <!-- Si Javascript est désactivé, ces liens ne seront pas visibles (vote sans ajax) -->

				<!-- vote + -->
				<span id="vote_plus_'.$article['id'].'">(<a href="#" onclick="vote(\''.$article['id'].'\',\'plus\'); return false;" title="Voter (+)">+</a>)</span>

				<!-- vote - -->
				<span id="vote_moins_'.$article['id'].'">(<a href="#" onclick="vote(\''.$article['id'].'\',\'moins\'); return false;" title="Voter (-)">-</a>)</span></span>

				<!-- les liens de vote "classiques" (sans javascript), masqués si Javascript actif -->
				<span id="hide_'.$article['id'].'">
					<!-- vote + -->
					(<a href="?act=vote&id_article='.$article['id'].'&vote=plus" title="Voter (+)">+</a>)

					<!-- vote - -->
					(<a href="?act=vote&id_article='.$article['id'].'&vote=moins" title="Voter (-)">-</a>)
				</span>
			</td>
			<td>
				<div style="padding-left: 20%; padding-right: 20%; margin-top: 1ex;">
					<div style="border: 1px solid #000; padding: 1px; font-size: 8pt; height: 12pt; background-color: #FFF; position: relative;">
						<div style="padding-top: 1pt; width: 100%; z-index: 2; color: #F60; position: absolute; text-align: center; font-weight: bold;">'.$popularite.' %</div>
						<div style="width:'.$popularite.'%; height: 12pt; z-index: 1; background-color: #009;"> </div>
					</div>
				</div>

				<!-- Si javascript inactif : les liens AJAX n\'apparaissent pas, les liens classique oui (vote sans Ajax)
				sinon, on masque les liens classiques et on active les liens AJAX -->
				<script>document.getElementById(\'hide_'.$article['id'].'\').style.display="none"; document.getElementById(\'show_'.$article['id'].'\').style.display="";</script>
			</td>
		</tr>'.PHP_EOL;
	}



	// récupération des articles depuis la base
	$array_articles = array(); $i = 0;

	$query = 'SELECT id, titre, fichier FROM demo_ajax_articles_liste;'; // A vous de mettre les where, order by etc. si nécessaire
	$requete_prepare_1 = $connexion->prepare($query);
	$requete_prepare_1->execute(array());
	$nb_lignes = $requete_prepare_1->rowCount();
	$i = 0;

	if ($nb_lignes > 0)
	{
		while($art = $requete_prepare_1->fetch(PDO::FETCH_ASSOC))
		{
			$array_articles[$i]['id'] = $art['id'];
			$array_articles[$i]['titre'] = $art['titre'];
			$array_articles[$i]['fichier'] = $art['fichier'];

			// ces variables seront mises à jour s'il y a un vote
			$array_articles[$i]['num_votes'] = 0;
			$array_articles[$i]['votes_plus'] = 0;
			$array_articles[$i]['votes_moins'] = 0;
			$array_articles[$i]['pourcent_depart'] = 0;

			// on lit les votes pour cet article
			$query2 = 'SELECT
						num_votes,
						votes_plus,
						votes_moins,
						pourcent_depart
					FROM
						demo_ajax_articles_votes
					WHERE
						id_article = :id_article;';
			$requete_prepare_2 = $connexion->prepare($query2);
			$requete_prepare_2->execute(array( ':id_article' => $art['id']));
			$nb_lignes2 = $requete_prepare_2->rowCount();

			if ($nb_lignes2 == 1) // il y a bien un vote pour cet article
			{
				while($vote = $requete_prepare_2->fetch(PDO::FETCH_ASSOC))
				{
					$array_articles[$i]['num_votes'] = $vote['num_votes'];
					$array_articles[$i]['votes_plus'] = $vote['votes_plus'];
					$array_articles[$i]['votes_moins'] = $vote['votes_moins'];
					$array_articles[$i]['pourcent_depart'] = $vote['pourcent_depart'];
				}
			}
			$i++;
		}
	}




	// maintenant, on affiche les articles
	//print_r($array_articles);
	echo '
	<table style="border:1px solid #000;">
	<thead>
		<tr>
			<th>Titre article</td>
			<th>Lien d\'accès</td>
			<th>Nb. Votes</td>
			<th>Voter</td>
			<th>Popularité</td>
		</tr>
	</thead>
	<tbody>'.PHP_EOL;
	foreach($array_articles as $i => $article)
	{
		DisplayArticle($article);
	}
	echo '
	</tbody>
	</table>'.PHP_EOL;



	// Vote sans javascript : même principe, mais "en local sur cette page".
	if ((isset($_GET['act'])) && ($_GET['act'] == 'vote'))
	{
		$dont_exec = true;
		require_once('ajax_vote.php'); // cf. plus haut dans cette page, pour hériter des fonctions
		$id_article = (isset($_GET['id_article'])) ? abs(intval($_GET['id_article'])) : 0;
		$vote = (isset($_GET['vote'])) ? $_GET['vote'] : 'plus';
		if (($vote != 'plus') && ($vote != 'moins')) { $vote = 'plus'; }

		if ($id_article != 0)
		{
			if (AddNewVote($id_article, $vote, false) === NULL)
			{
				echo '<p>Votre vote a bien été pris en compte, merci ! <a href="articles.php">Cliquez sur ce lien pour revenir aux articles</a></p>';
			}
		};
	};

?>
</body>
</html>

Code source de l'AJAX

L'AJAX est du javascript : c'est donc une suite d'instructions que nous allons placer dans un fichier, par exemple ajax_vote.js d'extension .js et appelé entre <head> et </head> par la ligne :

	<script src="ajax_vote.js"></script>

Dans ce fichier, il y a une fonction qui crée un objet nommé XMLHTTPRequest. C'est l'objet XML qui va s'interposer entre le Javascript et le PHP. Il peut avoir 3 états : opération faite, opération en cours, opération finie. Les commentaires du fichier JS détaillent mieux tout cela.

function vote(id_article, type_vote)
{
	// l'URL appelée pour voter
	var url = 'ajax_vote.php';
	var httpRequest = false;

	if (window.XMLHttpRequest)
	{ // Mozilla, Safari,...
		httpRequest = new XMLHttpRequest();
		if (httpRequest.overrideMimeType)
		{
			httpRequest.overrideMimeType('text/xml');
		}
	}
	else if (window.ActiveXObject)
	{ // IE
		try
		{
			httpRequest = new ActiveXObject("Msxml2.XMLHTTP");
		}
		catch (e)
		{
			try
			{
				httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
			}
			catch (e) {}
		}
	}

	if (!httpRequest)
	{
		alert('Abandon :( Impossible de créer une instance XMLHTTP');
		return false;
	}

	httpRequest.onreadystatechange = function() { alertContents(httpRequest, id_article); };
	httpRequest.open('GET', url + '?id_article=' + id_article + '&vote=' + type_vote, true);
	httpRequest.send(null);
	return true;
}



function alertContents(httpRequest, id_article)
{
	if (httpRequest.readyState == 4)
	{
		if (httpRequest.status == 200)
		{
			// Edit page content
			str = httpRequest.responseText;
			var tab = str.split('|');

			// On lit (et on parse) la réponse : 1er nombre = nombre total de votes, puis Plus, puis Moins
			var total_votes = tab[0];
			var total_votes_plus  = tab[1];
			var total_votes_moins = tab[2];

			// on met à jour la page de présentation, maintenant
			document.getElementById('numvotes_' + id_article).innerHTML = total_votes;

			// on efface les liens de vote
			document.getElementById('vote_plus_' + id_article).innerHTML = '';
			document.getElementById('vote_moins_' + id_article).innerHTML = '';

		}
		else
		{
			alert('Un problème est survenu avec la requête.');
		}
	}
}

Une démo ?

Voir une démo de ce script

Point de vue sécurisation, il faut traiter les erreurs SQL : on n'affiche pas les erreurs aux gens, on les traite... Je suppose pour cet article que vous avez bien entendu de quoi les masquer ou les traiter. Attention, il ne faut pas masquer une erreur pour le plaisir des yeux : il faut absolument la corriger...

Même si ce n'est pas explicitement écrit, dans les pages PHP vous avez fait un "require_once" sur un fichier contenant une connexion à MySQL : 4 identifiants, une ouverture de connexion.

Point de vue améliorations, on citera d'emblée (et sans hésiter) l'apparence. Oui, la démo est très sommaire mais il s'agit de montrer ce que ça donne en ligne, le reste c'est vous qui l'adaptez à vos besoins !