MySQL centralisé : affichage des requêtes et de leur durée

Les Pages Perso Chez Free

Par Otomatic, le , dans Créer ses pages perso. / Dernière modification : le par Al.
Tags : Erreur 500, MySQL, PHP, Sécurité, Tutoriaux

Le but de ce « tutoriel » n'est pas de vous apprendre à vous servir de MySQL, ni comment effectuer des requêtes et traiter les résultats1, mais de vous montrer qu'il est important d'être, parfois, « partisan du moindre effort », ce qui ne veut pas dire fainéant – qui ne fait rien – mais plutôt être capable de passer quelques heures (quatre) à préparer et automatiser une tâche, ce qui vous permettra ensuite ne pas en perdre une chaque jour à refaire exactement les mêmes choses.

Écrire une procédure de connexion à MySQL et à la base de données sur chaque page, même par copier/coller devient vite fastidieux, d'autant plus que si vous changez d'hébergeur ou de base de données, il va falloir modifier toutes les pages. Mettre des appels mysql_query(…) et tester si il n'y a pas d'erreur à chaque fois que l'on a besoin d'effectuer une requête ne permet ni de les mémoriser, ni de calculer leur durée. De plus l'affichage des erreurs éventuelles est, pour le moins, un peu nébuleuse. Pour simplifier tout ça et permettre beaucoup plus de possibilités, il faut « centraliser » les connexions à MySQL et à la base données ainsi que les requêtes, donc créer une class PHP qui regroupera toutes les possibilités d'appels à MySQL. (Voir Les classes et les objets).

Je vous propose ci-après, une class MySQL, diverses fonctions associées, la manière de gérer l'affichage des requêtes et leur durée ainsi que la gestion facilitée des erreurs. De plus, cette manière de procéder permet de pouvoir utiliser d'autres protocoles comme MySQLi (MySQL Improved) SANS avoir besoin de modifier quoi que ce soit dans tous vos scripts, la seule modification à faire sera de remplacer le type de base de données affecté à une variable, par exemple, mysql à remplacer par mysqli.

Nota : Tout ce qui suit fonctionne parfaitement en local sous WAMP et chez plusieurs hébergeurs différents avec, au minimum, PHP 5.1.3 et MySQL 5.0.51 et fonctionne toujours avec PHP 5.6.a2 et MySQL 5.5.16, en passant par PHP 5.3.x, 5.4.x et 5.5.x, donc chez Free.

Pour des questions de compatibilité avec Dotclear les codes PHP des fichiers ne représentent pas exactement le contenu. Évitez de procéder à des Copier/Coller, utilisez plutôt les fichiers complets qui sont dans ⬇ l'archive class_mysql.zip.

1. Fichier config_base.php

À mettre dans mon_site/inc/config_base.php

On va d'abord créer (ou modifier) un fichier de configuration d'accès à MySQL et à la base de données qui donne les noms d'hôte, utilisateur, mot de passe, base de données, etc. Ce fichier sera dans mon_site/inc/config_base.php

On va y ajouter la définition de deux constantes VOIR_REQUETE et OTO_DEBUG qui resteront commentées par défaut et ne seront décommentées que lorsque l'on voudra afficher les requêtes ou des informations. La troisième constante, OTO_NO_SET_NAMES, permet d'envoyer ou non une requête SET NAMES utf8 juste après la connexion à la base de données. Donc à décommenter si votre site n'est pas encodé utf-8.

<?php
//Calcul du temps de génération du script
$oto_start = microtime(true);
//Définition des variables MySQL en fonction de l'hébergeur
// Active le mode DEBUG en enlevant // du début de la ligne ci-dessous
//define('OTO_DEBUG', 1);
// Afficher en bas de page toutes les requêtes exécutées
// en décommentant la ligne suivante (Ne pas utiliser en production)
//define('VOIR_REQUETES', 1);
// Ne pas définir pour envoyer SET NAMES utf8
//define('OTO_NO_SET_NAMES', 1);
switch ($_SERVER['SERVER_NAME']) {
case "aviatechno.free.fr": //Site chez free.fr
$db_host = 'sql.free.fr';
$db_name = 'database_free.fr';
$db_username = 'user_free.fr';
$db_password = 'mot_de_passe_sql_free';
$database_type = "mysql";
break;
case "aviatechno": //Site local WAMP
$db_host = 'localhost';
$db_name = 'database_locale';
$db_username = 'user_local';
$db_password = 'mot_de_passe_local';
$database_type = "mysqli";
break;
case "aviatechno.net": //Site chez Gandi
$db_host = 'localhost';
$db_name = 'database_gandi';
$db_username = 'user_gandi';
$db_password = 'mot_de_passe_gandi';
$database_type = "mysqli";
break;
default:
$db_host = NULL;
$db_name = NULL;
$db_username = NULL;
$db_password = NULL;
}
?>

