Les requĂȘtes prĂ©parĂ©es

La base de donnĂ©es MySQL supporte les requĂȘtes prĂ©parĂ©es. Une requĂȘte prĂ©parĂ©e ou requĂȘte paramĂ©trable est utilisĂ©e pour exĂ©cuter la mĂȘme requĂȘte plusieurs fois, avec une grande efficacitĂ© et protĂšge des injections SQL.

Flux de travail de base

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.

Le serveur MySQL supporte le mode anonyme, avec des marqueurs de position utilisant le caractĂšre ?.

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 exécute l'instruction avec les valeurs liées en utilisant les ressources internes précédemment créées.

Exemple #1 PremiÚre étape : la préparation

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

// RequĂȘte non prĂ©parĂ©e
$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT, label TEXT)");

// RequĂȘte prĂ©parĂ©e, Ă©tape 1 : prĂ©paration
$stmt = $mysqli->prepare("INSERT INTO test(id, label) VALUES (?, ?)");

// RequĂȘte prĂ©parĂ©e, Ă©tape 2 : lie les valeurs et exĂ©cute la requĂȘte
$id = 1;
$label = 'PHP';
$stmt->bind_param("is", $id, $label); // "is" signifie que $id est lié comme un entier et $label comme une chaßne

$stmt->execute();

Exécution répétée

Une requĂȘte prĂ©parĂ©e peut ĂȘtre exĂ©cutĂ©e Ă  plusieurs reprises. A chaque exĂ©cution, la valeur courante de la variable liĂ©e est Ă©valuĂ©e, et envoyĂ©e au serveur. La requĂȘte n'est pas analysĂ©e de nouveau. Le template de requĂȘte n'est pas une nouvelle fois envoyĂ©e au serveur non plus.

Exemple #2 RequĂȘte de type INSERT prĂ©parĂ©e une seule fois, et exĂ©cutĂ©e plusieurs fois

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

// RequĂȘte non prĂ©parĂ©e
$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT, label TEXT)");

// RequĂȘte prĂ©parĂ©e, Ă©tape 1 : la prĂ©paration
if (!($stmt = $mysqli->prepare("INSERT INTO test(id) VALUES (?)"))) {
echo
"Échec lors de la prĂ©paration : (" . $mysqli->errno . ") " . $mysqli->error;
}

// RequĂȘte prĂ©parĂ©e, Ă©tape 2 : lie les valeurs et exĂ©cute la requĂȘte
$id = 1;
$stmt->bind_param("is", $id, $label); // "is" signifie que $id est lié comme un entier et $label comme une chaßne

$data = [
1 => 'PHP',
2 => 'Java',
3 => 'C++'
];

foreach (
$data as $id => $label) {
$stmt->execute();
}

$result = $mysqli->query('SELECT id, label FROM test');
var_dump($result->fetch_all(MYSQLI_ASSOC));

L'exemple ci-dessus va afficher :

array(3) {
  array(2) {
    ["id"]=>
    string(1) "1"
    ["label"]=>
    string(3) "PHP"
  }
  [1]=>
  array(2) {
    ["id"]=>
    string(1) "2"
    ["label"]=>
    string(4) "Java"
  }
  [2]=>
  array(2) {
    ["id"]=>
    string(1) "3"
    ["label"]=>
    string(3) "C++"
  }
}

Chaque requĂȘte prĂ©parĂ©e occupe des ressources sur le serveur. Elles doivent ĂȘtre fermĂ©es explicitement immĂ©diatement aprĂšs utilisation. Si l'on ne le fait pas, la requĂȘte sera fermĂ©e lorsque le gestionnaire de requĂȘte sera libĂ©rĂ© par PHP.

L'utilisation de requĂȘte prĂ©parĂ©e n'est pas toujours la façon la plus efficace d'exĂ©cuter une requĂȘte. Une requĂȘte prĂ©parĂ©e exĂ©cutĂ©e une seule fois provoque plus d'aller-retour client-serveur qu'une requĂȘte non prĂ©parĂ©e. C'est pour cela que la requĂȘte de type SELECT n'est pas exĂ©cutĂ©e comme requĂȘte prĂ©parĂ©e dans l'exemple ci-dessus.

De plus, il faut prendre en considĂ©ration l'utilisation des syntaxes multi-INSERT MySQL pour les INSERTs. Par exemple, les multi-INSERTs requiĂšrent moins d'aller-retour client-serveur que la requĂȘte prĂ©parĂ©e vue dans l'exemple ci-dessus.

