Vous êtes sur la page 1sur 45

Chapitre 5 Manipulation de bases de données MySQL

5.1 Introduction

5.1.1 Un SGBD, trois extensions

PHP n’intègre pas nativement les fonctionnalités d’accès aux bases de données. Il utilise pour
cela des extensions, qui s’appuient elles-mêmes sur les pilotes (drivers). PHP compte trois
extensions dédiées à la manipulation de bases de données MySQL : l’API MySQL originale,
MySQLi et PDO.
Le manuel PHP fournit une comparaison détaillée de ces trois extensions. Voici ce qu’il faut
retenir.

L’API MySQL originale présente une interface procédurale dont toutes les fonctions
commencent par « mysql_ ». Elle fut la seule API MySQL disponible dans PHP durant de
nombreuses années et l’on trouve énormément de code sur Internet qui l’emploie.
Cependant, cette API ne prend en charge les fonctionnalités de MySQL que jusqu’à la version
4.1.3, sortie en 2004. Son développement est arrêté depuis lors, au profit de l’extension
MySQLi qui a été introduite par PHP 5.0 la même année. L’API MySQL, obsolète depuis
longtemps, a ensuite été purement et simplement retirée de PHP à compter de la version 7.0
du langage, sortie fin 2015. Elle ne doit donc plus être utilisée, sauf pour réaliser des
opérations de maintenance sur des sites anciens.

MySQLi est l’extension recommandée pour l’accès aux bases de données MySQL depuis la
sortie de PHP 5.0 jusqu’à aujourd’hui. C’est une API orientée objet à laquelle il a été ajouté
une interface procédurale, de manière à en faciliter l’adoption par les développeurs habitués à
l’API MySQL originale. Pour chaque méthode de l’interface objet, il existe donc une fonction
équivalente utilisable en style procédural et commençant par « mysqli_ » (ex : la fonction
mysqli_query). L’extension MySQLi comporte six classes :
 mysqli : représente une connexion entre un programme PHP (client) et une base de
données MySQL (serveur),
 mysqli_smt : représente une requête préparée,
 mysqli_result : représente un jeu de résultats renvoyé par une requête,
 mysqli_warning : représente une alerte/un avertissement MySQL,
 mysqli_exception : représente une exception MySQL,
 mysqli_driver : représente le pilote utilisé par l’extension.

PDO est l’acronyme de Portable Data Objects. Disponible depuis PHP 5.1, PDO n’est pas une
extension de PHP comme les autres : c’est une couche d’abstraction qui propose une interface
commune pour tous les SGBD les plus populaires, à commencer par MySQL. Ainsi, avec PDO,
c’en est terminé des fonctions mysqli_query pour MySQL, sqlite_query pour SQLite, pg_query

91
pour PostgreSQL, etc. Une seule méthode permet d’adresser une requête à tous les SGBD
supportés : PDO::query. Utiliser PDO présente donc deux avantages majeurs :
 pour le développeur, il n’y a qu’une seule interface PHP à maîtriser pour accéder à la
plupart des bases de données du marché,
 pour son client, le code d’un site Web n’étant pas lié à un SGBD particulier, il est
relativement facile de changer de type de base de données si le besoin s’en fait sentir.

Avec de tels atouts, PDO est devenue l’extension de PHP pour MySQL la plus couramment
utilisée de nos jours. Le tableau ci-dessous, extrait du manuel PHP et complété par nos soins,
montre que PDO est certes un peu moins complète que MySQLi (et les autres extensions
dédiées auxquelles elle peut se substituer) : l’universalité qu’elle propose est à ce prix.

Extension MySQL MySQLi PDO


Introduite en PHP version 2.0 5.0 5.1
Incluse avec PHP 5.x Oui Oui Oui
Incluse avec PHP 7.x Non Oui Oui
Statut du développement Maintenance Active Active
Cycle de vie Obsolète Active Active
Recommandé pour de nouveaux projets Non Oui Oui
Interface orientée objet Non Oui Oui
Interface procédurale Oui Oui Non
L'API supporte les requêtes non-bloquantes, asynchrones avec
Non Oui Non
mysqlnd
Connexions persistantes disponibles Oui Oui Oui
L'API supporte les jeux de caractères Oui Oui Oui
L'API supporte les requêtes préparées côté serveur Non Oui Oui
L'API supporte les requêtes préparées côté client Non Non Oui
L'API supporte les procédures stockées Non Oui Oui
L'API supporte les requêtes multiples Non Oui La plupart
L'API supporte les transactions Non Oui Oui
Les transactions peuvent être contrôlées avec SQL Oui Oui Oui
Supporte toutes les fonctionnalités de MySQL 5.1+ Non Oui La plupart

PDO propose une interface orientée objet uniquement. Elle comporte trois classes :
 PDO : représente une connexion entre un programme PHP et un serveur de base de
données,
 PDOStatement : représente une requête préparée, un jeu de résultats d’une requête,
ou les deux,
 PDOException : représente une exception lancée par PDO.

92
Dans la suite de ce chapitre, nous étudierons successivement la connexion à un serveur
MySQL, les requêtes simples sur une base de données, les requêtes préparées et les
transactions. Pour chaque thème abordé, une solution utilisant MySQLi et une solution
utilisant PDO seront présentées en détail.

5.1.2 Présentation de la base de données bibliographique

Dans ce chapitre comme dans le suivant, nous utilisons une base de données bibliographique
qui comporte trois tables :

auteur (id_auteur*, nom, prenom)


editeur (id_editeur*, nom)
livre (id_livre*, id_auteur, id_editeur, titre, date_publication, pages)

Dans la table livre, une contrainte d’intégrité référentielle est posée sur l’identifiant de
l’auteur et sur celui de l’éditeur, grâce à la déclaration de clés étrangères.

Voici le contenu des tables de cette base :

auteur

id_auteur nom prenom


1 Engels Jean
2 Thuillier Victor
3 Heurtel Olivier
4 Brison Carl
5 Rollet Olivier
6 Soutou Christian
7 Bisson Anne-Christine
8 Nixon Robin
9 Martinez Nicolas

editeur

id_editeur nom
1 ENI
2 Eyrolles
3 O'Reilly
4 Ellipses

93
livre

id_livre id_auteur id_editeur titre date_publication pages


PHP 7 - Développer un site Web
1 3 1 2018-09-12 910
dynamique et interactif
2 6 2 SQL pour Oracle 2015-04-02 644
3 9 1 Apache 2.4 : Installation et configuration 2015-06-10 430
4 7 1 SQL - Les fondamentaux du langage 2017-12-13 409
5 1 2 PHP 7 : Cours et exercices 2017-02-17 585
J'apprends facilement le PHP, la
6 4 4 programmation orientée objet et la classe 2018-05-29 240
PDO
7 6 2 Programmer avec MySQL 2017-06-01 523
Développer un site web en PHP, MySQL
8 8 3 2015-11-05 778
et JavaScript
9 2 2 Programmez en orienté objet en PHP 2017-05-18 456
Apprendre à développer un site web avec
10 5 1 PHP et MySQL - Exercices pratiques et 2018-08-16 592
corrigés

Cette base de données s’appelle « exercices_php », est accessible sur la machine « localhost »,
grâce à l'identifiant « script_php » et au mot de passe « zh6tjPp6T56N4dbF ».

Le script de création de cette base MySQL est fourni dans une archive ZIP, qui contient un
fichier nommé « base_livres_UTF-8.sql ».

Pour pouvoir tester les exemples à suivre, vous devez ajouter cette base de données à votre
serveur MySQL local. En utilisant le gestionnaire phpMyadmin par exemple, il faut :
1. créer une nouvelle base de données (colonne de gauche) et la nommer
« exercices_php »,
2. sélectionner cette base (cliquer sur son nom dans la colonne de gauche),
3. importer le fichier « base_livres_UTF-8.sql » (colonne de droite > onglet Importer >
bouton Parcourir),
4. dans l’onglet Privilèges : ajouter un nouvel utilisateur, le nommer « script_php », lui
attribuer le mot de passe « zh6tjPp6T56N4dbF » et les privilèges sur la base
« SELECT », « INSERT », « UPDATE » et « DELETE » (cases à cocher).

5.2 Connexion au SGBD MySQL

Avant de pouvoir adresser une requête à une base de données MySQL, il faut se connecter au
serveur MySQL et sélectionner cette base. Ces opérations sont réalisées lors de la création
d’une instance de la classe mysqli ou de la classe PDO, qui représentent toutes deux une
connexion à une base de données ou encore un accès à cette base pour le programme.

94
Dans les deux cas, un soin particulier doit être apporté au paramétrage de la connexion. Par
ailleurs, il est fréquent qu’un serveur MySQL soit temporairement indisponible ou
inaccessible : la tentative de connexion peut échouer et il est impératif de gérer ce cas de
figure.

5.2.1 Création d’un objet mysqli

Les quatre premiers arguments du constructeur de la classe mysqli permettent de spécifier


successivement l’adresse ou le nom d’hôte du serveur MySQL, le nom de l’utilisateur qui se
connecte, son mot de passe et enfin le nom de la base de données à sélectionner sur le
serveur. Exemple :

// Connexion au serveur MySQL et à la base de données


$base = new mysqli('localhost', 'script_php', 'zh6tjPp6T56N4dbF',
'exercices_php');

Un objet mysqli est toujours retourné lors de cette opération. Pour s’assurer que la connexion
a pu être établie, il faut tester la valeur de la propriété connect_error : elle vaut null si tout
s’est bien passé, sinon elle contient un message d’erreur qui peut être affiché directement :

$base = new mysqli('localhost', 'script_php', 'zh6tjPp6T56N4dbF',


'exercices_php');

// La connexion a-t-elle échoué ?


if ($base->connect_error)
exit('Erreur de connexion au serveur MySQL : '.$base->connect_error);
}

Il est important de définir le jeu de caractères utilisé pour l’envoi des requêtes et la réception
des résultats avec set_charset, faute de quoi certaines méthodes de l’objet mysqli ne pourront
pas fonctionner pas correctement. Cette fonction retourne true en cas de succès et false en
cas d’échec. Si une erreur survient, sa description est contenue dans la propriété error de
l’objet :

