Blog de Portekoi : Programmation et Astuces

Programmation Cobol, Php, Asp, Java et DotNet…

Requêtes préparées avec Mysqli

Il y a de cela un petit moment, j’avais lu une discussion à propos des requêtes préparées. (prepared statements en anglais)

C’est quoi une requêtes préparée?

La vraie question serait : qu’est-ce qui la différencie d’une requête normale?

Une requête normale :

SELECT mot_de_passe FROM USER WHERE login= 'Portekoi'

Une requête préparée :

SELECT mot_de_passe FROM USER WHERE login= ?

La différence réside dans la méthode de passation des valeurs. Par exemple, ici, le terme « Portekoi » est placé directement dans la requête dans l’exemple 1.

Dans l’exemple 2, la valeur est transmise séparément. C’est l’un des avantages de cette méthode.

Pourquoi utiliser des requêtes préparées?

Il y a en réalité deux avantages :

La sécurité

Comme vu précédemment, la 1ière méthode implique de placer les variables, bien souvent saisies par un utilisateur, dans la requête directement.
Si je reprends la première requête en injectant un code malveillant :

SELECT mot_de_passe FROM USER WHERE login = ''; DELETE * FROM USER WHERE 1 = '1'

Le code injecté est celui-ci :

‘; delete * from user where 1 = ‘1

Si le code n’est pas protégé, cette requête supprimera toutes les données de la table ‘user’

Dans une requête préparée, lorsque l’on transmet les variables, on transmet aussi le type de cette donnée.