Le grand avantage du fichier de configuration des accès à la base de données est que l'on peut ajouter autant d'hébergeurs ou de noms de sites que nécessaire. En ce qui me concerne, et vu le grand nombre d'essais que j'effectue en local et avec des sous-domaines, il y a douze entrées.

2. Fichier mysql.php

À mettre dans mon_site/inc/dbchoix/mysql.php.

Fichier contenant la création de la « class » et la connexion à MySQL et à la base de donnée définie dans le fichier config_base.php.

Cette « class » contient beaucoup de possibilités, dont la création de champ, de tables et leurs modifications.

<?php
//On s'assure que MySQL est supporté
if (!function_exists('mysql_connect')) exit('Cet environnement ne supporte pas MySQL qui est requis pour utiliser cette classe');
class Data_Base {
public $prefix;
public $link_id;
public $query_result;
public $saved_queries = array();
public $num_queries = 0;
public $error_no = false;
public $error_msg = 'Inconnu';
//Connexion à Mysql puis à la base de données
function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix = "") {
$this->prefix = $db_prefix;
$this->link_id = @mysql_connect($db_host, $db_username, $db_password);
if ($this->link_id) {
if (@mysql_select_db($db_name, $this->link_id)) {
// Setup the client-server character set
if (!defined('OTO_NO_SET_NAMES'))
$this->set_names('utf8');
return $this->link_id;
}
else error('Impossible de se connecter à la base de données. MySQL dit : '.mysql_error(), __FILE__, __LINE__);
}
else error('Impossible de se connecter au serveur MySql. MySQL dit : '.mysql_error(), __FILE__, __LINE__);
}
function query($sql) {
if (strlen($sql) > 50000) exit('Requête bien trop importante.');
if (defined('VOIR_REQUETES')) $q_start = microtime(true);
$this->query_result = @mysql_query($sql, $this->link_id);
if ($this->query_result) {
if (defined('VOIR_REQUETES')) $this->saved_queries[] = array($sql, (microtime(true) - $q_start));
++$this->num_queries;
return $this->query_result;
}
else {
if (defined('VOIR_REQUETES')) $this->saved_queries[] = array($sql, 0);
$this->error_no = @mysql_errno($this->link_id);
$this->error_msg = @mysql_error($this->link_id);
return false;
}
}
function result($query_id = 0, $row = 0) {
return ($query_id) ? @mysql_result($query_id, $row) : false;
}
function fetch_assoc($query_id = 0) {
return ($query_id) ? @mysql_fetch_assoc($query_id) : false;
}
function fetch_row($query_id = 0) {
return ($query_id) ? @mysql_fetch_row($query_id) : false;
}
function num_rows($query_id = 0) {
return ($query_id) ? @mysql_num_rows($query_id) : false;
}
function affected_rows() {
return ($this->link_id) ? @mysql_affected_rows($this->link_id) : false;
}
function insert_id() {
return ($this->link_id) ? @mysql_insert_id($this->link_id) : false;
}
function get_num_queries() {
return $this->num_queries;
}
function get_saved_queries() {
return $this->saved_queries;
}
function free_result($query_id = false) {
return ($query_id) ? @mysql_free_result($query_id) : false;
}
function escape($str) {
if (is_array($str)) return '';
else if (function_exists('mysql_real_escape_string')) return mysql_real_escape_string($str, $this->link_id);
else return mysql_escape_string($str);
}
function error() {
$result['error_sql'] = @current(@end($this->saved_queries));
$result['error_no'] = @mysql_errno($this->link_id);
$result['error_msg'] = @mysql_error($this->link_id);
return $result;
}
function close() {
if ($this->link_id) {
if ($this->query_result) @mysql_free_result($this->query_result);
return @mysql_close($this->link_id);
}
else return false;
}
function get_names() {
$result = $this->query('SHOW VARIABLES LIKE \'character_set_connection\'');
return $this->result($result, 0, 1);
}
function set_names($names) {
return $this->query('SET NAMES \''.$this->escape($names).'\'');
}
function get_version() {
$result = $this->query('SELECT VERSION()');
return array(
'name'		=> 'MySQL Standard',
'version'	=> preg_replace('%^([^-]+).*$%', '\\1', $this->result($result))
);
}
function table_exists($table_name, $no_prefix = false) {
$result = $this->query('SHOW TABLES LIKE \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\'');
return $this->num_rows($result) > 0;
}
function field_exists($table_name, $field_name, $no_prefix = false)	{
$result = $this->query('SHOW COLUMNS FROM '.($no_prefix ? '' : $this->prefix).$table_name.' LIKE \''.$this->escape($field_name).'\'');
return $this->num_rows($result) > 0;
}
function index_exists($table_name, $index_name, $no_prefix = false)	{
$exists = false;
$result = $this->query('SHOW INDEX FROM '.($no_prefix ? '' : $this->prefix).$table_name);
while ($cur_index = $this->fetch_assoc($result))	{
if (strtolower($cur_index['Key_name']) == strtolower(($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name)) {
$exists = true;
break;
}
}
return $exists;
}
function create_table($table_name, $schema, $no_prefix = false)	{
if ($this->table_exists($table_name, $no_prefix))
return true;
$query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n";
// Go through every schema element and add it to the query
foreach ($schema['FIELDS'] as $field_name => $field_data)	{
$field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']);
$query .= $field_name.' '.$field_data['datatype'];
if (isset($field_data['collation']))
$query .= 'CHARACTER SET utf8 COLLATE utf8_'.$field_data['collation'];
if (!$field_data['allow_null'])
$query .= ' NOT NULL';
if (isset($field_data['default']))
$query .= ' DEFAULT '.$field_data['default'];
$query .= ",\n";
}
// If we have a primary key, add it
if (isset($schema['PRIMARY KEY']))
$query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n";
// Add unique keys
if (isset($schema['UNIQUE KEYS'])) {
foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields)
$query .= 'UNIQUE KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$key_name.'('.implode(',', $key_fields).'),'."\n";
}
// Add indexes
if (isset($schema['INDEXES'])) {
foreach ($schema['INDEXES'] as $index_name => $index_fields)
$query .= 'KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.'('.implode(',', $index_fields).'),'."\n";
}
// We remove the last two characters (a newline and a comma) and add on the ending
$query = substr($query, 0, strlen($query) - 2)."\n".') ENGINE = '.(isset($schema['ENGINE']) ? $schema['ENGINE'] : 'MyISAM').' CHARACTER SET utf8';
return $this->query($query) ? true : false;
}
function drop_table($table_name, $no_prefix = false) {
if (!$this->table_exists($table_name, $no_prefix))
return true;
return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false;
}
function rename_table($old_table, $new_table, $no_prefix = false)	{
// If there new table exists and the old one doesn't, then we're happy
if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix))
return true;
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false;
}
function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false)	{
if ($this->field_exists($table_name, $field_name, $no_prefix))
return true;
$field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type);
if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value))
$default_value = '\''.$this->escape($default_value).'\'';
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type.($allow_null ? ' ' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : ' ').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false;
}
function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) {
if (!$this->field_exists($table_name, $field_name, $no_prefix))
return true;
$field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type);
if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value))
$default_value = '\''.$this->escape($default_value).'\'';
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' MODIFY '.$field_name.' '.$field_type.($allow_null ? ' ' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : ' ').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false;
}
function drop_field($table_name, $field_name, $no_prefix = false)	{
if (!$this->field_exists($table_name, $field_name, $no_prefix))
return true;
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false;
}
function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false)	{
if ($this->index_exists($table_name, $index_name, $no_prefix))
return true;
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ('.implode(',', $index_fields).')') ? true : false;
}
function drop_index($table_name, $index_name, $no_prefix = false)	{
if (!$this->index_exists($table_name, $index_name, $no_prefix))
return true;
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false;
}
function truncate_table($table_name, $no_prefix = false) {
return $this->query('TRUNCATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false;
}
}
//Connection à MySql et à la base de données
//Calcul du temps de connexion MySQL et base de données
$oto_start_mysql = microtime(true);
$db = new Data_Base($db_host, $db_username, $db_password, $db_name, $db_prefix);
$oto_stop_mysql = microtime(true);
define('TYPE_BASE','MySQL');
?>