Exemple #3 Moins d'aller-retour en utilisant les multi-INSERTs SQL

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT)");

$values = [1, 2, 3, 4];

$stmt = $mysqli->prepare("INSERT INTO test(id) VALUES (?), (?), (?), (?)");
$stmt->bind_param('iiii', ...$values);
$stmt->execute();

Types de données des valeurs du jeu de résultats

Le protocole serveur client MySQL dĂ©finit un protocole de transfert des donnĂ©es diffĂ©rent pour les requĂȘtes prĂ©parĂ©es et pour les requĂȘtes non prĂ©parĂ©es. Les requĂȘtes prĂ©parĂ©es utilisent un protocole appelĂ© binaire. Le serveur MySQL envoie les donnĂ©es du jeu de rĂ©sultats "tel que", au format binaire. Les rĂ©sultats ne sont pas sĂ©rialisĂ©s en chaĂźnes de caractĂšres avant envoi. La bibliothĂšque cliente reçoit des donnĂ©es binaires et tente de convertir les valeurs en un type de donnĂ©es PHP appropriĂ©. Par exemple, les rĂ©sultats depuis une colonne INT SQL seront fournis comme variables de type entier PHP.

Exemple #4 Types de données natifs

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

// RequĂȘte non prĂ©parĂ©e
$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT, label TEXT)");
$mysqli->query("INSERT INTO test(id, label) VALUES (1, 'PHP')");

$stmt = $mysqli->prepare("SELECT id, label FROM test WHERE id = 1");
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();

printf("id = %s (%s)\n", $row['id'], gettype($row['id']));
printf("label = %s (%s)\n", $row['label'], gettype($row['label']));

L'exemple ci-dessus va afficher :

id = 1 (integer)
label = PHP (string)

Ce comportement diffĂšre pour les requĂȘtes non prĂ©parĂ©es. Par dĂ©faut, les requĂȘtes non prĂ©parĂ©es retournent tous les rĂ©sultats sous forme de chaĂźnes de caractĂšres. Ce comportement par dĂ©faut peut ĂȘtre modifiĂ© en utilisant une option lors de la connexion. Si cette option est utilisĂ©e, alors il n'y aura plus de diffĂ©rence entre une requĂȘte prĂ©parĂ©e et une requĂȘte non prĂ©parĂ©e.

Récupération des résultats en utilisant des variables liées

Les rĂ©sultats depuis les requĂȘtes prĂ©parĂ©es peuvent ĂȘtre rĂ©cupĂ©rĂ©s en liant les variables de sortie, ou en interrogeant l'objet mysqli_result.

Les variables de sortie doivent ĂȘtre liĂ©es aprĂšs l'exĂ©cution de la requĂȘte. Une variable doit ĂȘtre liĂ©e pour chaque colonne du jeu de rĂ©sultats de la requĂȘte.

Exemple #5 Liage des variables de sortie

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

// RequĂȘte non prĂ©parĂ©e
$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT, label TEXT)");
$mysqli->query("INSERT INTO test(id, label) VALUES (1, 'PHP')");

$stmt = $mysqli->prepare("SELECT id, label FROM test WHERE id = 1");
$stmt->execute();

$stmt->bind_result($out_id, $out_label);

$out_id = NULL;
$out_label = NULL;
if (!
$stmt->bind_result($out_id, $out_label)) {
echo
"Échec lors du liage des paramùtres de sortie : (" . $stmt->errno . ") " . $stmt->error;
}

while (
$stmt->fetch()) {
printf("id = %s (%s), label = %s (%s)\n", $out_id, gettype($out_id), $out_label, gettype($out_label));
}

L'exemple ci-dessus va afficher :

id = 1 (integer), label = a (string)

Les requĂȘtes prĂ©parĂ©es retournent des jeux de rĂ©sultats non mis en mĂ©moire tampon par dĂ©faut. Les rĂ©sultats de la requĂȘte ne sont pas implicitement rĂ©cupĂ©rĂ©s et transfĂ©rĂ©s depuis le serveur vers le client pour une mise en mĂ©moire tampon cĂŽtĂ© client. Le jeu de rĂ©sultats prend des ressources serveur tant que tous les rĂ©sultats n'ont pas Ă©tĂ© rĂ©cupĂ©rĂ©s par le client. Aussi, il est recommandĂ© de rĂ©cupĂ©rer rapidement. Si un client Ă©choue dans la rĂ©cupĂ©ration de tous les rĂ©sultats, ou si le client ferme la requĂȘte avant d'avoir rĂ©cupĂ©rĂ© toutes les donnĂ©es, les donnĂ©es doivent ĂȘtre rĂ©cupĂ©rĂ©es implicitement par mysqli.

