INFO405 - TP2/3/4

par Tom
le 31/03/2020
  • 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 entier
    • d = nombre réel
    • s = 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é