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