Cette class est écrite de telle manière qu'il n'y ait que très peu de changement par rapport aux appels classiques mysql_xxx_yyy :

$result = mysql_query($sql); devient $result = $db->query($sql);
$row = mysql_fetch_row($result); devient $row = $db->fetch_row($result);

et ainsi de suite, ce qui permet facilement les remplacements multi-fichiers.

3. Fichier dbcommun.php

À mettre dans mon_site/inc/dbchoix/dbcommun.php

Nous avons la « class » MySQL et le fichier de configuration, maintenant, nous avons besoin d'un script pour choisir le type gestionnaire de base de données en fonction de ce qui a été défini pour $database_type dans le fichier de configuration, ainsi que le chargement de quelques fonctions pour l'utilisation MySQL et l'affichage des erreurs.

<?php
//Choix type de base de données
include_once(DOSSIER.'inc/dbchoix/mysql_fonctions.php');
if(!isset($database_type))
$database_type = "mysql";
if($database_type == "mysqli") {
//On s'assure que MySQLi est supporté - Sinon basculement sur mysql
if (!function_exists('mysqli_connect'))	{
$database_type = "mysql";
}
}
switch($database_type) {
case "mysql":
include_once(DOSSIER.'inc/dbchoix/mysql.php');
break;
case "mysqli":
include_once(DOSSIER.'inc/dbchoix/mysqli.php');
break;
default:
exit ("Type de base de données non défini ou non supporté");
break;
}
?>