$base = new mysqli('localhost', 'script_php', 'zh6tjPp6T56N4dbF',


'exercices_php');

if ($base->connect_error)
exit('Erreur de connexion au serveur MySQL : '.$base->connect_error);

// Définition du jeu de caractères utilisé par les requêtes MySQL


$base->set_charset("utf8")
or exit('Erreur de définition de jeu de caractères : '.$base->error);

Enfin, quand elle n’est plus utile, on ferme la connexion au serveur MySQL avec la méthode
close.

95
Le squelette complet d’un script PHP se connectant à une base de données MySQL en utilisant
un objet mysqli est donc le suivant :

// Connexion au serveur MySQL et à la base de données


$base = new mysqli('localhost', 'script_php', 'zh6tjPp6T56N4dbF',
'exercices_php');

// La connexion a-t-elle échoué ?


if ($base->connect_error)
exit('Erreur de connexion au serveur MySQL : '.$base->connect_error);

// Définition du jeu de caractères utilisé par les requêtes MySQL


$base->set_charset("utf8")
or exit('Erreur de définition de jeu de caractères : '.$base->error);

// Ecrire ici les requêtes sur la base de données

// Fermeture de la connexion au serveur MySQL


$base->close();

5.2.2 Création d’un objet PDO

Le constructeur de la classe PDO possède quatre arguments : le Data Source Name ou DSN, le
nom de l’utilisateur qui se connecte au serveur de base de données, son mot de passe et un
éventuel tableau d’options pour la connexion à établir (dont les valeurs sont à choisir parmi
les constantes prédéfinies de la classe).

Qu’est-ce que le DSN ? C’est une chaîne de caractères qui va spécifier successivement quel
SGBD on veut utiliser (MySQL, SQLite, PostgreSQL, etc.), l’adresse ou le nom d’hôte du serveur
de ce SGBD et le nom de la base de données. D’autres paramètres peuvent être ajoutés,
comme par exemple le jeu de caractères utilisé par les requêtes et leurs résultats. Le format
du DSN est propre à chaque pilote de base de données supporté par PDO. Pour MySQL, le DSN
est défini dans cette page.

Dans le cas qui nous occupe, nous voulons établir une connexion au serveur MySQL
fonctionnant sur « localhost », sélectionner la base de données « exercices_php » et utiliser le
jeu de caractères « utf8 ». Le DSN s’écrira donc :

'mysql:host=localhost;dbname=exercices_php;charset=utf8'

Par conséquent, la connexion au serveur MySQL avec les paramètres présentés plus hauts, via
la création d’une instance de la classe PDO, s’écrit de la manière suivante :

// Connexion au serveur MySQL et à la base de données


$base = new PDO('mysql:host=localhost;dbname=exercices_php;charset=utf8',
'script_php', 'zh6tjPp6T56N4dbF');

96
Un objet PDO est renvoyé en cas de d’établissement de la connexion. Sinon, une exception
PDOException est lancée automatiquement. Il est conseillé de l’intercepter :

try {
$base = new PDO('mysql:host=localhost;dbname=exercices_php;charset=utf8',
'script_php', 'zh6tjPp6T56N4dbF');
}
catch (PDOException $e) {
exit('Erreur de connexion au serveur MySQL : '. $e->getMessage());
}

Une telle configuration de la connexion pose un problème de taille : la plupart des méthodes
de l’objet PDO ainsi créé ne génèrent pas d’exception en cas de problème (alors que
l’extension PDO est pourtant entièrement orientée objet). C’est le cas notamment de query,
utilisée pour exécuter des requêtes sur la base de données. En fait, plusieurs modes de gestion
des erreurs sont à disposition du développeur dans l’extension PDO, dont le lancement
d’exceptions. Pour activer ce mode, il faut utiliser le tableau d’options passé en quatrième
argument du constructeur, de la manière suivante :

try {
$base = new PDO('mysql:host=localhost;dbname=exercices_php;charset=utf8',
'script_php', 'zh6tjPp6T56N4dbF',
array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
}
catch (PDOException $e) {
exit('Erreur de connexion au serveur MySQL : '. $e->getMessage());
}

Quand elle n’est plus utile, on ferme la connexion en détruisant simplement la variable qui
référence l’objet PDO, grâce à la fonction unset.

Le squelette complet d’un script PHP se connectant à une base de données MySQL en utilisant
un objet PDO est donc le suivant :

// Connexion au serveur MySQL et à la base de données


try {
$base = new PDO('mysql:host=localhost;dbname=exercices_php;charset=utf8',
'script_php', 'zh6tjPp6T56N4dbF',
array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
}
catch (PDOException $e) {
exit('Erreur de connexion au serveur MySQL : '. $e->getMessage());
}

// Ecrire ici les requêtes sur la base de données

// Fermeture/destruction de la connexion au serveur MySQL


unset($base);

97
Signalons enfin que certaines options de la connexion, dont le mode de gestion des erreurs,
peuvent être modifiées après l’établissement de celle-ci en employant la méthode
setAttribute.

5.3 Requête simple et exploitation du jeu de résultats

Dans la suite et par souci de concision, on omettra de faire figurer le code PHP relatif à
l’ouverture et à la fermeture de la connexion à la base de données exercices_php. Dans les
exemples à venir, on considère ainsi que cette connexion est déjà établie grâce à un objet
mysqli ou PDO référencé par une variable nommée $base.

5.3.1 Avec MySQLi

La méthode query exécute une requête SQL sur la base de données MySQL connectée. Celle-ci
retourne false en cas d’échec. Exemple avec une requête qui sélectionne le titre et l’année de
parution de tous les livres consacrés à PHP dans notre base de données bibliographique :

$résultats = $base->query("SELECT titre, YEAR(date_publication) AS année "


."FROM livre "
."WHERE titre LIKE '%PHP%';")
or exit('La requête au serveur MySQL a échoué : '.$base->error);

Placer dans une variable la chaîne de caractères de la requête SQL produit un code à la fois
plus lisible et plus souple (si cette chaîne doit être traitée au préalable par exemple) :

$requête = "SELECT titre, YEAR(date_publication) AS année "


."FROM livre "
."WHERE titre LIKE '%PHP%';";

$résultats = $base->query($requête)
or exit('La requête au serveur MySQL a échoué : '.$base->error);

En cas de succès, fonction query retourne un objet mysqli_result qui contient le jeu de
résultats obtenu par la requête. Ce tableau à deux dimensions (comportant des lignes
indexées et des colonnes nommées), ne peut cependant pas être accédé directement.

L’objet mysqli_result possède un pointeur interne, positionné lors de sa création sur la


première ligne du jeu de résultats, et définissant la ligne courante. Il possède également des
méthodes qui renvoient la ligne de résultat courante (sous la forme d’un tableau
unidimensionnel contenant des chaînes de caractères), puis avancent le pointeur d’une ligne.
Grâce aux fonctions suivantes, on peut donc parcourir un jeu de résultats ligne après ligne :
 fetch_row : retourne la ligne courante sous la forme d’un tableau indexé (clés entières
commençant à 0),

98
 fetch_assoc : retourne la ligne courante sous la forme d’un tableau associatif (clés =
noms des colonnes du tableau de résultats),
 fetch_array : retourne la ligne courante sous la forme, au choix, d’un tableau indexé,
d’un tableau associatif ou des deux (les valeurs de la ligne de résultat apparaissent
alors deux fois dans le tableau renvoyé : une fois avec une clé entière et une fois avec
une clé textuelle).

Ces trois fonctions renvoient null quand il n’y a plus de ligne disponible dans le jeu de résultats
(le pointeur interne a atteint la dernière ligne). Une boucle while permet à la fois de parcourir
toutes les lignes du jeu de résultats et de tester cette condition :

// Requête SQL sur la base de données


$requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE '%PHP%';";

$résultats = $base->query($requête)
or exit('La requête au serveur MySQL a échoué : '.$base->error);

// Cas où la requête a renvoyé au moins une ligne de résultat


if ($résultats->num_rows > 0) {

// parcours des lignes de résultats avec fetch_row()


while ($livre = $résultats->fetch_row())
echo "<p>".$livre[0].", paru en ".$livre[1]."</p>\n";

echo "<hr>";
// rénitialisation de la position du pointeur interne
$résultats->data_seek(0);
// parcours des lignes de résultats avec fetch_assoc()
while ($livre = $résultats->fetch_assoc())
echo "<p>".$livre['titre'].", paru en ".$livre['année']."</p>\n";

echo "<hr>";
// rénitialisation de la position du pointeur interne
$résultats->data_seek(0);
// parcours des lignes de résultats avec fetch_array()
while ($livre = $résultats->fetch_array(MYSQLI_BOTH))
echo "<p>".$livre['titre'].", paru en ".$livre[1]."</p>\n";
}
else
// Cas où la requête a renvoyé un résultat vide
echo "<p>Aucun livre ne correspond dans la base de données.</p>\n";

// Destruction de l'objet contenant le jeu de résultats de la requête


$résultats->free();

Essayer cet exemple

Comme on le voit, la propriété num_rows contient le nombre de ligne du jeu de résultats


renvoyé par la requête. La méthode data_seek permet quant à elle de positionner le pointeur
interne sur une ligne quelconque du tableau de résultats et autorise donc n’importe quel type

99
de parcours de celui-ci. Enfin, la méthode free détruit l’objet de résultats quand celui-ci n’est
plus utile.

Depuis la version 5.3, PHP propose la méthode fetch_all, qui renvoie l’ensemble du jeu de
résultats d’une requête, sous la forme d’un tableau à deux dimensions (au choix : indexé,
associatif ou les deux). Exemple :

// Requête SQL sur la base de données


$requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE '%PHP%';";

$résultats = $base->query($requête)
or exit('La requête au serveur MySQL a échoué : '.$base->error);

// Cas où la requête a renvoyé au moins une ligne de résultat


if ($résultats->num_rows > 0) {

// l'ensemble du jeu de résultats est placé dans un tableau


$livres = $résultats->fetch_all(MYSQLI_ASSOC);

// parcours automatique des lignes du tableau


foreach ($livres as $livre)
echo "<p>".$livre['titre'].", paru en ".$livre['année']."</p>\n";
}
else
// Cas où la requête a renvoyé un résultat vide
echo "<p>Aucun livre ne correspond dans la base de données.</p>\n";

// Destruction de l'objet contenant le jeu de résultats de la requête


$résultats->free();

Essayer cet exemple

La méthode fetch_all est recommandable dans la plupart des cas, car elle évite d’avoir à gérer
le parcours ligne à ligne du tableau de résultats. Cependant, si ce dernier est susceptible d’être
de très grande taille, cette méthode peut consommer beaucoup de ressources, en temps de
traitement comme en espace mémoire.

100
5.3.2 Avec PDO

Un objet PDO utilise lui aussi une méthode query pour exécuter une requête SQL. Exemple
avec les exceptions activées :

// Requête SQL sur la base de données


$requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE '%PHP%';";

try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());

En cas de succès, query retourne un jeu de résultats contenu dans un objet PDOStatement.
Celui-ci possède une méthode fetch, qui retourne la ligne courante du jeu de résultats, sous la
forme d’un tableau indexé, un tableau associatif ou, par défaut, des deux. Mais ce n’est pas un
simple équivalent de la méthode fetch_array de MySQLi : fetch de PDO offre de nombreuses
possibilités de formatage d’une ligne de résultats, détaillées dans le manuel.

Exemple de transformation en un objet possédant autant de propriétés que de colonnes dans


le tableau de résultats et nommées comme ces colonnes :

// Requête SQL sur la base de données


$requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE '%PHP%';";

try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());
}

