***** CETTE PAGE EST IMPORTANTE. PLACEZ-LA DANS VOS FAVORIS *****
Lorsqu'une requête SQL doit utiliser une valeur tirée d'une variable, nous exposons notre base de données aux injections SQL. Il s'agit d'une technique utilisée par les utilisateurs malveillants pour tenter d'obtenir des informations sensibles tirées de la base de données.
C'est pourquoi, lorsqu'une requête contient une valeur tirée d'une variable, nous utiliserons toujours des requêtes préparées, aussi appelées requêtes paramétrables, afin de protéger nos données contre les injections SQL.
Notez que si votre requête ne contient aucune valeur tirée d'une variable PHP, il est préférable d'utiliser une requête régulière (structure de code avec $mysqli->query()).
Dans cette fiche, je commence par vous expliquer en détails comment une requête préparée doit être réalisée pour que votre code soit robuste, convivial, performant et fonctionnel.
Je vous donne ensuite des exemples d'utilisation :
▼Publicité Le texte se poursuit plus bas
Votre programme devra utiliser la structure de code suivante. De nombreux commentaires ont été ajoutés pour vous aider à bien comprendre chacune des étapes.
Selon la documentation officielle de PHP, l'exécution d'une requête préparée se déroule en deux étapes : la préparation et l'exécution.
Lors de la préparation, un template de requête est envoyé au serveur de base de données. Le serveur effectue une vérification de la syntaxe, et initialise les ressources internes du serveur pour une utilisation ultérieure.
La préparation est suivie de l'exécution. Pendant l'exécution, le client lie les valeurs des paramètres et les envoie au serveur. Le serveur crée une requête depuis le template et y lie les valeurs pour l'exécution, en utilisant les ressources internes créées précédemment.
La syntaxe sera légèrement difféfente selon la version de PHP utilisée.
// 1. Dans la requête il faut placer un ? à la place de chaque paramètre (chaque variable utilisée dans la requête).
// Remarquez qu'avec les requêtes préparées, PHP s'occupera d'ajouter lui-même les apostrophes de chaque côté d'une variable string.
$requete = "SELECT champ1, champ2, champ3 FROM table1 WHERE champ4=? OR champ5=?";
// 2. Prépare la requête (MySQL connaîtra le but de la requête avant même de connaître la valeur des variables).
// Il est d'usage d'utiliser une variable nommée stmt (StaTeMenT).
$stmt = $mysqli->prepare($requete);
if ($stmt) {
// 3. Indique le type de chacun des paramètres : string (s), integer (i) ou decimal (d).
// Assigne ensuite à chacun des paramètres, dans l'ordre, la variable qui contient sa valeur.
$stmt->bind_param('xx', $var1, $var2);
// 4. Exécute la requête.
$stmt->execute();
// Sans cette ligne, il ne sera pas possible de connaître le nombre de lignes retournées par un SELECT.
$stmt->store_result();
// Si la requête a fonctionné
if (0 == $stmt->errno) {
if ($stmt->num_rows > 0) {
// Pour une requête INSERT, UPDATE ou DELETE, travailler plutôt avec $stmt->affected_rows
// 5. Fait le lien entre la position des champs lus par le SELECT et les variables qui seront initialisées lors du fetch.
// Cette étape n'aura pas lieu si la requête était un INSERT, un UPDATE ou un DELETE.
$stmt->bind_result($champ1, $champ2, $champ3);
// 6. Pour chaque enregistrement, initialise les variables du bind_result() à partir des champs lus.
while ($stmt->fetch()) {
...
}
}
else {
// aucun enregistrement ne correspond à la requête ou aucun enregistrement n'a été modifié par la requête.
...
}
}
else {
// Arrivera ici si les paramètres posent problème (ex : contrainte d'intégrité référentielle non respectée ou erreur dans le bind_param()).
...
echo_debug($stmt->error);
}
// 7. Libère la mémoire (doit être fait à la fin du if ($stmt)).
$stmt->close();
}
else {
// Arrivera ici s'il y a une erreur dans la requête (ex : mauvais nom de champ).
...
echo_debug($mysqli->error);
}
// 1. Dans la requête il faut placer un ? à la place de chaque paramètre (chaque variable utilisée dans la requête).
// Remarquez qu'avec les requêtes préparées, PHP s'occupera d'ajouter lui-même les apostrophes de chaque côté d'une variable string.
$requete = "SELECT champ1, champ2, champ3 FROM table1 WHERE champ4=? OR champ5=?";
// 2. Prépare la requête (MySQL connaîtra le but de la requête avant même de connaître la valeur des variables).
// Il est d'usage d'utiliser une variable nommée stmt (StaTeMenT).
try {
$stmt = $mysqli->prepare($requete);
// 3. Indique le type de chacun des paramètres : string (s), integer (i) ou decimal (d).
// Assigne ensuite à chacun des paramètres, dans l'ordre, la variable qui contient sa valeur.
$stmt->bind_param('xx', $var1, $var2);
// 4. Exécute la requête.
$stmt->execute();
// Sans cette ligne, il ne sera pas possible de connaître le nombre de lignes retournées par un SELECT.
$stmt->store_result();
// Si la requête a fonctionné
if (0 == $stmt->errno) {
if ($stmt->num_rows > 0) {
// Pour une requête INSERT, UPDATE ou DELETE, travailler plutôt avec $stmt->affected_rows
// 5. Fait le lien entre la position des champs lus par le SELECT et les variables qui seront initialisées lors du fetch.
// Cette étape n'aura pas lieu si la requête était un INSERT, un UPDATE ou un DELETE.
$stmt->bind_result($champ1, $champ2, $champ3);
// 6. Pour chaque enregistrement, initialise les variables du bind_result() à partir des champs lus.
while ($stmt->fetch()) {
...
}
}
else {
// aucun enregistrement ne correspond à la requête ou aucun enregistrement n'a été modifié par la requête.
...
}
}
else {
// Arrivera ici si les paramètres posent problème (ex : contrainte d'intégrité référentielle non respectée ou erreur dans le bind_param()).
...
echo_debug($stmt->error);
}
// 7. Libère la mémoire (doit être fait à la fin du if ($stmt)).
$stmt->close();
} catch (Exception $e) {
// Arrivera ici s'il y a une erreur dans la requête (ex : mauvais nom de champ).
...
echo_debug($mysqli->error);
}
Voici un exemple pour une requête SELECT qui ne retourne jamais plus d'un enregistrement.
Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.
$id = $_POST['id'];
$requete = "SELECT prenom, nomfamille FROM clients WHERE id=?";
$stmt = $mysqli->prepare($requete);
if ($stmt) {
$stmt->bind_param('i', $id);
$stmt->execute();
$stmt->store_result(); // nécessaire pour pouvoir travailler avec $stmt->num_rows
if (0 == $stmt->errno) {
if ($stmt->num_rows > 0) {
$stmt->bind_result($prenom, $nomfamille);
$stmt->fetch(); // c'est ici que $prenom, $nomfamille sont initialisés
echo "Bonjour, $prenom $nomfamille !";
}
else {
echo "<div class='message-avertissement'>Le client demandé n'existe pas.</div>";
}
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 1).</div>";
echo_debug($stmt->error);
}
$stmt->close();
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 2).</div>";
echo_debug($mysqli->error);
}
Dans ce second exemple, nous travaillons avec une requête qui peut retourner plusieurs enregistrements.
Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.
$ville = '';
$annee = -1;
if (isset($_GET['ville'])) {
$ville = strtoupper($_GET['ville']); // dans cet exemple, on convertit en majuscules puisqu'il y a un UPPER dans la requête
}
if (isset($_GET['annee'])) {
$annee = $_GET['annee'];
}
$requete = "SELECT id, prenom, nomfamille FROM clients WHERE UPPER(ville)=? AND EXTRACT(year FROM naissance) = ? ORDER BY nomfamille, prenom";
$stmt = $mysqli->prepare($requete);
if ($stmt) {
$stmt->bind_param('si', $ville, $annee);
$stmt->execute();
$stmt->store_result(); // nécessaire pour pouvoir travailler avec $stmt->num_rows
if (0 == $stmt->errno) {
if ($stmt->num_rows > 0) {
$stmt->bind_result($id, $prenom, $nomfamille);
echo '<table>';
// Pour chaque enregistrement, initialise les variables du bind_result() à partir des champs lus.
while ($stmt->fetch()) {
echo "<tr><td><a href='detailsclient.php?id=$id'>Détails</a></td><td>$nomfamille</td><td>$prenom</td></tr>";
}
echo '</table>';
}
else {
echo "<div class='message-avertissement'>Il n'y a aucun client né en $annee dans la ville $ville.</div>";
}
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 1).</div>";
echo_debug($stmt->error);
}
$stmt->close();
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 2).</div>";
echo_debug($mysqli->error);
}
Cet autre exemple illustre l'utilisation d'une requête préparée lors d'un INSERT.
Attention : si votre code doit faire une redirection, vous devrez plutôt utiliser la version présentée plus bas, qui initialise des variables de session.
Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.
$prenom = '';
$nomfamille = '';
// Retrouve les données du formulaire.
if (isset($_POST['prenom'])) {
$prenom = $_POST['prenom'];
}
if (isset($_POST['nomfamille'])) {
$nomfamille = $_POST['nomfamille'];
}
// Valide les données.
$messageErreur = '';
// refaire en PHP toutes les validations HTML et JavaScript
// Si tout était valide
if ('' == $messageErreur) {
// Tente l'enregistrement des données.
$requete = "INSERT INTO clients(prenom, nomfamille, actif) VALUES(?, ?, 1)";
$stmt = $mysqli->prepare($requete);
if ($stmt) {
$stmt->bind_param('ss', $prenom, $nomfamille);
$stmt->execute();
if (0 == $stmt->errno) {
echo "<div class='message-information'>Le client a été ajouté avec succès !</div>";
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche d'enregistrer le client (code 1).</div>";
echo_debug($stmt->error);
}
$stmt->close();
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche d'enregistrer le client (code 2).</div>";
echo_debug($mysqli->error);
}
}
else {
echo "<div class='message-erreur'>$messageErreur</div>";
}
Parfois, la page qui réalise le INSERT doit effectuer une redirection après avoir accompli son travail. Elle n'affichera donc rien à l'écran.
Voici à nouveau l'extrait de code précédent mais cette fois, il travaille avec des variables de session.
Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.
$prenom = '';
$nomfamille = '';
$_SESSION['operation_reussie'] = false;
$_SESSION['message_operation'] = "";
// Retrouve les données du formulaire.
if (isset($_POST['prenom'])) {
$prenom = $_POST['prenom'];
}
if (isset($_POST['nomfamille'])) {
$nomfamille = $_POST['nomfamille'];
}
// Valide les données.
$messageErreur = '';
// refaire en PHP toutes les validations HTML et JavaScript
// Si tout était valide
if ('' == $messageErreur) {
// Tente l'enregistrement des données.
$requete = "INSERT INTO clients(prenom, nomfamille, actif) VALUES(?, ?, 1)";
$stmt = $mysqli->prepare($requete);
if ($stmt) {
$stmt->bind_param('ss', $prenom, $nomfamille);
$stmt->execute();
if (0 == $stmt->errno) {
$_SESSION['operation_reussie'] = true;
$_SESSION['message_operation'] = "Le client a été ajouté avec succès !";
}
else {
$_SESSION['message_operation'] = "Nous sommes désolés, un problème technique nous empêche d'enregistrer le client (code 1).";
log_debug($stmt->error);
}
$stmt->close();
}
else {
$_SESSION['message_operation'] = "Nous sommes désolés, un problème technique nous empêche d'enregistrer le client (code 2).";
log_debug($mysqli->error);
}
}
else {
$_SESSION['message_operation'] = $messageErreur;
}
Voici finalement un exemple d'utilisation avec un DELETE.
Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.
// Retrouve l'information dans l'URL.
if (isset($_GET['id'])) {
$id = $_GET['id'];
}
else {
$id = -1;
}
// Tente la suppression des données.
$requete = "DELETE FROM clients WHERE id=?";
$stmt = $mysqli->prepare($requete);
if ($stmt) {
$stmt->bind_param('i', $id);
$stmt->execute();
if (0 == $stmt->errno) {
// si l'id n'existe pas, ça ne génère pas d'erreur mais ça ne supprime rien.
if ($stmt->affected_rows > 0) {
echo "<div class='message-information'>Le client a été supprimé avec succès !</div>";
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de supprimer le client (code 1).</div>";
echo_debug("Id de client non trouvé pour la suppression : $id");
}
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de supprimer le client (code 2).</div>";
echo_debug($stmt->error);
}
$stmt->close();
}
else {
echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de supprimer le client (code 3).</div>";
echo_debug($mysqli->error);
}
« Les requêtes préparées ». PHP. http://php.net/manual/fr/mysqli.quickstart.prepared-statements.php
Site fièrement hébergé chez A2 Hosting.