Dans ce script, on peut voir deux particularités :

3a. Utilisation de la constante DOSSIER

Cette constante est définie au début de chaque page (Je dis "page web" et non fichier) et contient le chemin nécessaire pour revenir à la racine du site, par exemple :

  • Une page située à la racine du site contient
    define('DOSSIER', '');
    car elle est à la racine.
  • Une page située dans un dossier de premier niveau, par exemple mon_site/dossier1/ma_page.php contient
    define('DOSSIER', '../');
    car il faut remonter d'un niveau pour arriver à la racine.
  • Une page située dans un dossier de second niveau, par exemple mon_site/dossier1/dossier2/ma_page.php contient
    define('DOSSIER', '../../');
    car il faut remonter de deux niveaux pour arriver à la racine.

Dans d'autres scripts ou applications Web ou CMS, on peut avoir :

  • define('MY_ROOT', dirname(__FILE__).'/');
  • ou une variable $chem_absolu = "../";

Il vous faut donc adapter le nom DOSSIER et/ou son contenu à ce que vous avez l'habitude de pratiquer sur vos sites.

3b. Chargement d'un fichier mysql_fonctions.php

4. Fichier mysql_fonctions.php

À mettre dans mon_site/inc/dbchoix/mysql_fonctions.php

Définitions de deux fonctions : affichage des requêtes et la fonction error