// Renvoie la ligne de résultats courante sous la forme d'un objet


// dont les noms de propriétés correspondent aux noms des colonnes
// retournés dans le jeu de résultats
$livre = $résultats->fetch(PDO::FETCH_OBJ);

if ($livre)
echo "<p>".$livre->titre.", paru en ".$livre->année."</p>\n";

Essayer cet exemple

La méthode fetchAll renvoie quant à elle l’ensemble du tableau de résultats.

101
Par ailleurs, la classe PDOStatement implémente l’interface Traversable de PHP : elle peut
donc être parcourue directement grâce à une boucle foreach. En d’autres termes, le jeu de
résultats contenu dans un objet PDOStatement peut être parcouru directement par une
boucle foreach. Ceci constitue la méthode la plus économe, en code et en ressources, pour
parcourir les lignes de résultats d’une requête sur une base de données :

// Requête SQL sur la base de données


$requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE '%PHP%';";

try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());
}

// Cas où la requête a renvoyé au moins une ligne de résultat :


if ($résultats->rowCount() > 0) {

// parcours direct et automatique des lignes de résultats


foreach ($résultats as $livre)
echo "<p>".$livre['titre'].", paru en ".$livre['année']."</p>\n";
}
else
// Cas où la requête a renvoyé un résultat vide
echo "<p>Aucun livre ne correspond dans la base de données.</p>\n";

// Destruction de l'objet contenant le jeu de résultats de la requête


unset($résultats);

Essayer cet exemple

Comme on peut le constater ci-dessus, la méthode rowCount contient le nombre de ligne du


jeu de résultats renvoyé par la requête.

Signalons enfin qu’un objet PDO possède une deuxième méthode d’exécution de requête : il
s’agit d’exec, qui, contrairement à query, ne retourne pas de jeu de résultats mais le nombre
de lignes insérées, modifiées ou effacées par la requête. La méthode exec est donc adaptée à
la modification de données dans la base (INSERT, UPDATE, DELETE, etc.) tandis que query doit
être employée pour effectuer des opérations de sélection (SELECT) retournant des résultats.

102
5.3.3 Prévention des injections SQL

Dans un site Web dynamique, il est extrêmement fréquent qu’une requête sur une base de
données contienne une valeur qui a été saisie par l’utilisateur dans un formulaire.
Par exemple, dans une boutique en ligne, un visiteur tape le nom d’une marque et, après
interrogation de la base de données du site, le serveur HTTP renvoie une page listant tous les
produits de cette marque.
Autre exemple, très proche du cas que nous avons illustré plus haut : sur le site d’une librairie
en ligne, un utilisateur saisit dans un formulaire un mot-clé du titre d’un livre (au hasard :
« PHP ») et se voit retourner la liste de tous les livres dont le titre contient ce mot.

Comme toujours, il ne faut jamais faire confiance à une saisie utilisateur. Ce dernier peut
saisir n’importe quoi dans un champ de formulaire. Pire, il peut écrire du code SQL pour
modifier la requête qui sera adressée au serveur MySQL, de manière à dévoiler, modifier ou
détruire les données qui se trouvent dans la base !

En rappelant que la commande SQL DROP TABLE efface une table entière dans une base de
données, voici une petite bande dessinée extraite du site xkcd.com qui vaut mieux qu’un long
discours (attention, humour geek !) :

D’autres exemples d’injections SQL sont consultables sur Wikipedia ou ailleurs.

Une des techniques de protection contre ces attaques est l’échappement des caractères qui
jouent un rôle particulier dans une commande SQL, notamment les apostrophes et les
guillemets qui délimitent les valeurs de type chaîne de caractères. Avec MySQLi, c’est la
méthode real_escape_string qui réalise ce traitement. Avec PDO, il s’agit de quote.
Cela demande une certaine rigueur mais c’est fondamental : toute chaîne de caractères de
valeur inconnue entrant dans la composition d’une requête simple doit être traitée
préalablement par l’une de ces méthodes.

Une autre façon de se prémunir des attaques par injection SQL est d’utiliser des requêtes
préparées. C’est l’objet de la section suivante.

103
5.4 Requête préparée

5.4.1 Principe, déroulement, avantages et inconvénients

Une requête préparée côté serveur (ou requête paramétrable) est un modèle (ou template)
de requête SQL. Il est nommé et stocké en mémoire d’un serveur de base de données sous
une forme compilée. Ce modèle comporte des paramètres et se comporte comme une
fonction SQL : une requête préparée peut être exécutée autant de fois que nécessaire, avec
des valeurs de paramètres différentes.
Le cycle de vie d’une requête préparée comporte trois phases : préparation de la requête
d’abord (une seule fois), exécution de la requête ensuite (une ou plusieurs fois), désallocation
(ou destruction) enfin. Détaillons le déroulement technique des deux premières phases :
Préparation de la requête :
1. envoi de la requête à préparer, par le logiciel client au serveur de base de données,
2. compilation de la requête (par le serveur),
3. création de son plan d’exécution,
4. stockage de la requête compilée en mémoire,
5. envoi d’un identifiant de requête préparée, par le serveur au client.

Exécution de la requête :
1. envoi d’une demande d’exécution de requête préparée, comportant son identifiant et
ses paramètres, par le logiciel client au serveur de base de données,
2. exécution de la requête préparée (par le serveur),
3. envoi du jeu de résultats obtenu par la requête, par le serveur au client.

Ce processus est à comparer à celui du déroulement d’une requête simple (non préparée) :
1. envoi de la requête, par le logiciel client au serveur de base de données,
2. compilation de la requête (par le serveur),
3. création de son plan d’exécution,
4. exécution de la requête,
5. envoi du jeu de résultats obtenu par la requête, par le serveur au client.

On peut constater que l’exécution d’une requête préparée comporte des étapes
supplémentaires par rapport à l’exécution d’une requête simple. En cas d’exécution unique,
une requête préparée sera donc plus coûteuse qu’une requête simple pour le serveur de base
de données, en temps de traitement comme en occupation mémoire.
En cas d’exécutions multiples, c’est l’inverse. En effet, la compilation de la requête et la
réalisation de son plan d’exécution sont effectuées une seule fois (lors de la préparation) pour

104
la requête préparée, alors que ces étapes, coûteuses en temps de traitement, doivent être
réalisées à chaque exécution d’une requête simple. À partir de deux exécutions, une requête
préparée est donc plus rapide qu’une requête simple.

Autre avantage fondamental des requêtes préparées, qui justifie à lui seul leur très grande
popularité : lors de leur exécution, elles sont insensibles aux injections SQL. En effet, ce sont
des requêtes compilées à l’avance par le SGBD : leur plan d’exécution/leur code exécutable
(qui traduit les commandes et clauses SQL de la requête) est créé lors de la préparation et ne
peut pas être modifié à l’exécution. Ainsi, on aura beau insérer des DROP TABLE (et autres
méchancetés) dans tous les paramètres d’une requête préparée, ces expressions seront
traitées à l’exécution comme des valeurs littérales de type chaînes de caractères et non
comme des commandes SQL !

Échapper les paramètres d’appel d’une requête préparée est donc inutile. Il faut toutefois
garder à l’esprit que le modèle de requête, c’est-à-dire la chaîne de caractères transmise au
SGBD pour préparer la requête, reste à protéger le cas échéant.

Dernier avantage notable des requêtes préparées : elles consomment moins de bande
passante que les requêtes simples. En effet, lors de l’exécution d’une requête préparée, seuls
l’identifiant et les paramètres de celle-ci sont transmis du client au serveur, alors qu’il s’agit de
la totalité de la requête dans le cas d’une requête simple.

Les requêtes préparées présentent tout de même quelques inconvénients :


 comme on l’a vu, elles sont moins efficaces en cas d’exécution unique,
 leur écriture est sensiblement plus longue et plus complexe que celle des requêtes
simples,
 elles consomment de la mémoire sur le serveur de base de données et doivent donc
être effacées dès qu’elles ne sont plus utiles. Par défaut, elles sont détruites quand le
logiciel client met fin à sa connexion au SGBD.

Les requêtes préparées côté serveur sont supportées par MySQL depuis la version 4.1. Comme
le confirme le manuel du SGBD, une requête préparée se déroule en trois étapes, qui
correspondent à trois commandes SQL différentes, dont le nom définit parfaitement le rôle :
 PREPARE : prépare une requête,
 EXECUTE : exécute cette requête,
 DEALLOCATE PREPARE : libère les ressources de la requête préparée (la détruit).

Le langage PHP, à travers ses extensions MySQLi et PDO, se contente de proposer des
méthodes faciles d’emploi qui appellent proprement les fonctions d’une API client dédiée aux
requêtes préparées de MySQL.

105
5.4.2 Avec MySQLi

Avec un objet/une connexion mysqli, la méthode prepare réalise la préparation d’une requête.
Cette méthode retourne false en cas d’échec. En cas de succès de la préparation, elle renvoie
un objet smt_mysqli, qui représente la requête préparée.

Le paramètre passé à la méthode prepare est le modèle de la requête à préparer. C’est une
chaîne de caractères où l’emplacement des paramètres de la requête préparée est défini par
des points d’interrogation, appelés marqueurs interrogatifs. Attention : ces marqueurs
peuvent prendre la place de valeurs littérales dans la requête à préparer (nombres, chaînes de
caractères, dates, etc.), mais pas celle de noms de colonnes, de noms de tables, de
commandes ou de mots-clés SQL.

Exemple d’un modèle de requête comportant trois paramètres, donc trois marqueurs
interrogatifs :

// Modèle de requête comportant trois paramètres : un mot de titre


// de livre (chaîne de caractères) et deux années (entiers)
$modèle_de_requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE ? "
."AND YEAR(date_publication) BETWEEN ? AND ?;";

// Préparation de la requête
$requête_préparée = $base->prepare($modèle_de_requête)
or exit('Erreur de préparation de la requête : '.$base->error);

Comme on le voit, on n’entoure pas un marqueur d’apostrophes ou de guillemets, même si


celui-ci définit l’emplacement d’un paramètre de type chaîne de caractères.
La méthode execute d’une requête préparée (objet smt_mysqli) exécute cette requête.
Seulement, cette méthode ne possède aucun argument. Comment donc passer ses
paramètres à la requête préparée ? En liant préalablement chacun d’entre eux à une variable
du programme PHP, grâce à la méthode bind_param.
Exemple :

// Liaison des paramètres de la requête préparée à des variables


$requête_préparée->bind_param('sii', $mot_de_titre, $année_début,
$année_fin);

106
La syntaxe de bind_param est comparable à celle du très classique printf : le premier
paramètre est une chaîne de caractères contenant autant de lettres que de paramètres dans
la requête préparée. Chaque lettre définit le type de son paramètre (dans l’ordre d’apparition
dans le modèle de requête) et donc le type attendu de la variable à lier correspondante :
 « i » : variable de type entier,
 « d » : variable de type décimal (flottant),
 « s » : variable de de type chaînes de caractères.

On trouve ensuite la liste des variables à lier : une par paramètre, là encore dans l’ordre de
leur apparition dans le modèle de requête. Notons que ces variables n’ont pas besoin de
posséder une valeur à ce stade ; elles peuvent donc apparaître pour la première fois dans le
programme.

Une fois cette mise en relation effectuée, lors de chaque appel/exécution de la requête
préparée, la méthode execute lit la valeur de chaque variable liée et prend celle-ci comme
paramètre de la requête. Pour modifier les paramètres d’appel d’une requête préparée, il
suffit donc de changer la valeur des variables qui leur sont liées.

Une fois exécutée, le jeu de résultats de la requête préparée est retourné par la méthode
get_result. Il peut alors être exploité comme n’importe quel jeu de résultats d’une requête
MySQL.

Enfin, quand on n’a plus besoin d’exécuter une requête préparée, on n’oublie pas de la
détruire dès que possible avec la méthode close, afin de libérer les ressources qu’elle occupe
sur le serveur de base de données.

Exemple complet comportant trois exécutions d’une requête préparée :

// Modèle de requête comportant trois paramètres : un mot de titre


// de livre (chaîne de caractères) et deux années (entiers)
$modèle_de_requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE ? "
."AND YEAR(date_publication) BETWEEN ? AND ?;";

// Préparation de la requête
$requête_préparée = $base->prepare($modèle_de_requête)
or exit('Erreur de préparation de la requête : '.$base->error);

// Liaison des paramètres de la requête préparée à des variables


$requête_préparée->bind_param('sii', $mot_de_titre, $année_début,
$année_fin);

// Affectation de certaines variables liées


$année_début = 2015;
$année_fin = 2017;

// Tableau de mots à rechercher successivement dans le titre


$AMP = array('%Apache%', '%PHP%', '%MySQL%');

107
// La requête préparée est exécutée plusieurs fois, grâce à une boucle
foreach ($AMP as $mot_de_titre) {

// Exécution
$requête_préparée->execute()
or exit("Erreur lors de l'exécution de la requête préparée : "
.$requête_préparée->error);

// Récupération du jeu de résultats


$résultats = $requête_préparée->get_result();

// Cas où la requête a renvoyé au moins 1 ligne : affichages des


résultats
if ($résultats->num_rows > 0) {
$livres = $résultats->fetch_all(MYSQLI_ASSOC);
foreach ($livres as $livre)
echo "<p>".$livre['titre'].", paru en ".$livre['année']."</p>\n";
}
else
// Cas où la requête a renvoyé un résultat vide
echo "<p>Aucun livre ne correspond dans la base de données.</p>\n";

echo "<hr>\n";

// Destruction de l'objet contenant le jeu de résultats


$résultats->free();
}

// Fermeture/destruction de la requête préparée


$requête_préparée->close();

Essayer cet exemple

5.4.3 Avec PDO

Avec un objet/une connexion PDO, la méthode prepare réalise la préparation d’une requête.
Cette méthode retourne false en cas d’échec. En cas de succès de la préparation, elle renvoie
un objet PDOStatement, qui représente la requête préparée.

Cet objet possède une méthode bindParam qui lie un paramètre de la requête préparée à une
variable du programme. Il faut donc appeler cette méthode plusieurs fois si la requête
possède plusieurs paramètres. C’est une différence importante avec bind_param de MySQLi,
qui permet de lier tous les paramètres d’un coup. Cependant, le choix opéré par PDO aboutit à
écriture plus lisible et plus robuste, c’est-à-dire moins susceptible de provoquer des erreurs de
la part du développeur (oubli d’une variable à lier, mauvais ordre des variables, etc.).

La méthode bindParam possède trois arguments : le numéro du paramètre (le premier étant
numéroté 1), le nom de la variable qui lui est liée et le type attendu pour les deux. Ce type est
à choisir parmi les constantes prédéfinies de PDO qui commencent par « PDO::PARAM_ ».
Dans le cas qui nous intéresse, le type chaîne de caractères est défini par la constante
PDO::PARAM_STR et le type entier par PDO::PARAM_INT.

108
En reprenant le modèle de requête présenté plus haut, nous obtenons donc le code suivant
pour la préparation de la requête et la liaison de ces paramètres :

// Modèle de requête comportant trois paramètres : un mot de titre


// de livre (chaîne de caractères) et deux années (entiers)
$modèle_de_requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE ? "
."AND YEAR(date_publication) BETWEEN ? AND ?;";

// Préparation de la requête
try {
$requête_préparée = $base->prepare($modèle_de_requête);
}
catch (PDOException $e) {
exit("Erreur lors de la préparation de la requête : ". $e->getMessage());
}

// Liaison des paramètres de la requête préparée à des variables


$requête_préparée->bindParam(1, $mot_de_titre, PDO::PARAM_STR);
$requête_préparée->bindParam(2, $année_début, PDO::PARAM_INT);
$requête_préparée->bindParam(3, $année_fin, PDO::PARAM_INT);

La requête préparée peut ensuite être exécutée autant de fois que nécessaire avec la
méthode execute.

L’objet PDOStatement qui contient notre requête préparée ne possède pas de méthode
get_result (ou getResult) comme un objet smt_mysqli. Comment récupérer le jeu de résultats
d’une requête préparée après une exécution de celle-ci ? Souvenons-nous de la définition de
la classe PDOStatement donnée dans l’introduction du chapitre : « représente une requête
préparée, un jeu de résultats d’une requête, ou les deux ».
Un objet PDOStatement utilisé dans le cadre d’une requête préparée possède donc deux
rôles : d’une part il représente la requête préparée, d’autre part il représente le jeu de
résultats obtenu après la dernière exécution de cette requête. Une méthode de récupération
du jeu de résultats est donc inutile : le jeu de résultats, c’est l’objet lui-même.

Exemple complet, qui utilise le fait qu’un objet PDOStatement est Traversable, ce qui signifie
que l’objet jeu de résultats/requête préparée peut être parcouru directement à l’aide d’une
boucle foreach :

// Modèle de requête comportant trois paramètres : un mot de titre


// de livre (chaîne de caractères) et deux années (entiers)
$modèle_de_requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE ? "
."AND YEAR(date_publication) BETWEEN ? AND ?;";

// Préparation de la requête
try {
$requête_préparée = $base->prepare($modèle_de_requête);
}
catch (PDOException $e) {

109
exit("Erreur lors de la préparation de la requête : ". $e->getMessage());
}

// Liaison des paramètres de la requête préparée à des variables


$requête_préparée->bindParam(1, $mot_de_titre, PDO::PARAM_STR);
$requête_préparée->bindParam(2, $année_début, PDO::PARAM_INT);
$requête_préparée->bindParam(3, $année_fin, PDO::PARAM_INT);

// Affectation de certaines variables liées


$année_début = 2015;
$année_fin = 2017;

// Tableau de mots à rechercher successivement dans le titre


$AMP = array('Apache', 'PHP', 'MySQL');

// La requête préparée est exécutée plusieurs fois, grâce à une boucle


foreach ($AMP as $mot) {
// Ajout d'un caractère % avant et après le mot de titre
$mot_de_titre = '%'.$mot.'%';

// Exécution
try {
$requête_préparée->execute();
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête préparée : ". $e-
>getMessage());
}

// Cas où la requête a renvoyé au moins une ligne de résultat :


if ($requête_préparée->rowCount() > 0) {
foreach ($requête_préparée as $livre)
echo "<p>".$livre['titre'].", paru en ".$livre['année']."</p>\n";
}
else
// Cas où la requête a renvoyé un résultat vide
echo "<p>Aucun livre ne correspond dans la base de données.</p>\n";

echo "<hr>\n";

// Ferme le curseur, permettant à la requête d'être de nouveau exécutée


$requête_préparée->closeCursor();
}

// Destruction de la requête préparée


unset($requête_préparée);

Essayer cet exemple

On remarquera l’appel de la méthode closeCursor, qui libère le jeu de résultats après


l’exploitation de celui-ci et permet à la requête préparée d’être à nouveau exécutée.

PDO autorise une autre syntaxe pour définir l’emplacement des paramètres d’un modèle de
requête : au lieu d’utiliser des « ? » (marqueurs interrogatifs), on emploie des noms précédés
du caractère « : » que l’on appelle des marqueurs nommés. Lors de la liaison d’un paramètre à
une variable avec bindParam, on reprend ces noms plutôt que le rang (le numéro) du
paramètre en question :

110
// Modèle de requête comportant trois paramètres : un mot de titre
// de livre (chaîne de caractères) et deux années (entiers)
$modèle_de_requête = "SELECT titre, YEAR(date_publication) AS année "
."FROM livre "
."WHERE titre LIKE :mot_de_titre "
."AND YEAR(date_publication) BETWEEN :annee_debut AND
:annee_fin;";

// Préparation de la requête
try {
$requête_préparée = $base->prepare($modèle_de_requête);
}
catch (PDOException $e) {
exit("Erreur lors de la préparation de la requête : ". $e->getMessage());
}

// Liaison des paramètres de la requête préparée à des variables


$requête_préparée->bindParam(':mot_de_titre', $mot_de_titre,
PDO::PARAM_STR);
$requête_préparée->bindParam(':annee_debut', $année_début, PDO::PARAM_INT);
$requête_préparée->bindParam(':annee_fin', $année_fin, PDO::PARAM_INT);

Essayer cet exemple

En utilisant des marqueurs nommés, on met en relation des noms de paramètres avec des
noms de variables, ce qui est plus lisible et plus sûr que de mettre en relation des numéros de
paramètres avec des noms de variables.

Comme le stipule son manuel, la méthode execute peut être appelée directement avec un
tableau de valeurs (donc sans avoir à lier les paramètres de la requête à des variables). Ainsi,
après avoir préparé la requête mais sans utiliser bindParam préalablement, on peut écrire le
code suivant :

$requête_préparée->execute(array(':mot_de_titre' => "%PHP%",


':annee_debut' => 2015,
':annee_fin' => 2017));

Ou encore cela, où les valeurs sont simplement lues dans des variables non liées :

$requête_préparée->execute(array(':mot_de_titre' => $mot_de_titre,


':annee_debut' => $année_début,
':annee_fin' => $année_fin));

Avec des marqueurs interrogatifs, l’écriture des paramètres est encore plus concise :

$requête_préparée->execute(array("%PHP%", 2015, 2017));

111
Ou encore :

$requête_préparée->execute(array($mot_de_titre, $année_début, $année_fin));

Cependant, cette syntaxe simplifiée ne permet pas de définir le type des paramètres de la
requête, qui sont tous traités par PHP comme des chaînes de caractères. Le serveur MySQL
effectue un transtypage correct la plupart du temps, mais la requête SQL peut échouer dans
certains cas, par exemple quand un entier est utilisé dans une clause LIMIT :

$modèle_de_requête = "SELECT titre, YEAR(date_publication) AS année "


."FROM livre "
."WHERE titre LIKE ? "
."AND YEAR(date_publication) BETWEEN ? AND ? "
."LIMIT ? ;";

$requête_préparée = $base->prepare($modèle_de_requête);

// Ne fonctionne pas car la valeur 3 est traitée comme une chaîne


// alors que le serveur MySQL attend un entier : erreur de syntaxe
$requête_préparée->execute(array("%PHP%", 2015, 2017, 3));

Essayer cet exemple

Si l’on souhaite lier des paramètres à des valeurs, l’emploi de la méthode bindValue est plus
sûr :

$modèle_de_requête = "SELECT titre, YEAR(date_publication) AS année "


."FROM livre "
."WHERE titre LIKE ? "
."AND YEAR(date_publication) BETWEEN ? AND ? "
."LIMIT ? ;";

$requête_préparée = $base->prepare($modèle_de_requête);

$requête_préparée->bindValue(1, "%PHP%", PDO::PARAM_STR);


$requête_préparée->bindValue(2, 2015, PDO::PARAM_INT);
$requête_préparée->bindValue(3, 2017, PDO::PARAM_INT);
$requête_préparée->bindValue(4, 3, PDO::PARAM_INT);

// Exécution avec les valeurs liées et typées : OK


$requête_préparée->execute();

Essayer cet exemple

112
5.5 Transaction

5.5.1 Principe, propriétés et déroulement

Une transaction est un ensemble d’opérations sur une base de données qui vérifie les
propriétés dites ACID, à savoir :
 Atomicité : les opérations qui constituent une transaction sont traitées comme un bloc
indivisible ; leur exécution répond à la logique du tout ou rien : à la fin de la
transaction, soit toutes les opérations sont réalisées (transaction réussie), soit
aucune n’est réalisée (transaction échouée) ; aucune exécution partielle n’est donc
permise,
 Cohérence : la base de données est laissée dans un état cohérent à la fin de la
transaction, même si son état peut être temporairement incohérent au cours de
l’exécution de celle-ci ;
 Isolation : l’exécution d’une transaction n’interfère pas avec l’exécution d’une autre
transaction qui doit travailler sur les mêmes données,
 Durabilité : les modifications réalisées par une transaction réussie deviennent
« permanentes », dans le sens où elles ne doivent pas être présentes seulement en
mémoire, mais stockées sur un support physique permettant de les retrouver/rétablir
en cas de besoin (plantage du logiciel serveur par exemple).

A quoi servent concrètement les transactions ? Le cas du virement bancaire constitue un


exemple emblématique que l’on retrouve partout dans la littérature. Ainsi, considérons une
banque appelée B1 qui possède un client nommé C1 et une banque appelée B2 qui possède
un client nommé C2. Un beau jour, C1 veut effectuer un virement de 100 euros sur le compte
de C2. Techniquement, cette opération est réalisée en deux étapes : d’abord, le solde du
compte de C1 est décrémenté de 100 € dans la base de données de B1 (débit du compte
émetteur du virement) ; puis, le solde du compte de C2 est incrémenté de la même somme
dans la base de données de B2 (crédit du compte destinataire). Malheureusement, au
moment du virement, le serveur de B2 est indisponible et ne peut répondre à la requête de
crédit du compte de C2, alors que le compte de C1 a déjà été débité. Le montant du virement
vient donc de se volatiliser dans la nature…
Pour éviter ce type de problèmes, les étapes du virement doit être été traitées de manière
indivisible au sein d’une transaction : soit toutes les étapes réussissent et le virement est
validé (commit), soit l’une d’elles échoue et le virement est annulé. Les deux comptes
retrouvent alors leurs soldes initiaux, grâce à un processus automatique de retour en arrière
(rollback). Voilà tout l’intérêt des transactions.

MySQL supporte les transactions depuis la version 4.0, avec l’introduction du moteur de
stockage InnoDB, qui est devenu le moteur par défaut du SGBD à compter de sa version 5.5.5.
La plupart des autres moteurs de stockage disponibles pour MySQL sont également
transactionnels, mais il existe des exceptions. MyISAM notamment, le moteur de stockage par

113
défaut de MySQL jusqu’à mi-2010, ne supporte pas les transactions. Si vous êtes amené à
travailler sur une base de données MySQL existante, il faut donc vérifier le moteur de stockage
utilisé par cette base avant de tenter d’y réaliser des transactions.

Le manuel MySQL spécifie les commandes SQL qui interviennent dans une transaction :
 START TRANSACTION (ou BEGIN) : démarre une transaction, c’est-à-dire débute le bloc
de commandes/d’opérations qui constituent la transaction,
 COMMIT : valide la transaction et rend ses modifications permanentes,
 ROLLBACK : annule la transaction et les modifications qu’elle a effectuées.

Que ce soit avec MySQLi ou PDO, PHP propose des méthodes qui portent quasiment le même
nom que les commandes SQL présentées ci-dessus.

5.5.2 Avec MySQLi

D’après la documentation de PHP, MySQLi supporte les transactions depuis PHP 5.5, mais
uniquement pour un SGBD MySQL 5.6 et supérieur utilisant le moteur de stockage InnoDB.

Le démarrage, la validation et l’annulation d’une transaction sont respectivement dévolues


aux méthodes begin_transaction, commit et rollback.

Exemple d’une transaction complète :

// Début d'une transaction


$base->begin_transaction();

// Des requêtes appartenant à la transaction :


// si l'une d'elles échoue, la méthode query() renvoie la valeur false
// et la variable $erreur passe à true : les requêtes suivantes ne sont
// alors pas exécutées
$erreur = false;
$base->query('SELECT * FROM auteur;') or $erreur = true;
if (!$erreur)
$base->query('SELECT * FROM editeur;') or $erreur = true;
if (!$erreur)
$base->query('SELECT * FROM livre;') or $erreur = true;

// Si toutes les requêtes ont réussi (aucune erreur) :


// on peut valider la transaction, avec commit()
if (!$erreur) {
$base->commit();
echo "<p>Transaction validée !</p>\n";
}
// Sinon, au moins une requête a échoué :
// il faut annuler la transaction, avec rollback()
else {
$base->rollback();
echo "<p>Transaction annulée !</p>\n";
}

Essayer cet exemple

114
Dans ce programme, toutes les requêtes sont normalement réussies et la transaction est
validée. Mais il suffit de changer un caractère dans l’une des requêtes (ex : « livre » devient
« ivre ») pour que celle-ci renvoie une erreur et que la transaction soit annulée.

Bien entendu, une transaction ne comportant que des requêtes SQL de type SELECT n’est
guère utile en pratique : ces requêtes ne sont présentes qu’à titre d’illustration et ont
justement le mérite de ne pas modifier notre base de données bibliographique, c’est-à-dire de
ne pas imposer une remise en état après l’essai de l’exemple précédent.