Il est Ă©galement possible de mettre en mĂ©moire tampon les rĂ©sultats d'une requĂȘte prĂ©parĂ©e en utilisant la fonction mysqli_stmt::store_result().

Récupération des résultats en utilisant l'interface mysqli_result

Au lieu d'utiliser des rĂ©sultats liĂ©s, les rĂ©sultats peuvent aussi ĂȘtre rĂ©cupĂ©rĂ©s via l'interface mysqli_result. La fonction mysqli_stmt::get_result() retourne un jeu de rĂ©sultats mis en mĂ©moire tampon.

Exemple #6 Utilisation de mysqli_result pour récupérer les résultats

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

// RequĂȘte non prĂ©parĂ©e
$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT, label TEXT)");
$mysqli->query("INSERT INTO test(id, label) VALUES (1, 'PHP')");

$stmt = $mysqli->prepare("SELECT id, label FROM test WHERE id = 1");
$stmt->execute();

$result = $stmt->get_result();

var_dump($result->fetch_all(MYSQLI_ASSOC));

L'exemple ci-dessus va afficher :

array(1) {
  [0]=>
  array(2) {
    ["id"]=>
    int(1)
    ["label"]=>
    string(3) "PHP"
  }
}

L'utilisation de l'interface mysqli_result offre d'autres avantages d'un point de vue flexibilité dans la navigation dans le jeu de résultats cÎté client.

Exemple #7 Jeu de résultats mis en mémoire tampon pour plus de flexibilité dans la lecture

<?php

mysqli_report
(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("example.com", "user", "password", "database");

// RequĂȘte non prĂ©parĂ©e
$mysqli->query("DROP TABLE IF EXISTS test");
$mysqli->query("CREATE TABLE test(id INT, label TEXT)");
$mysqli->query("INSERT INTO test(id, label) VALUES (1, 'PHP'), (2, 'Java'), (3, 'C++')");

$stmt = $mysqli->prepare("SELECT id, label FROM test");
$stmt->execute();

$result = $stmt->get_result();

for (
$row_no = $result->num_rows - 1; $row_no >= 0; $row_no--) {
$result->data_seek($row_no);
var_dump($result->fetch_assoc());
}

L'exemple ci-dessus va afficher :

array(2) {
  ["id"]=>
  int(3)
  ["label"]=>
  string(1) "C++"
}
array(2) {
  ["id"]=>
  int(2)
  ["label"]=>
  string(1) "Java"
}
array(2) {
  ["id"]=>
  int(1)
  ["label"]=>
  string(1) "PHP"
}

Échappement et injection SQL

Les variables liĂ©es sont envoyĂ©es au serveur sĂ©parĂ©ment de la requĂȘte, ne pouvant ainsi pas interfĂ©rer avec celle-ci. Le serveur utilise ces valeurs directement au moment de l'exĂ©cution, aprĂšs que le template ne soit analysĂ©. Les paramĂštres liĂ©s n'ont pas besoin d'ĂȘtre Ă©chappĂ©s sachant qu'ils ne sont jamais placĂ©s dans la chaĂźne de requĂȘte directement. Une astuce doit ĂȘtre fournie au serveur pour spĂ©cifier le type de variable liĂ©e, afin d'effectuer la conversion appropriĂ©e. Voir la fonction mysqli_stmt::bind_param() pour plus d'informations.

Une telle sĂ©paration est souvent considĂ©rĂ©e comme la seule fonctionnalitĂ© pour se protĂ©ger des injections SQL, mais le mĂȘme degrĂ© de sĂ©curitĂ© peut ĂȘtre atteint avec les requĂȘtes non-prĂ©parĂ©es, si toutes les valeurs sont correctement formatĂ©es. À noter : qu'un formatage correct n'est pas la mĂȘme chose qu'un Ă©chappement et nĂ©cessite plus de logique qu'un simple Ă©chappement. Aussi, les requĂȘtes prĂ©parĂ©es sont simplement une mĂ©thode plus simple et moins prompte aux erreurs concernant cette approche sĂ©curitaire.

Émulation cĂŽtĂ© client de la prĂ©paration d'une requĂȘte

L'API n'inclut pas d'Ă©mulation cĂŽtĂ© client de la prĂ©paration d'une requĂȘte.

Voir aussi

add a note

User Contributed Notes

There are no user contributed notes for this page.