<?php
function voir_requetes_memorisees() {
global $db ;
//Récupère les requêtes sauvegardées
$saved_queries = $db->get_saved_queries();
$nb_queries = count($saved_queries);
?>
<div style='width:760px;margin:5px auto;'>
<h2 style='margin:0;color:#FFFFFF;background-color:#B84623;font-size:1.1em; padding:4px;'><span>Informations d'accès à la base de données</span></h2>
<table summary='Requêtes exécutées' style='border:1px solid #CC9966;border-collapse:collapse;margin:0 auto;background-color:#fbf4e2;padding:0;width:100%'>
<thead>
<tr>
<th style='border:1px solid #CC9966;line-height:120%;padding:5px;font-size:0.80em;text-align:center;'>Durée (sec)</th>
<th style='border:1px solid #CC9966;line-height:120%;padding:5px;font-size:0.80em;text-align:center;'>Requête</th>
</tr>
</thead>
<tbody>
<?php
$query_time_total = 0.0;
if($nb_queries > 0) {
foreach ($saved_queries as $cur_query)	{
$query_time_total += $cur_query[1];
?>
<tr>
<td style='border:1px solid #CC9966;line-height:120%;padding:4px 2px;font-size:0.70em;text-align:left;'><?php echo ($cur_query[1] != 0) ? sprintf('%.6f',$cur_query[1]) : ' ' ?></td>
<td style='border:1px solid #CC9966;line-height:120%;padding:4px 2px;font-size:0.70em;text-align:left;'><?php echo htmlspecialchars($cur_query[0]) ?></td>
</tr>
<?php
}
?>
<tr>
<td style='border:1px solid #CC9966;line-height:120%;padding:4px 2px;font-size:0.70em;text-align:center;' colspan="2">Temps total des requêtes : <?php echo sprintf('%.6f',$query_time_total) ?></td>
</tr>
<?php
}
else {
?>
<tr>
<td style='border:1px solid #CC9966;line-height:120%;padding:4px 2px;font-size:0.70em;text-align:left;' colspan="2">Aucune requête effectuée sur cette page</td>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<?php
}
// Affiche un message d'erreur
function error($message, $file = null, $line = null, $db_error = false) {
global $db;
// Vide les tampons
while (@ob_end_clean());
// Redémarre le buffer
if (extension_loaded('zlib'))
ob_start('ob_gzhandler');
// headers no-cache
header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :)
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache'); // For HTTP/1.0 compatibility
header('Content-type: text/html; charset=utf-8');
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv='Content-Language' content='fr'>
<title>Erreur sur le site</title>
<style type="text/css">
<!--
body {margin: 10% 20% auto 20%; font: 10px Verdana, Arial, Helvetica, sans-serif}
#errorbox {border: 1px solid #B84623}
h2 {margin: 0; color: #FFFFFF; background-color: #B84623; font-size: 1.1em; padding: 5px 4px}
#errorbox div {padding: 6px 5px; background-color: #F1F1F1}
-->
</style>
</head>
<body>
<div id="errorbox">
<h2>Une erreur est survenue</h2>
<div>
<?php
if (defined('OTO_DEBUG') && !is_null($file) && !is_null($line)) {
echo "\t\t".'<strong>Fichier:</strong> '.$file.'<br>'."\n\t\t".'<strong>Line:</strong> '.$line.'<br /><br />'."\n\t\t".'<strong>Message d\'erreur</strong>: '.$message."\n";
if ($db_error) {
echo "\t\t".'<br /><br /><strong>Message base de données :</strong> '.htmlspecialchars($db_error['error_msg']).(($db_error['error_no']) ? ' (Errno: '.$db_error['error_no'].')' : '')."\n";
if ($db_error['error_sql'] != '')
echo "\t\t".'<br /><br /><strong>Défaut requête :</strong> '.htmlspecialchars($db_error['error_sql'])."\n";
}
}
else
echo "\t\t".'Erreur: <strong>'.$message.'.</strong>'."\n";
?>
</div>
</div>
</body>
</html>
<?php
// Si une connexion base de données déjà établie, on la ferme
if ($db_error)
$GLOBALS['db']->close();
exit;
}
?>

La mise en page du tableau des requêtes est définie en interne de la fonction. Vous pouvez personnaliser l'affichage en modifiant les valeurs des styles.

5. Maintenant, il faut appeler les fichiers

dans le bon ordre au début de chaque page Web.

Donc, mettre au début de chaque page :

include(DOSSIER.'inc/config_base.php');
include(DOSSIER.'inc/dbchoix/dbcommun.php');

Arrivé là VOUS ÊTES CONNECTÉ à MySQL ET à la base de données et le nom de l'objet MySQL est $db. Donc, toutes les requêtes et traitements vers MySQL devront être de la forme $result = $db->fonction(paramètres); ce qui pourrait donner dans une page, après l'entête ci-dessus :

…
$sql = "SELECT m_date AS maj, UNIX_TIMESTAMP(m_date) AS modified FROM avia_modifs ORDER BY m_date DESC LIMIT 1";
$result = $db->query($sql) or error('Extraction date impossible', __FILE__, __LINE__, $db->error());
$row = $db->fetch_assoc($result);
extract($row);
…
$sql = "SELECT m_type AS type, m_texte AS texte FROM avia_modifs ORDER BY m_id DESC LIMIT 10";
$result = $db->query($sql) or error('Extraction dernières modifications impossible', __FILE__, __LINE__, $db->error());
while($row = $db->fetch_assoc($result)) {
extract($row);
…

Remarque : Si la ligne include(DOSSIER.'inc/config_base.php'); n'est pas au tout début de vos pages, le calcul du temps total d'exécution du script sera un peu faussé.

6. Fichier pied_requete.php

À mettre dans mon_site/inc/pied_requete.php

Si on veut voir toutes les requêtes exécutées dans une page avec la durée de chacune il va falloir, là aussi, ajouter un peu de code.

<?php
// Conversion taille en unités compréhensibles
function taille_fichier($size) {
$units = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB');
for ($i = 0; $size > 1024; $i++)
$size /= 1024;
return round($size, 2).' '.$units[$i];
}
// Display queries (if enabled/defined)
if (defined('VOIR_REQUETES')) {
if(function_exists('voir_requetes_memorisees'))
voir_requetes_memorisees();
}
// Display debug info (if enabled/defined)
if (defined('OTO_DEBUG')) {
echo '<p style="line-height:95%;font-size:0.70em;margin:4px auto;padding:0 0.5em;color:#993300;text-align:center;">[ ';
// Calculate mysql connect time
$time_diff_mysql = sprintf('%.4f', $oto_stop_mysql - $oto_start_mysql);
// Calculate script generation time
$time_diff = sprintf('%.4f', microtime(true) - $oto_start);
echo sprintf('Page en %1$s secondes, connexion '.TYPE_BASE.' en %3$s sec, %2$s requêtes', $time_diff, $db->get_num_queries(),$time_diff_mysql);
if (function_exists('memory_get_usage')) {
echo ' - '.sprintf('Utilisation de la mémoire : %1$s', taille_fichier(memory_get_usage()));
if (function_exists('memory_get_peak_usage'))
echo ' '.sprintf('(pic d\'utilisation : %1$s)', taille_fichier(memory_get_peak_usage()));
}
echo ' ]</p>'."\n";
}
if(isset($db)) $db->close();
?>

Et, ce code, il faut l'insérer JUSTE avant la fin de chaque page par :

include(DOSSIER.'inc/pied_requete.php');

7. Comment afficher les requêtes et les détails

On est arrivé à la fin de l'écriture du code, nous allons donc voir comment afficher la liste des requêtes à la fin d'une page.

On va « décommenter » la ligne //define('VOIR_REQUETES', 1); donc supprimer les // du début de ligne, du fichier config_base.php, et, après exécution de la page, voilà ce qu'on peut obtenir en bas de page :

Affichage erreur

Si la durée d'une requête est inférieure à une microseconde, il n'y aura rien d'affiché dans la colonne durée car la résolution de la mesure du temps écoulé est une microseconde. Et, si la durée de chaque requête est inférieure à une microseconde, le temps total ne pourra pas être affiché.

On va recommenter la ligne //define('VOIR_REQUETES', 1); donc ajouter les // du début de ligne, du fichier config_base.php.

Puis on va décommenter la ligne //define('OTO_DEBUG', 1); donc supprimer les // du début de ligne, du fichier config_base.php, et, après exécution de la page voilà ce qu'on peut obtenir en bas de page :

[ Page en 0,0312 secondes, connexion MySQLi en 0,0156 sec, 7 requêtes - Utilisation de la mémoire : 445,61 KiB (pic d'utilisation : 465,02 KiB) ]

Donc, la durée d'exécution pour générer la page, le temps mis pour se connecter à MySQL et à la base de données, le nombre de requêtes et l'utilisation mémoire par PHP.

En fonctionnement normal, en production (Site réel) les modes VOIR_REQUETES et OTO_DEBUG ne doivent être utilisés qu'occasionnellement, par exemple pour détecter l'origine d'un problème.

8. Affichage des erreurs et des détails

En fonctionnement normal :

Si la gestion des erreurs MySQL dans le codage des appels à MySQL est effectuée correctement, c'est-à-dire du style :

$result = $db->query($sql) or error('Message si erreur', __FILE__, __LINE__, $db->error());

en cas d'erreur, on obtiendra quelque chose de ce genre :

Affichage erreur

Il n'y a aucun détail et c'est suffisant pour les visiteurs.

Si le webmaistre veut avoir quelques détails supplémentaires, il « suffit » de décommenter le ligne //define('OTO_DEBUG', 1); du fichier config_base.php pour obtenir des informations supplémentaires sur l'erreur :

Affichage erreur

Et si en plus le webmaistre décommente la ligne //define('VOIR_REQUETES', 1); du fichier config_base.php il obtiendra tous les détails de l'erreur, dont la requête elle-même :

Affichage erreur

On peut donc voir clairement qu'il y a une erreur de syntaxe dans la requête, avec un espace superflu dans OR DER au lieu de ORDER.

Nota : Les messages d'erreurs MySQL sont en français sur mon serveur MySQL local.

C'était un peu long et j'espère que ça vous sera profitable.

Crédits : Class et fonctions inspirées par PunBB/FluxBB.

Cerise qur le gâteau, bien que chez Free MySQLi (MySQL Improved) ne soit pas supporté, voici la « class » MySQLi.

9. Fichier mysqli.php

À mettre dans "mon_site/inc/dbchoix/mysqli.php"

<?php
//On s'assure que MySQLi est supporté
if (!function_exists('mysqli_connect'))	exit('Cet environnement ne supporte pas MySQLi (MySQL Improved) qui est requis pour utiliser cette classe');
class Data_Base {
public $prefix;
public $link_id;
public $query_result;
public $saved_queries = array();
public $num_queries = 0;
public $error_no = false;
public $error_msg = 'Inconnu';
//Connexion à Mysqli puis à la base de données
function __construct($db_host, $db_username, $db_password, $db_name, $db_prefix = "") {
$this->prefix = $db_prefix;
$this->link_id = @mysqli_connect($db_host, $db_username, $db_password, $db_name);
if (!$this->link_id)
error('Impossible de se connecter à MySQL et à la base de données. MySQL dit : '.mysqli_connect_error(), __FILE__, __LINE__);
// Setup the client-server character set
if (!defined('OTO_NO_SET_NAMES'))
$this->set_names('utf8');
return $this->link_id;
}
function query($sql) {
if (strlen($sql) > 50000) exit('Requête bien trop importante.');
if (defined('VOIR_REQUETES')) $q_start = microtime(true);
$this->query_result = @mysqli_query($this->link_id, $sql);
if ($this->query_result) {
if (defined('VOIR_REQUETES')) $this->saved_queries[] = array($sql, sprintf('%.5f', microtime(true) - $q_start));
++$this->num_queries;
return $this->query_result;
}
else {
if (defined('VOIR_REQUETES')) $this->saved_queries[] = array($sql, 0);
$this->error_no = @mysqli_errno($this->link_id);
$this->error_msg = @mysqli_error($this->link_id);
return false;
}
}
function result($query_id = 0, $row = 0, $col = 0) {
if ($query_id) {
if ($row !== 0 && @mysqli_data_seek($query_id, $row) === false) return false;
$cur_row = @mysqli_fetch_row($query_id);
if ($cur_row === false) return false;
return $cur_row[$col];
}
else return false;
}
function fetch_assoc($query_id = 0)	{
return ($query_id) ? @mysqli_fetch_assoc($query_id) : false;
}
function fetch_row($query_id = 0)	{
return ($query_id) ? @mysqli_fetch_row($query_id) : false;
}
function num_rows($query_id = 0) {
return ($query_id) ? @mysqli_num_rows($query_id) : false;
}
function affected_rows() {
return ($this->link_id) ? @mysqli_affected_rows($this->link_id) : false;
}
function insert_id()	{
return ($this->link_id) ? @mysqli_insert_id($this->link_id) : false;
}
function get_num_queries() {
return $this->num_queries;
}
function get_saved_queries()	{
return $this->saved_queries;
}
function free_result($query_id = false)	{
return ($query_id) ? @mysqli_free_result($query_id) : false;
}
function escape($str)	{
return is_array($str) ? '' : mysqli_real_escape_string($this->link_id, $str);
}
function error() {
$result['error_sql'] = @current(@end($this->saved_queries));
$result['error_no'] = $this->error_no;
$result['error_msg'] = $this->error_msg;
return $result;
}
function close()	{
if ($this->link_id)	{
if ($this->query_result) @mysqli_free_result($this->query_result);
return @mysqli_close($this->link_id);
}
else return false;
}
function get_names()	{
$result = $this->query('SHOW VARIABLES LIKE \'character_set_connection\'');
return $this->result($result, 0, 1);
}
function set_names($names)	{
return $this->query('SET NAMES \''.$this->escape($names).'\'');
}
function get_version()	{
$result = $this->query('SELECT VERSION()');
return array(
'name'		=> 'MySQL Improved',
'version'	=> preg_replace('%^([^-]+).*$%', '\\1', $this->result($result))
);
}
function table_exists($table_name, $no_prefix = false)	{
$result = $this->query('SHOW TABLES LIKE \''.($no_prefix ? '' : $this->prefix).$this->escape($table_name).'\'');
return $this->num_rows($result) > 0;
}
function field_exists($table_name, $field_name, $no_prefix = false)	{
$result = $this->query('SHOW COLUMNS FROM '.($no_prefix ? '' : $this->prefix).$table_name.' LIKE \''.$this->escape($field_name).'\'');
return $this->num_rows($result) > 0;
}
function index_exists($table_name, $index_name, $no_prefix = false)	{
$exists = false;
$result = $this->query('SHOW INDEX FROM '.($no_prefix ? '' : $this->prefix).$table_name);
while ($cur_index = $this->fetch_assoc($result)) {
if (strtolower($cur_index['Key_name']) == strtolower(($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name)) {
$exists = true;
break;
}
}
return $exists;
}
function create_table($table_name, $schema, $no_prefix = false)	{
if ($this->table_exists($table_name, $no_prefix))
return true;
$query = 'CREATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name." (\n";
// Go through every schema element and add it to the query
foreach ($schema['FIELDS'] as $field_name => $field_data) {
$field_data['datatype'] = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_data['datatype']);
$query .= $field_name.' '.$field_data['datatype'];
if (isset($field_data['collation']))
$query .= 'CHARACTER SET utf8 COLLATE utf8_'.$field_data['collation'];
if (!$field_data['allow_null'])
$query .= ' NOT NULL';
if (isset($field_data['default']))
$query .= ' DEFAULT '.$field_data['default'];
$query .= ",\n";
}
// If we have a primary key, add it
if (isset($schema['PRIMARY KEY']))
$query .= 'PRIMARY KEY ('.implode(',', $schema['PRIMARY KEY']).'),'."\n";
// Add unique keys
if (isset($schema['UNIQUE KEYS'])) {
foreach ($schema['UNIQUE KEYS'] as $key_name => $key_fields)
$query .= 'UNIQUE KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$key_name.'('.implode(',', $key_fields).'),'."\n";
}
// Add indexes
if (isset($schema['INDEXES'])) {
foreach ($schema['INDEXES'] as $index_name => $index_fields)
$query .= 'KEY '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.'('.implode(',', $index_fields).'),'."\n";
}
// We remove the last two characters (a newline and a comma) and add on the ending
$query = substr($query, 0, strlen($query) - 2)."\n".') ENGINE = '.(isset($schema['ENGINE']) ? $schema['ENGINE'] : 'MyISAM').' CHARACTER SET utf8';
return $this->query($query) ? true : false;
}
function drop_table($table_name, $no_prefix = false) {
if (!$this->table_exists($table_name, $no_prefix))
return true;
return $this->query('DROP TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false;
}
function rename_table($old_table, $new_table, $no_prefix = false)	{
// If the new table exists and the old one doesn't, then we're happy
if ($this->table_exists($new_table, $no_prefix) && !$this->table_exists($old_table, $no_prefix))
return true;
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$old_table.' RENAME TO '.($no_prefix ? '' : $this->prefix).$new_table) ? true : false;
}
function add_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false)	{
if ($this->field_exists($table_name, $field_name, $no_prefix))
return true;
$field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type);
if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value))
$default_value = '\''.$this->escape($default_value).'\'';
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false;
}
function alter_field($table_name, $field_name, $field_type, $allow_null, $default_value = null, $after_field = null, $no_prefix = false) {
if (!$this->field_exists($table_name, $field_name, $no_prefix))
return true;
$field_type = preg_replace(array_keys($this->datatype_transformations), array_values($this->datatype_transformations), $field_type);
if (!is_null($default_value) && !is_int($default_value) && !is_float($default_value))
$default_value = '\''.$this->escape($default_value).'\'';
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' MODIFY '.$field_name.' '.$field_type.($allow_null ? '' : ' NOT NULL').(!is_null($default_value) ? ' DEFAULT '.$default_value : '').(!is_null($after_field) ? ' AFTER '.$after_field : '')) ? true : false;
}
function drop_field($table_name, $field_name, $no_prefix = false)	{
if (!$this->field_exists($table_name, $field_name, $no_prefix))
return true;
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP '.$field_name) ? true : false;
}
function add_index($table_name, $index_name, $index_fields, $unique = false, $no_prefix = false)	{
if ($this->index_exists($table_name, $index_name, $no_prefix))
return true;
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' ADD '.($unique ? 'UNIQUE ' : '').'INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name.' ('.implode(',', $index_fields).')') ? true : false;
}
function drop_index($table_name, $index_name, $no_prefix = false)	{
if (!$this->index_exists($table_name, $index_name, $no_prefix))
return true;
return $this->query('ALTER TABLE '.($no_prefix ? '' : $this->prefix).$table_name.' DROP INDEX '.($no_prefix ? '' : $this->prefix).$table_name.'_'.$index_name) ? true : false;
}
function truncate_table($table_name, $no_prefix = false)	{
return $this->query('TRUNCATE TABLE '.($no_prefix ? '' : $this->prefix).$table_name) ? true : false;
}
}
//Connection à MySql et à la base de données
//Calcul du temps de connexion MySQL et base de données
$oto_start_mysql = microtime(true);
$db = new Data_Base($db_host, $db_username, $db_password, $db_name);
$oto_stop_mysql = microtime(true);
define('TYPE_BASE','MySQLi');
?>

Pour passer de MySQL à MySQLi, la SEULE chose à faire est de remplacer $database_type = "mysql"; par $database_type = "mysqli"; pour le site afférent dans le fichier config_base.php. Il n'y a absolument RIEN d'autre à modifier dans tous vos scripts PHP.

De la même manière, on pourrait ajouter d'autres « class » pour supporter d'autres types de bases de données comme pgsql ou sqlite, mais c'est une autre aventure.

Note :
1 Ce ne sont pas les tutoriels qui manquent sur ce sujet, par exemple : Concevez votre site Web avec PHP et MySQL