5.5.3 Avec PDO

PDO supporte les transactions depuis PHP 5.1 et y consacre une page de manuel.

Le démarrage, la validation et l’annulation d’une transaction sont respectivement assurées par


les méthodes beginTransaction, commit et rollback.

Activer les exceptions lors de la création de l’objet PDO (connexion à la base de données
MySQL) permet d’écrire le code d’une transaction de façon claire et concise :

try {
// Début d'une transaction
$base->beginTransaction();

// Des requêtes appartenant à la transaction :


// si l'une d'elles échoue, une exception est lancée
// et l'exécution du bloc try est interrompue
$base->query('SELECT * FROM auteur;');
$base->query('SELECT * FROM editeur;');
$base->query('SELECT * FROM livre;');

// Si on arrive ici, toutes les requêtes ont réussi :


// on peut valider la transaction, avec commit()
$base->commit();
echo "<p>Transaction validée !</p>\n";
}
// Si le bloc catch est exécuté, cela signifie qu'une exception a été lancée
// dans le bloc try précédent. Une requête de la transaction a donc échoué :
// il faut annuler la transaction, avec rollback()
catch (PDOException $e) {
$base->rollback();
echo "<p>Transaction annulée !</p>\n";
}

Essayer cet exemple

Là encore, modifier le nom d’une table dans une des requêtes permet de vérifier que la
transaction est annulée en cas d’erreur.

115
5.6 Bilan de la comparaison entre MySQLi et PDO

Au terme de ce long chapitre, nous avons passé en revue les fonctionnalités et l’utilisation de
MySQLi et de PDO dans les usages les plus courants d’une base de données MySQL. Même si
ce n’était pas le but premier, nous avons ainsi pu réaliser une comparaison des deux
extensions. Quel bilan et quelle conclusion pouvons-nous en tirer ?

Pour notre part, il est assez évident que PDO permet d’écrire un code plus lisible et plus
robuste que MySQLi. En effet :
 à travers une interface unique, PDO permet d’accéder de la même façon à la plupart
des bases de données du marché,
 pour tous les types de requêtes, le jeu de résultats obtenu peut être parcouru
directement à l’aide d’une boucle foreach,
 une autre option est de manipuler le jeu de résultats avec la méthode fetch, qui
propose de nombreux formats de sortie,
 pour les requêtes préparées, PDO offre la possibilité d’utiliser des marqueurs nommés
pour définir les paramètres, plus sûrs et plus lisibles que les marqueurs interrogatifs,
 de même, la liaison aux variables du programme est traitée paramètre par paramètre,
remplaçant une méthode unique dont l’appel peut être la source de nombreuses
erreurs d’écriture,
 enfin, PDO offre une interface entièrement objet et fait appel nativement aux
exceptions (parmi trois modes de gestion des erreurs), qui permettent un traitement
unifié et souple des problèmes d’exécution.

Par conséquent, à l’inverse du manuel PHP, nous recommandons l’usage de PDO pour toutes
les opérations courantes sur une base de données MySQL.

MySQLi reste toutefois utile pour accéder à certaines fonctionnalités avancées et/ou récentes
de MySQL, que PDO ne propose pas encore.

116
Chapitre 6 Mise en pratique

6.1 Présentation

On se propose d’utiliser notre base de données bibliographique pour créer la maquette du site
d’une bibliothèque municipale.

Cette maquette sera composée d’une part d’une partie publique, à savoir les pages accessibles
aux usagers de la bibliothèque, et d’autre part d’une partie privée, c’est-à-dire les pages
accessibles seulement aux gestionnaires de la bibliothèque.

Dans la partie publique, on trouvera les fichiers suivants :


1. ressources_communes.php : constantes et fonctions utilisées par les autres fichiers,
2. rechercher_livres.php : recherche multicritères sur les livres contenus dans la base de
données,
3. afficher_fiche_livre.php : affichage détaillé des informations sur un livre.

La partie privée de l’application comprendra les fichiers suivants :


4. ajouter_editeur.php : ajout d’un nouvel éditeur dans la base,
5. ajouter_auteur.php : ajout d’un nouvel auteur,
6. ajouter_livre.php : ajout d’un nouveau livre,
7. lister_editeurs.php : affichage de tous les éditeurs,
8. modifier_editeur.php : modification du nom d’un éditeur déjà présent dans la base,
9. supprimer_editeur.php : suppression d’un éditeur de la base de données.

Les quatre premiers fichiers listés ci-dessus sont donnés et présentés dans la suite.

Les autres seront à réaliser par vous-même, à titre de première expérience de développement
Web avec PHP et MySQL.

117
6.1.1 Ressources communes

Dans le fichier ressources_communes.php, on déclare un certain nombre de constantes


relatives à l’accès au serveur MySQL, à la base de données bibliographique, à l’encodage et au
traitement des chaînes de caractères.

On déclare également une fonction de connexion à la base de données utilisant PDO :

/************************ CONSTANTES *************************/

// Paramètres d'accès au serveur MySQL


const MYSQL_MACHINE_HOTE = "localhost";
const MYSQL_NOM_UTILISATEUR = "script_php";
const MYSQL_MOT_DE_PASSE = "zh6tjPp6T56N4dbF";
const MYSQL_BASE_DE_DONNEES = "exercices_php";
const MYSQL_CHARSET = "utf8";
const MYSQL_FORMAT_DATE = "Y-m-d";
const MYSQL_PREFIXE_DSN = "mysql:";
const MYSQL_DSN = MYSQL_PREFIXE_DSN."host=".MYSQL_MACHINE_HOTE
.";dbname=".MYSQL_BASE_DE_DONNEES
.";charset=".MYSQL_CHARSET;

// Taille maximale des chaînes dans la base de données


const LIVRE_TITRE_LONGUEUR_MAXI = 100;
const AUTEUR_NOM_LONGUEUR_MAXI = 50;
const AUTEUR_PRENOM_LONGUEUR_MAXI = 30;
const EDITEUR_NOM_LONGUEUR_MAXI = 50;

// Jeu de caractères et options de traitement des chaînes


const HTMLSPECIALCHARS_ENCODING = "UTF-8";
const HTMLSPECIALCHARS_FLAGS = ENT_COMPAT;
// Format français pour les dates : jj/mm/aaaa
const FORMAT_DATE_AFFICHAGE = "d/m/Y";

/************************* FONCTIONS *************************/

// Connexion à un serveur MySQL et à une base de données via PDO


function PDO_connecte_MySQL() {
try {
$base = new PDO(MYSQL_DSN, MYSQL_NOM_UTILISATEUR, MYSQL_MOT_DE_PASSE);
}
catch (PDOException $e) {
exit('Erreur de connexion au serveur MySQL : '. $e->getMessage());
}
return $base;
}

Ce fichier sera inclus dans toutes les pages de notre site maquette.

118
6.1.2 Recherche de livres

La page rechercher_livres.php se connecte à la base de données et affiche, lors de sa première


utilisation, le formulaire de recherche multicritères suivant :

Le premier élément du formulaire est un champ de texte permettant de saisir un mot du titre
des livres à rechercher. Cette zone peut éventuellement être laissée vide par l'utilisateur,
auquel cas elle n’est pas prise en compte lors de l’exécution de la recherche.

L’élément suivant est une liste déroulante permettant le choix d'une année minimale pour la
publication des livres recherchés. La valeur par défaut est « Indifférent », puis sont listées par
ordre décroissant toutes les années jusqu'en 1900, en commençant par l'année courante.

Le troisième élément est une liste déroulante dont la valeur par défaut est « Indifférent ». Elle
liste ensuite tous les noms d’éditeurs présents dans la base de données, par ordre
alphabétique.

Le dernier élément est un bouton qui déclenche une recherche multicritères sur les livres de la
base de données, en utilisant les valeurs saisies ou sélectionnées dans le formulaire.

119
Les résultats de la recherche sont affichés sous la forme d'une simple liste de titres classés par
ordre alphabétique, en dessous du formulaire. Exemple avec la recherche du mot de titre
« PHP », pour les livres publiés à partir de 2016 chez Eyrolles :

Essayer cette page

Pour obtenir ce résultat, on déclare d’abord dans le fichier rechercher_livres.php les fonctions
utilisateur année_courante() et écrit_liste_déroulante_années() :

// Renvoi de l'année courante


function année_courante() {
$date_courante = getdate();
return $date_courante['year'];
}

// Création dynamique d'un élément de formulaire :


// liste déroulante d'années comprises entre deux bornes passées en
paramètre,
// classées par ordre décroissant
function écrit_liste_déroulante_années($nom_liste, $annee_min, $annee_max) {
// écriture du début de l'élément de formulaire
$nom_liste = htmlspecialchars($nom_liste, HTMLSPECIALCHARS_FLAGS,
HTMLSPECIALCHARS_ENCODING);
echo " <select name=\"$nom_liste\">\n";
// écriture de la première option
echo " <option value=\"\">Indifférent</option>\n";
// écriture des options suivantes
for($annee = $annee_max; $annee >= $annee_min; $annee--) {
echo " <option value=\"$annee\">$annee</option>\n";
}
// écriture de la fin de l'élément de formulaire
echo " </select>\n";
}

120
Puis on déclare la fonction écrit_liste_déroulante_éditeurs(), qui récupère tous les noms
d’éditeurs dans la base de données, les classe par ordre alphabétique et en fait une liste
déroulante pour formulaire :

// Création dynamique d'un élément de formulaire :


// liste déroulante des éditeurs repertoriés dans la base de données
function écrit_liste_déroulante_éditeurs($base) {
// écriture du début de l'élément de formulaire
echo " <select name=\"éditeur\">\n";
// écriture de la première option
echo " <option value=\"\">Indifférent</option>\n";
// sélection de l'identifiant et du nom de tous les éditeurs
$requête = "SELECT id_editeur, nom FROM editeur ORDER BY nom;";
try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());
}
// écriture des options suivantes
foreach ($résultats as $résultat) {
$identifiant = htmlspecialchars($résultat['id_editeur'],
HTMLSPECIALCHARS_FLAGS, HTMLSPECIALCHARS_ENCODING);
$nom = htmlspecialchars($résultat['nom'], HTMLSPECIALCHARS_FLAGS,
HTMLSPECIALCHARS_ENCODING);
echo " <option value=\"$identifiant\">$nom</option>\n";
}
// destruction du jeu de résultats de la requête
unset($résultats);
// écriture de la fin de l'élément de formulaire
echo " </select>\n";
}

La fonction écrit_formulaire() utilise les deux fonctions décrites précédemment pour écrire le
code HTML du formulaire de recherche multicritères à afficher dans la page :

// Création du formulaire de recherche multicritères


function écrit_formulaire($base) {
echo "<form method=\"post\">\n";
// champ de saisie d'un mot du titre
echo " <p>\n";
echo " <label>Mot du titre </label>\n";
echo " <input type=\"text\" name=\"mot_titre\">\n";
echo " </p>\n";
// liste déroulante de l'année de publication minimale
echo " <p>\n";
echo " <label>Publication à partir de </label>\n";
$année_courante = année_courante();
écrit_liste_déroulante_années('année_début', ANNEE_MINI,
$année_courante);
echo " </p>\n";
echo " <p>\n";
echo " <label>Editeur </label>\n";
// liste déroulante des éditeurs
écrit_liste_déroulante_éditeurs($base);
echo " </p>\n";
// bouton "Rechercher"

121
echo " <p>\n";
echo " <input type=\"submit\" name=\"recherche\"
value=\"Rechercher\">\n";
echo " </p>\n";
echo "</form>\n";
}

La fonction valide_formulaire() s’assure que les caractères saisis pour le mot de titre sont bien
alphanumériques. Elle renvoie une chaîne vide si c’est le cas, sinon un message d’erreur :

// Validation des valeurs saisies et sélectionnées dans le formulaire ;


// on vérifie que :
// - le mot de titre saisi ne comporte que des caractères alphanumériques
function valide_formulaire() {
$messages_erreur = "";
// mot du titre alphanumérique
if (!empty($_POST['mot_titre']))
if (!ctype_alnum(trim($_POST['mot_titre'])))
$messages_erreur .= "<p style='color:red;'>Veuillez corriger le mot
de titre saisi.</p>\n";
return $messages_erreur;
}

La requête de recherche des livres est construite étape par étape par la fonction
recherche_livres_dans_BD(), en fonction des données saisies et sélectionnées dans le
formulaire :

// Recherche de livres dans la base de données, par construction


// et exécution d'une requête utilisant les paramètres
// issus du formulaire de recherche multicritères
function recherche_livres_dans_BD($base) {
// début de construction de la requête : par défaut, on sélectionne tous
les livres
$requête = "SELECT titre, id_livre AS 'identifiant' "
."FROM livre, auteur, editeur "
."WHERE livre.id_auteur = auteur.id_auteur "
."AND livre.id_editeur = editeur.id_editeur ";
// éventuelle restriction de la recherche selon le mot de titre saisi
if (!empty($_POST['mot_titre'])) {
$mot_titre = $base->quote('%'.trim($_POST['mot_titre'].'%'));
$requête .= "AND titre LIKE $mot_titre ";
}
// éventuelle restriction de la recherche selon l'année de publication
minimale
if (!empty($_POST['année_début'])) {
$année_début = $base->quote($_POST['année_début']);
$requête .= "AND YEAR(date_publication) >= $année_début ";
}
// éventuelle restriction de la recherche selon l'éditeur
if (!empty($_POST['éditeur'])) {
$éditeur = $base->quote(htmlspecialchars_decode($_POST['éditeur']));
$requête .= "AND editeur.id_editeur = $éditeur ";
}
// fin de construction de la requête : classement des résultats
// par ordre alphabétique des titres
$requête .= "ORDER BY titre ;";

122
// exécution de la requête sur la base de données
try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());
}
return $résultats;
}

Une fois la requête exécutée, on affiche les titres des livres sélectionnés grâce à la fonction
affiche_livres(). Chaque titre de livre constitue un lien hypertexte vers le fichier
afficher_fiche_livre.php et passe l’identifiant de ce livre comme paramètre d’URL :

// Affichage des livres trouvés, sous la forme d'une liste


// de titres/liens hypertextes
function affiche_livres($livres) {
echo "<h2>Résultat</h2>\n";
// cas où il y a au moins un livre à afficher
if ($livres && $livres->rowCount() > 0) {
foreach ($livres as $livre)
echo "<p><a
href=\"afficher_fiche_livre.php?identifiant=".$livre['identifiant']."\">"
.htmlspecialchars($livre['titre'], HTMLSPECIALCHARS_FLAGS,
HTMLSPECIALCHARS_ENCODING)
."</a></p>\n";
}
// cas où la recherche n'a donné aucun résultat
else
echo "<p>Aucun livre ne correspond à votre recherche.</p>\n";
}

123
Grâce à ces nombreuses fonctions, le programme principal de la page rechercher_livres.php
est simple et très lisible :

$base = PDO_connecte_MySQL();

écrit_formulaire($base);

// si une recherche a été effectuée (bouton "Rechercher" cliqué)


if (isset($_POST['recherche'])) {
$messages_erreur = valide_formulaire();
// cas où le formulaire est valide :
// on effectue une requête dans la base et on affiche les résultats
if (empty($messages_erreur)) {
$livres = recherche_livres_dans_BD($base);
affiche_livres($livres);
// destruction de l'objet contenant le jeu de résultats de la requête
unset($livres);
}
// cas où le formulaire est invalide :
// on invite l'utilisateur à corriger ses choix
else
echo $messages_erreur;
}

// fermeture/destruction de la connexion au serveur MySQL


unset($base);

6.1.3 Fiche d’un livre

Quand on clique sur l’un des titres/liens hypertextes résultant de la recherche de livres, on
appelle la page afficher_fiche_livre.php avec l’identifiant de ce livre (paramètre d’URL). Cette
page se reconnecte à la base de données, sélectionne toutes les informations sur le livre
souhaité et les affiche sous la forme d’une fiche détaillée. Exemple :

Essayer cette page

124
Dans ce fichier, on déclare d’abord la fonction recherche_livre(), qui va chercher dans la base
de données toutes les informations sur le livre dont l’identifiant est passé en paramètre :

// Recherche d'un livre dans la base à partir de son identifiant,


// avec sélection des colonnes nécessaires à son affichage détaillé
function recherche_livre($base, $identifiant) {
$identifiant = $base->quote($identifiant);
$requête = "SELECT id_livre, titre, date_publication, pages,
auteur.prenom AS 'auteur_prenom', auteur.nom AS 'auteur_nom',
editeur.nom AS 'editeur_nom'
FROM livre, auteur, editeur
WHERE livre.id_livre = $identifiant
AND livre.id_auteur = auteur.id_auteur
AND livre.id_editeur = editeur.id_editeur;";
try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());
}
return $résultats;
}

La fonction affiche_livre() est quant à elle dédiée à l’affichage formaté de ces informations
dans une page HTML. Elle affiche notamment la date au format « jj/mm/aaaa » (qui n’est pas
celui utilisé par le serveur MySQL) et met le nom de famille de l’auteur en majuscules :

// Affichage de la fiche détaillée d'un livre


function affiche_livre($livre) {
$date_publication = new DateTime($livre['date_publication']);
$date_publication_formatée = $date_publication-
>format(FORMAT_DATE_AFFICHAGE);
echo "<h2>Fiche livre</h2>\n";
echo "<p><strong>Référence : </strong>"
.htmlspecialchars($livre['id_livre'], HTMLSPECIALCHARS_FLAGS,
HTMLSPECIALCHARS_ENCODING)."</p>\n";
echo "<p><strong>Titre : </strong>".htmlspecialchars($livre['titre'],
HTMLSPECIALCHARS_FLAGS, HTMLSPECIALCHARS_ENCODING)."</p>\n";
echo "<p><strong>Auteur : </strong>"
.htmlspecialchars($livre['auteur_prenom'], HTMLSPECIALCHARS_FLAGS,
HTMLSPECIALCHARS_ENCODING)." "
.htmlspecialchars(strtoupper($livre['auteur_nom']),
HTMLSPECIALCHARS_FLAGS, HTMLSPECIALCHARS_ENCODING)."</p>\n";
echo "<p><strong>Éditeur : </strong>"
.htmlspecialchars($livre['editeur_nom'], HTMLSPECIALCHARS_FLAGS,
HTMLSPECIALCHARS_ENCODING)."</p>\n";
echo "<p><strong>Date de publication : </strong>"
.htmlspecialchars($date_publication_formatée,
HTMLSPECIALCHARS_FLAGS, HTMLSPECIALCHARS_ENCODING)."</p>\n";
echo "<p><strong>Pages : </strong>"
.htmlspecialchars($livre['pages'], HTMLSPECIALCHARS_FLAGS,
HTMLSPECIALCHARS_ENCODING)."</p>\n";
}

125
Là encore, le programme principal de la page est très simple :

// si la page a bien été appelée avec un identifiant (paramètre passé par


URL)
if (!empty($_GET['identifiant'])) {

// lecture dans la base de données des informations sur le livre souhaité


$base = PDO_connecte_MySQL();
$livres = recherche_livre($base, $_GET['identifiant']);
unset($base);

// cas où le livre a été trouvé dans la base : affichage des détails


if ($livres && $livres->rowCount() > 0) {
affiche_livre($livres->fetch(PDO::FETCH_ASSOC));
unset($livres);
}
// cas où la recherche n'a donné aucun résultat
else
echo "<p>Aucun livre ne correspond à cet identifiant dans la base de
données.</p>\n";
}
// cas d'un appel incorrect de la page
else
echo "<p>Erreur : la page a été appelée sans l'identifiant du livre à
afficher.</p>\n";

echo "<p>Faire une <a href=\"rechercher_livres.php\">nouvelle


recherche</a></p>\n";

6.1.4 Ajout d’un éditeur