Exemple : (Tiré du site : http://www.lephpfacile.com/manuel-php/mysqli.prepare.php )

$mysqli = new mysqli("localhost", "root", "mdp", "basedetest");

$login= "Portekoi";

/* Crée une requête préparée */
if ($stmt = $mysqli->prepare("SELECT mot_de_passe FROM user WHERE login = ? and type = ?")) {

/* Le premier paramètre est le type de la variable. Les 'n' suivants sont les variables. */
/* J'ai volontairement ajouté une seconde close dans le where que je transmets en dur : */
/* %s : string / %d : décimal / %i : int / %b : blob */
$stmt->bind_param("%s%s", $login, 'root');

/* Exécution de la requête */
$stmt->execute();
}

Dans une requête préparée, les données et la requête sont envoyées séparément au serveur. Ainsi, on ne peut l’altérer.

Il n’y a plus besoin de gérer les « quotes » ou la fonction « mysqli_real_escape_string() »

Les performances

Voici les échanges avec le serveur lors d’une requête normale :

  • Envoi de la requête au serveur
  • Compilation
  • Création du plan d’exécution
  • Exécution de la requête
  • Envoi de la réponse

Si la même requête est envoyée 2 fois, ce schéma sera reproduit.

Maintenant, avec une requête préparée :

  • Envoi de la requête au serveur
  • Compilation
  • Création du plan d’exécution
  • Stockage de la requête
  • Envoi de la réponse avec un identifiant

En cas de requêtes répétées :

  • Envoi de la même requête au serveur
  • Exécution
  • Envoi de la réponse

L’utilisation

Comme vu juste avant, l’utilisation des requêtes préparées est un peu lourd à mettre en place.
En effet, entre la requête, la préparation, l’association des variables et l’exécution, cela peut paraitre un peu difficile à utiliser.

Voici un exemple de requête en utilisant une classe trouvée sur le web. J’ai ajouté un pattern « décorateur » afin de renvoyer : un état, un nombre de ligne et le résultat.

//Connexion
$db = new DBExtend($login_db, $mdp_db, $base_db);

$result = $db->DoSelect('SELECT mot_de_passe FROM user WHERE login = ? and type = ?', array($login, 'root'), array('%s', '%s'));

//Lecture des résultats :
if ($result["num_rows"] > 0) {
    //Lecture
    foreach($result["rows"] as $key => $val){
   
    }
}
//débogue
var_dump($result);

//Un insert : traduction : insert into ma_tabme (champs1, champs2) values ('valeur1', 'valuer2')
$result = $db->DoInsert("ma_table", array("champ1" => "valeur1", "champ2_public" => "valeur2"), array("%s", "%s"));

//Un update : traduction : update ma_table set champ1 = 'valeur1', champ2 = 'valeur2' where id = 68
$result = $db->DoUpdate("ma_table", array("champ1" => "valeur1", "champ2" => "valeur2"), array("%s", "%s") , array("id" => 68), array("%d"));

On voit ici clairement la force des requêtes préparées. Meilleure sécurité et performances accrues. Je place ci-dessous les classes PHP.

Class « DB » / Source : http://www.johnmorrisonline.com/simple-php-class-prepared-statements-mysqli/

<?php
//Crédits : http://www.johnmorrisonline.com/simple-php-class-prepared-statements-mysqli/
if ( !class_exists( 'DB' ) ) {
    class DB {
        public function __construct($user, $password, $database, $host = 'localhost') {
            $this->user = $user;
            $this->password = $password;
            $this->database = $database;
            $this->host = $host;
        }
        protected function connect() {
            $c = new mysqli($this->host, $this->user, $this->password, $this->database);
            $c->set_charset("utf8");
            return $c;
        }
        protected function query($query) {
            $db = $this->connect();
            $result = $db->query($query);
            while ( $row = $result->fetch_object() ) {
                $results[] = $row;
            }
            return $results;
        }

        protected function insert($table, $data, $format) {
            // Check for $table or $data not set
            if ( empty( $table ) || empty( $data ) ) {
                return false;
            }
            // Connect to the database
            $db = $this->connect();
            // Cast $data and $format to arrays
            $data = (array) $data;
            $format = (array) $format;
            // Build format string
            $format = implode('', $format);
            $format = str_replace('%', '', $format);
            list( $fields, $placeholders, $values ) = $this->prep_query($data);
            // Prepend $format onto $values
            array_unshift($values, $format);

            // Prepary our query for binding
            $stmt = $db->prepare("INSERT INTO {$table} ({$fields}) VALUES ({$placeholders})");

            // Dynamically bind values
            call_user_func_array( array( $stmt, 'bind_param'), $this->ref_values($values));
            // Execute the query
            $stmt->execute();
            //close
            $db->close();
            // Check for successful insertion
            Tools::WriteFile("logs.txt", "##Statement###");
            Tools::WriteFile("logs.txt", $stmt->affected_rows);
            Tools::WriteFile("logs.txt", "##Statement###");
            if ( $stmt->affected_rows >= 0) {
                return $stmt->insert_id;
                exit();
            }
           
            return $stmt->affected_rows;
        }
       
        protected function update($table, $data, $format, $where, $where_format) {
            // Check for $table or $data not set
            if ( empty( $table ) || empty( $data ) ) {
                return false;
            }
            // Connect to the database
            $db = $this->connect();
            // Cast $data and $format to arrays
            $data = (array) $data;
            $format = (array) $format;
            // Build format array
            $format = implode('', $format);
            $format = str_replace('%', '', $format);
            $where_format = implode('', $where_format);
            $where_format = str_replace('%', '', $where_format);
            $format .= $where_format;
            list( $fields, $placeholders, $values ) = $this->prep_query($data, 'update');
            //Format where clause
            $where_clause = '';
            $where_values = '';
            $count = 0;
            foreach ( $where as $field => $value ) {
                if ( $count > 0 ) {
                    $where_clause .= ' AND ';
                }
                $where_clause .= $field . '=?';
                $where_values[] = $value;
                $count++;
            }

            // Prepend $format onto $values
            array_unshift($values, $format);
            $values = array_merge($values, $where_values);

            // Prepary our query for binding
            $stmt = $db->prepare("UPDATE {$table} SET {$placeholders} WHERE {$where_clause}");
            // Dynamically bind values
            call_user_func_array( array( $stmt, 'bind_param'), $this->ref_values($values));
            // Execute the query
            $stmt->execute();
            //close
            $db->close();
            // Check for successful insertion
            if ( $stmt->affected_rows ) {
                return $stmt->affected_rows;
            }
            return $stmt->affected_rows;
        }
        protected function select($query, $data, $format) {
            // Connect to the database
            $db = $this->connect();
            //Prepare our query for binding
            $stmt = $db->prepare($query);
            //Normalize format
            $format = implode('', $format);
            $format = str_replace('%', '', $format);
            // Prepend $format onto $values
            array_unshift($data, $format);
            //Dynamically bind values
            call_user_func_array( array( $stmt, 'bind_param'), $this->ref_values($data));
            //Execute the query
            $stmt->execute();
            //Fetch results
            $result = $stmt->get_result();
            //close
            $db->close();
            //Create results object
            //fetch_assoc pour obtenir un tableau
            while ($row = $result->fetch_assoc()) {
                $results[] = $row;
            }

            return $results;
        }
        protected function delete($table, $id) {
            // Connect to the database
            $db = $this->connect();
            // Prepary our query for binding
            $stmt = $db->prepare("DELETE FROM {$table} WHERE ID = ?");
            // Dynamically bind values
            $stmt->bind_param('d', $id);
            // Execute the query
            $stmt->execute();
            // Check for successful insertion
            if ( $stmt->affected_rows ) {
                return true;
            }
        }
        private function prep_query($data, $type='insert') {
            // Instantiate $fields and $placeholders for looping
            $fields = '';
            $placeholders = '';
            $values = array();
            // Loop through $data and build $fields, $placeholders, and $values
            foreach ( $data as $field => $value ) {
                $fields .= "{$field},";
                $values[] = $value;
                if ( $type == 'update') {
                    $placeholders .= $field . '=?,';
                } else {
                    $placeholders .= '?,';
                }
            }
            // Normalize $fields and $placeholders for inserting
            $fields = substr($fields, 0, -1);
            $placeholders = substr($placeholders, 0, -1);
            return array( $fields, $placeholders, $values );
        }
        private function ref_values($array) {
            $refs = array();

            foreach ($array as $key => $value) {
                $refs[$key] = &$array[$key];
            }

            return $refs;
        }
    }
}
?>

Ma classe « décorateur » :

<?php

class DBExtend extends DB{


    public function DoInsert($table, $data, $format){

        $r = $this->InitialiseResult();
        $t = $this->insert($table, $data, $format);

        if($t >= 0){
            $r["state"] = true;
            $r["last_id"] = $t;
        }

        return $r;
    }

    public function DoSelect($query, $data, $format){

        $r = $this->InitialiseResult();

        $t = $this->select($query, $data, $format);


        if(count($t) >= 0){
            $r["state"] = true;
            $r["rows"] = $t;
            $r["num_rows"] = count($t);
        }

        return $r;
    }

    public function DoUpdate($table, $data, $format, $where, $where_format){

        $r = $this->InitialiseResult();
        $t = $this->update($table, $data, $format, $where, $where_format);
        if($t >= 0){
            $r["state"] = true;
            $r["num_rows"] = $t;
        }

        return $r;
    }

    /**
     * mysqli::InitialiseResult()
     *
     * @param mixed $p
     * @return
     */

    private function InitialiseResult()
    {
        $r = array(); //on écrase
        $r["state"] = false;

        switch($p){
            case "select":

                $r["rows"] = array();
                $r["num_rows"] = 0;
                break;

        }

        return $r;
    }

}
?>

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *