Académique Documents
Professionnel Documents
Culture Documents
5.1 Introduction
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.
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.
Dans ce chapitre comme dans le suivant, nous utilisons une base de données bibliographique
qui comporte trois tables :
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.
auteur
editeur
id_editeur nom
1 ENI
2 Eyrolles
3 O'Reilly
4 Ellipses
93
livre
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).
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.
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 :
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 :
if ($base->connect_error)
exit('Erreur de connexion au serveur MySQL : '.$base->connect_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 :
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 :
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 :
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.
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.
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 :
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) :
$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.
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 :
$résultats = $base->query($requête)
or exit('La requête au serveur MySQL a échoué : '.$base->error);
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";
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 :
$résultats = $base->query($requête)
or exit('La requête au serveur MySQL a échoué : '.$base->error);
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 :
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.
try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());
}
if ($livre)
echo "<p>".$livre->titre.", paru en ".$livre->année."</p>\n";
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 :
try {
$résultats = $base->query($requête);
}
catch (PDOException $e) {
exit("Erreur lors de l'exécution de la requête : ". $e->getMessage());
}
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 !) :
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
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 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 :
// 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);
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.
// 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);
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);
echo "<hr>\n";
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 :
// 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());
}
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 :
// 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());
}
// 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());
}
echo "<hr>\n";
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());
}
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 :
Ou encore cela, où les valeurs sont simplement lues dans des variables non liées :
Avec des marqueurs interrogatifs, l’écriture des paramètres est encore plus concise :
111
Ou encore :
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 :
$requête_préparée = $base->prepare($modèle_de_requête);
Si l’on souhaite lier des paramètres à des valeurs, l’emploi de la méthode bindValue est plus
sûr :
$requête_préparée = $base->prepare($modèle_de_requête);
112
5.5 Transaction
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).
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.
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.
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.
PDO supporte les transactions depuis PHP 5.1 et y consacre une page de manuel.
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();
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.
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
Ce fichier sera inclus dans toutes les pages de notre site maquette.
118
6.1.2 Recherche de livres
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 :
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() :
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 :
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 :
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 :
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 :
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 :
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);
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 :
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 :
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 :
125
Là encore, le programme principal de la page est très simple :
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).
126
Si l’utilisateur valide le formulaire sans avoir saisi le moindre caractère, il est rappelé à l’ordre :
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 :
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 :
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 :
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));
129
6.2 Travail dirigé
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
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
131
6.2.4 Exercice 4
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
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 :
134
On termine par la modification du nom de l’éditeur dans la base, dont la réussite est annoncée
à l’utilisateur :
135