Contrairement aux deux pages précédentes, celle que nous présentons ci-dessous concerne la
partie privée du site de la bibliothèque : elle n’est accessible qu’au personnel autorisé de celle-
ci, après connexion avec identifiant et mot de passe (ce système d’identification n’est pas géré
dans notre maquette de site).

La page ajouter_editeur.php affiche d’abord un formulaire comportant simplement un champ


de saisie de texte et un bouton de validation :

126
Si l’utilisateur valide le formulaire sans avoir saisi le moindre caractère, il est rappelé à l’ordre :

Si un nom a bien été saisi, on affiche un formulaire de confirmation :

Si l’utilisateur clique sur le bouton « Modifier », il revient formulaire de saisie initiale. S’il
utilise le bouton « Confirmer », le programme recherche d’abord si ce nom d’éditeur existe
dans la base de données. Si oui, il le signale sans modifier le contenu de la base :

Si l’éditeur n’existe pas déjà dans la base, il est ajouté et l’utilisateur est informé du succès de
cette opération :

Essayer cette page

127
Le fichier ajouter_editeur.php utilise deux fonctions, qui réalisent respectivement l’affichage
du formulaire de saisie et celui du formulaire de confirmation :

// Affichage du formulaire de saisie du nom de l'éditeur


function ecrit_formulaire_saisie() {
echo "<form method=\"post\">\n";
echo "<p><label>Nom de l'éditeur </label>\n";
echo "<input type=\"text\" name=\"éditeur\" value=\""
.(isset($_SESSION['éditeur']) ?
htmlspecialchars($_SESSION['éditeur'], HTMLSPECIALCHARS_FLAGS,
HTMLSPECIALCHARS_ENCODING) : '')
."\"></p>\n";
echo "<p><input type=\"submit\" name=\"ajout\" value=\"Ajouter à la
base\"></p>\n";
echo "</form>\n";
}

// Affichage du formulaire de confirmation


function ecrit_message_et_formulaire_confirmation() {
echo "<p>Nom de l'éditeur : ".htmlspecialchars($_SESSION['éditeur'],
HTMLSPECIALCHARS_FLAGS, HTMLSPECIALCHARS_ENCODING)."</p>\n";
echo "<form method=\"post\">\n";
echo "<input type=\"submit\" name=\"confirmation\"
value=\"Confirmer\">\n";
echo "<input type=\"submit\" value=\"Modifier\">\n";
echo "</form>\n";
}

Cette page utilise une session pour transmettre le nom de l’éditeur saisi initialement aux
différents écrans qui suivent (formulaire de confirmation d’abord et ajout dans la base
ensuite).

Le programme principal définit l’enchaînement logique des étapes que nous avons présentées
plus haut :

// Le formulaire de saisie a été validé :


if (isset($_POST['ajout'])) {
// le nom d'éditeur n'est pas vide
if (!empty($_POST['éditeur'])) {
// copie du nom dans une variable de session
$_SESSION['éditeur'] = $_POST['éditeur'];
// affichage du formulaire de confirmation
ecrit_message_et_formulaire_confirmation();
}
// le nom de l'éditeur est vide
else {
ecrit_formulaire_saisie();
echo "<p style='color:red;'>Veuillez saisir un nom d'éditeur, s'il
vous plaît.</p>\n";
}
}
// Le formulaire de confirmation a été validé :
else if (isset($_POST['confirmation'])) {
// connexion à la base de données MySQL
$base = PDO_connecte_MySQL();

128
// recherche de l'éditeur dans la base de données :
$nom_pour_affichage = htmlspecialchars($_SESSION['éditeur'],
HTMLSPECIALCHARS_FLAGS, HTMLSPECIALCHARS_ENCODING);
$nom_pour_requête = $base->quote(substr(trim($_SESSION['éditeur']), 0,
EDITEUR_NOM_LONGUEUR_MAXI));

$requête = "SELECT * FROM editeur WHERE nom = $nom_pour_requête ;";


try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());
}

// cas où l'éditeur se trouve déjà dans la base de données


if ($résultats && $résultats->rowCount() > 0) {
$éditeur = $résultats->fetch(PDO::FETCH_ASSOC);
echo "<p style='color:maroon;'>L'éditeur \"$nom_pour_affichage\" "
."est déjà enregistré dans la base de données.</p>\n";
echo "<p><a href=\"".$_SERVER['SCRIPT_NAME']."\">Ajouter un autre
éditeur</a></p>\n";
}
// cas où l'éditeur n'a pas été trouvé : on l'ajoute dans la base de
données
else {
$requête = "INSERT INTO editeur (nom) VALUES ($nom_pour_requête) ;";
try {
$nbe_lignes_insérées = $base->exec($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e-
>getMessage());
}
if ($nbe_lignes_insérées == 1)
echo "<p style='color:green;'>L'éditeur \"$nom_pour_affichage\" "
."a bien été enregistré dans la base de données.</p>\n";
else
echo "L'enregistrement de l'éditeur \"$nom_pour_affichage\" a
échoué.</p>\n";

echo "<p><a href=\"".$_SERVER['SCRIPT_NAME']."\">Ajouter un autre


éditeur</a></p>\n";
}
// libération de la mémoire occupée par le jeu de résultats de la requête
unset($résultats);

// fermeture de la connexion au serveur MySQL


unset($base);

// suppression de l'éditeur mémorisé


unset($_SESSION['éditeur']);
}
// Premier chargement ou nouvelle saisie :
// affichage de la page dans son état initial
else
ecrit_formulaire_saisie();

129
6.2 Travail dirigé

Et maintenant, c’est à vous ! Vous trouverez ci-dessous quelques exercices de difficulté et de


longueur croissante. Il s’agit de développer les exemples précédents ou de s’en inspirer pour
créer de nouvelles pages de la maquette du site de bibliothèque municipale.

Ces exercices ne sont pas notés et ne sont pas à rendre. Ils ne doivent pas être confondus avec
les trois devoirs (activités) obligatoires à réaliser chaque semestre dans le module
d’enseignement. Ils constituent simplement une première mise en application pratique de ce
cours sur le développement Web avec PHP et MySQL.

Les corrigés ne sont pas fournis. Cependant, si vous restez bloqué dans la réalisation d’un
exercice, vous pouvez contacter votre tuteur pour obtenir des conseils ou des pistes de
résolution.

6.2.1 Exercice 1

Ajouter un champ de saisie de texte « Nom de l’auteur » dans la page de


rechercher_livres.php :

Avant de procéder à la recherche de livres dans la base de données, on vérifiera que le nom de
l’auteur saisi n’est pas vide et ne comporte que des caractères alphabétiques.

130
6.2.2 Exercice 2

Ajouter maintenant une liste déroulante d’années permettant de définir l’année maximale de
la recherche :

Avant de procéder à la recherche de livres dans la base de données, on vérifiera que l’année
minimale et l’année maximale sont compatibles, à savoir que la première est inférieure ou
égale à la seconde si les deux ont été sélectionnées.

6.2.3 Exercice 3

Ajouter enfin à la page rechercher_livres.php la mémorisation de tous les champs du


formulaire, de sorte que les choix de l’utilisateur soient conservés lors de l’affichage des
résultats de la recherche :

131
6.2.4 Exercice 4

Sur le modèle de la page ajouter_editeur.php, créer une page ajouter_auteur.php :

Comme pour un éditeur, l’ajout d’un auteur passe par un formulaire de confirmation :

De même, l’auteur n’est ajouté à la base que s’il n’existe pas déjà.

6.2.5 Exercice 5

Réaliser une page ajouter_livre.php :

Notez les liens hypertextes permettant d’accéder directement aux pages d’ajout d’un auteur
ou d’un éditeur en cas de besoin.

132
Remarquez également l’absence d’option « Indifférent » dans les listes déroulantes. En effet,
tous les champs du formulaire doivent être remplis pour que la demande d’ajout d’un livre
dans la base puisse être traitée. Par ailleurs, on vérifiera préalablement que :
 le titre saisi ne comporte que des caractères imprimables,
 la date est au format « jj/mm/aaaa » et correspond à une date existante du calendrier
grégorien,
 le nombre de pages saisi est un nombre entier.

Comme pour un nouvel éditeur ou un nouvel auteur, l’ajout d’un nouveau livre passe par une
étape de confirmation des données puis une étape de vérification de l’existence dans la base.

6.2.6 Exercice 6

Petite pause dans la difficulté : créer une page lister_editeurs.php qui sélectionne simplement
le nom de tous les éditeurs dans la base de données et les affiche classés par ordre
alphabétique :

Les liens « Modifier » et « Supprimer » qui suivent chaque nom d’éditeur pointent
respectivement vers les pages modifier_editeur.php et supprimer_editeur.php, qui seront
réalisées dans les exercices suivants. Chaque lien hypertexte possède comme paramètre
d’URL l’identifiant de l’éditeur dont le nom est placé sur la même ligne.

6.2.7 Exercice 7

Réaliser une page supprimer_editeur.php, qui efface de la base de données l’éditeur dont
l’identifiant lui a été passé en paramètre, après une phase de confirmation :

133
Rappelons que la table livre de notre base de données possède des contraintes d’intégrité
référentielle sur les identifiants de l’auteur et de l’éditeur. Ainsi, tout livre ajouté à la base doit
posséder un identifiant d’éditeur qui existe dans la table du même nom. Réciproquement,
MySQL empêchera la suppression d’un éditeur dont l’identifiant est utilisé dans la table des
livres (donc quand la base contient au moins un livre publié par cet éditeur). Dans un tel cas de
figure, le nombre de lignes modifiées dans la base de données après une tentative de
suppression est nul. C’est une situation qu’il faut gérer après validation du formulaire de
confirmation, par exemple de la manière suivante :

6.2.8 Exercice 8

Créer enfin une page modifier_editeur.php ; celle-ci prend l’identifiant d’un éditeur comme
paramètre d’appel, sélectionne le nom de cet éditeur dans la base de données et précharge
celui-ci dans un formulaire de saisie identique à celui de la page ajouter_editeur.php :

Vient ensuite l’étape de confirmation :

134
On termine par la modification du nom de l’éditeur dans la base, dont la réussite est annoncée
à l’utilisateur :

Bon travail à toutes et à tous !

135