- ignorer
$stmt->get_result()
→ appeler qu'une seule fois sinon renvoie false
todo
Utilisation de MySQL en PHP
Introduction
Afin de pouvoir communiquer avec une base MySQL depuis un script PHP, on va utiliser les fonctions de la librairie MySQLi, qui est une interface objet permettant de faire toutes sortes de choses pratiques telles que :
- faire des requêtes
Côté langage, le PHP est un mix entre le Python et le Java, c'est à dire que comme le Python, pas de déclaration de variables ou de typage statique, et comme le Java, on a de l'orienté objet et une syntaxe type C (avec des accolades et des points-virgules).
Points importants
- un nom de variable commence toujours par un dollar ($)
- pour afficher, on fait
echo
- pour concaténer (coller) deux chaînes, on utilise l'opérateur
.
- l'opérateur
->
est utilisé pour accéder à un membre d'un objet (c'est l'équivalent du point en Java) - pour créer une liste, c'est comme en Python :
[]
est une liste vide - on peut ajouter un élément à une liste en faisant
$maListe[] = valeur;
Exemple :
$num = 123;
echo "Bonjour " . $num;
affiche Bonjour 123
à l'écran.
Il est possible d'insérer le contenu d'une variable directement dans une chaîne, comme ceci :
echo "Bonjour $num";
Important
Le fichier index.php présent dans votre dossier de TP contient la ligne
require_once "../../system/index.php";
Cette ligne charge tout le logiciel de forum pour la suite du TP.
Afin de pouvoir tester du code PHP en toute sécurité, sans passer par le logiciel du forum, remplacez la ligne par
require_once "../../system/bdd.php";
Et veillez à écrire tout votre code de test après cette dernière.
Pensez bien à remettre index.php
à la place de bdd.php
dès que votre code est prêt afin de pouvoir tester le forum pour la suite du TP.
MySQLi
Bases
MySQLi fournit une interface orientée objet à une base MySQL.
Cela signifie que pour faire des requêtes, on manipulera différents types d'objets.
Pour commencer, durant ce TP, la fonction bdd()
est mise à notre disposition et nous renvoie un objet "base de données". C'est la base de toutes les actions que nous ferons qui seront liées à la base.
Requêtes simples
La première fonction utile de la base de données est query
, comme son nom l'indique, elle sert à exécuter une requête quelconque.
mysqli::query ( string $query [, int $resultmode = MYSQLI_STORE_RESULT ] ) : mixed
Le premier paramètre est la requête, sous forme de chaîne, et le deuxième (optionnel) indique sous quelle forme on souhaite obtenir les résultats. On y reviendra plus tard.
La fonction renvoie false
si une erreur se produit, il faut donc toujours vérifier que le résultat n'est pas false
avant de l'utiliser (à l'aide d'un if
).
Si il n'y a pas d'erreur, elle renvoie un objet "résultat", qui possède notamment un attribut num_rows
qui est le nombre de lignes renvoyées.
Exemple d'utilisation :
// crée une table
bdd()->query("CREATE TABLE truc (id INTEGER, nom VARCHAR(255))");
// insère une ligne
bdd()->query("INSERT INTO truc (nom) VALUES ('bonjour')");
// récupère les lignes
$resultat = bdd()->query("SELECT id, nom FROM truc");
// vérifie si le résultat est non nul
if ($resultat)
{
// affiche le nombre de lignes renvoyées
echo "Il y a " . $resultat->num_rows . " résultats.";
}
Les deux premières lignes ne devraient pas poser de problèmes, il s'agit juste de requêtes toutes simples ne donnant pas de résultat.
La troisième ligne est une requête plus compliquée car elle renvoie un résultat, les lignes renvoyées par le SELECT.
Comme dit plus haut, il faut toujours vérifier que le résultat n'est pas nul avec un if
.
Notons que le if
peut être condensé ainsi :
if ($resultat = bdd()->query("SELECT id, nom FROM truc"))
{
// affiche le nombre de lignes renvoyées
echo "Il y a " . $resultat->num_rows . " résultats.";
}
Si tout va bien, on peut accéder aux données du résultat, ici j'affiche num_rows
qui est le nombre de lignes.
Globalement, dès que vous aurez à faire une requête, le code sera sensiblement toujours le même, c'est-à-dire un appel à bdd()->query(...)
suivi éventuellement d'un if
si on veut traiter les résultats.
Imaginons maintenant qu'on veuille réellement obtenir les résultats ; on va cette fois ci utiliser la fonction $résultat->fetch_object()
, qui sert à renvoyer "la prochaine ligne" sous forme d'un objet.
Elle renvoie soit un objet, soit false
si on est arrivé à la fin des résultats, ce qui nous permet d'écrire le code suivant :
if ($resultat = bdd()->query("SELECT id, nom FROM truc"))
{
while ($ligne = $resultat->fetch_object())
{
echo "Ligne avec ID=$ligne->id et Nom=$ligne->nom\n"; // \n signifie retour à la ligne, comme en C ou en Java
}
}
Requêtes préparées
Il arrive parfois qu'on ait besoin d'insérer des valeurs dans les requêtes ; des valeurs qui ne sont pas connues à l'avance. On pourrait imaginer une recherche par département : on souhaite récupérer tous les employés résidant dans le département 74 :
SELECT * FROM Employe WHERE depart = 74 AND prenom = 'Bob';
On pourrait être tenté de simplement copier la valeur dans la chaîne :
$valeur = 74; // entré par l'utilisateur, dans un formulaire de recherche par exemple
if ($resultat = bdd()->query("SELECT * FROM Employe WHERE depart = $valeur AND prenom = '$prenom'"))
{
...
Mais que se passe-t-il si, par exemple, l'utilisateur rentre autre chose qu'un nombre ? Ou bien qu'il rentre autre chose que son vrai prénom ? Imaginons que pour la requête d'avant, l'utilisateur écrive 0 OR TRUE; DROP TABLE Employe; --
dans département
, on obtiendrait comme requête finale :
SELECT * FROM Employe WHERE depart = 0 OR TRUE; DROP TABLE Employe; -- AND prenom = 'Test';
(--
signifie commentaire)
Ainsi, en voulant effectuer un simple SELECT, on efface toute la table Employé !
Il existe plusieurs moyens de se prémunir de ce genre de problèmes.
Tout d'abord, on peut convertir les valeurs en nombres à l'aide de la fonction int(...)
. Cela suffira pour la plupart des cas de figure.
Néanmoins, cela ne fonctionnera pas lorsqu'on voudra effectuer une recherche de chaîne, il faut donc trouver autre chose. C'est là qu'entrent en scène les requêtes préparées.
Tout le problème que j'ai décrit ici porte en informatique le nom d'injection SQL, et c'est un problème extrêmement important en développement Web. Le principe d'une injection est, dans le cas général, le fait de permettre à un utilisateur d'insérer une chaîne dans une autre chaîne, quand le tout sera ensuite utilisé pour effectuer une requête par exemple.
Les requêtes préparées sont le meilleur moyen d'éviter ça proprement. Pour cela, on sépare la requêtes et les paramètres. Cela signifie qu'on écrira dans la requête des ?
partout où l'on souhaite insérer une valeur :
SELECT * FROM Employe WHERE depart = ? AND prenom = ?
(Note: pas d'apostrophes autour du ?
)
On passe ensuite la requête dans la fonction prepare
qui indique au serveur qu'on va utiliser une requête préparée.
$req = bdd()->prepare("SELECT * FROM Employe WHERE depart = ? AND prenom = ?");
On passe ensuite les paramètres via une fonction séparée, qui permet de s'assurer que les deux ne sont pas mélangés directement. Cette fonction s'appelle en PHP bind_param
, et se présente sous la forme :
mysqli_stmt::bind_param ( string $types , mixed &$var1 [, mixed &$... ] ) : bool
Elle s'applique à un objet requête renvoyé par bdd()->prepare(...)
et prend en paramètres :
- une chaîne contenant les types des paramètres (dans l'ordre d'apparition dans la requête)
i
= nombre entierd
= nombre réels
= chaîne de caractères"iis"
signifie deux entiers puis une chaîne
- les valeurs des paramètres
$req->bind_param("is", $departement, $prenom);
Les paramètres sont donc envoyés de manière sécurisée.
On peut ensuite utiliser la fonction execute()
et get_result()
qui sont les équivalents de query(...)
:
$req->execute();
$resultat = $req->get_result(); // ATTENTION : il ne faut JAMAIS appeler get_result() plus d'une fois
Le code ci-dessus serait équivalent (avec une requête non préparée) à :
$resultat = bdd()->query("......");
query()
est équivalent à execute()
suivi de get_result()
.
EXEMPLES
Exemples de fonctions, pour que vous ayiez une idée de ce qu'on attend de vous :
(ne recopiez PAS ce code, essayez de l'écrire vous-même et surtout de le COMPRENDRE sinon c'est pas drôle)
/**
Modifie le nombre de points de l'utilisateur.
@param id : l'id de l'utilisateur.
@param point : le nombre de point de l'utilisateur.
@return si le nombre de points de l'utilisateur a été modifié ou non.
*/
function modifie_point_utilisateur($id, $point)
{
// initialise la requête préparée
$stmt = bdd()->prepare("UPDATE Utilisateur SET points = ? WHERE id = ?;");
// rentre les paramètres
// premier i = point (entier)
// deuxième i = id (entier)
$stmt->bind_param("ii", $point, $id);
// renvoie true si :
// la requête a été exécutée correctement - execute() a renvoyé true
// ET
// précisément UNE ligne a été affectée
// (si l'utilisateur n'est pas trouvé, alors affected_rows vaudra 0 car 0 lignes auront été affectées)
return $stmt->execute() && bdd()->affected_rows == 1;
}
/**
Sélectionne les messages selon le sujet.
@param id_sujet : l'id du sujet du message
@return la liste des messages avec : id, texte, login (le login de l'auteur), date_creation.
*/
function recupere_message_par_sujet($id_sujet)
{
// initialise la requête préparée
$stmt = bdd()->prepare("SELECT c.id, c.texte, u.login, c.date_creation FROM Commentaire c, Utilisateur u WHERE c.sujet = ? AND c.auteur = u.id;");
// rentre les paramètres
// i = id (entier)
$stmt->bind_param("i", $id_sujet);
// crée une liste vide pour stocker les résultats
$elems = [];
// si la requête a été exécutée correctement
// -> alors on récupère le résultat (get_result()) dans $res
// ET si le résultat ($res) est valide
if ($stmt->execute() && $res = $stmt->get_result())
{
// tant qu'il reste des lignes à traiter, on stocke la prochaine dans $elem
while ($elem = $res->fetch_object())
{
// on ajoute $elem à la liste des résultats
$elems[] = $elem;
}
}
return $elems;
}
Mots de passes
règle #1 : ne jamais stocker des mots de passe en clair, jamais, peu importe la raison
en PHP, on utilise la fonction password_hash($mot_de_passe, PASSWORD_DEFAULT)
qui renvoie le mot de passe sous un forme "cryptée"
et on peut ensuite utiliser la fonction password_verify($mot_de_passe_saisi, $mot_de_passe_enregistre)
qui renvoie true ou false selon si le mot de passe saisi correspond à celui enregistré