Aller au contenu
M07 · C. Manipuler les données

Interroger la base depuis un formulaire : requêtes préparées

Envoyer à MySQL une valeur saisie par l'utilisateur sans danger avec prepare et execute, les marqueurs ? et :nom, la recherche LIKE, la page de détail ?id=, le cas « aucun résultat », rowCount et lastInsertId.

1Situer

Le visiteur de Souk Atlas tape « argan » dans la barre de recherche, ou clique sur un produit pour voir sa fiche. Dans les deux cas, une valeur venue du navigateur doit entrer dans une requête SQL. C'est le moment le plus sensible d'un site dynamique : on le fait toujours avec une requête préparée.

2Comprendre

Une requête préparée : le SQL d'abord, les valeurs ensuite
recherche : argan SELECT nom, prix FROM produits WHERE nom LIKE ? 1. prepare : la structure part seule 2. execute : la valeur part à part MySQL · base souk Huile d'argan 250 ml 120.00 fetchAll() → [ ['nom' => "Huile d'argan 250 ml", 'prix' => …] ] 0 ligne : aucun nom ne contient ce texte le OR n'a jamais été lu comme du SQL

Le visiteur cherche « argan ». Cette valeur vient du navigateur : on ne la colle pas dans le SQL.

1 / 5

Pourquoi on ne colle jamais une valeur dans le SQL

On pourrait écrire $pdo->query("SELECT * FROM produits WHERE nom LIKE '%$q%'"). Ça marche… jusqu’à ce que quelqu’un cherche huile d'argan : l’apostrophe ferme la chaîne SQL et la requête plante. Et un visiteur malveillant peut taper du SQL à la place d’un mot : c’est l’injection SQL, étudiée en détail à la leçon C.5.

$req = $pdo->prepare('SELECT nom, prix FROM produits WHERE prix <= ? AND stock > ?');
$req->execute([150, 0]);          // les ? sont remplis dans l'ordre
$produits = $req->fetchAll();

Avec des marqueurs nommés, l’ordre n’a plus d’importance et la requête se lit mieux :

$req = $pdo->prepare('SELECT * FROM clients WHERE ville_id = :ville AND date_inscription >= :depuis');
$req->execute(['ville' => 1, 'depuis' => '2024-06-01']);

On peut aussi lier chaque valeur avec bindValue(':ville', 1, PDO::PARAM_INT) avant un execute() sans argument. C’est utile pour imposer un type, par exemple pour LIMIT :n.

Afficher les résultats

Le schéma est presque toujours le même : lire les paramètres, préparer, exécuter, puis boucler sur les lignes ou traiter le cas « rien trouvé ».

<?php
$req = $pdo->prepare('SELECT id, nom, prix FROM produits WHERE nom LIKE ?');
$req->execute(["%$q%"]);
$produits = $req->fetchAll();
?>
<?php if (!$produits): ?>
  <p>Aucun produit pour « <?= htmlspecialchars($q) ?> ».</p>
<?php else: ?>
  <ul>
    <?php foreach ($produits as $p): ?>
      <li><a href="produit.php?id=<?= $p['id'] ?>"><?= htmlspecialchars($p['nom']) ?></a></li>
    <?php endforeach; ?>
  </ul>
<?php endif; ?>

Un tableau vide est faux pour if, d’où if (!$produits). Et toute valeur affichée passe par htmlspecialchars, même si elle vient de votre base : un nom de produit peut contenir < ou &.

La page de détail

Après une écriture : rowCount et lastInsertId

Les requêtes préparées servent aussi pour INSERT, UPDATE et DELETE (leçon C.3). Deux méthodes renseignent sur ce qui s’est passé :

  • $req->rowCount() : le nombre de lignes modifiées, ajoutées ou supprimées ;
  • $pdo->lastInsertId() : l’identifiant AUTO_INCREMENT que MySQL vient d’attribuer.
$req = $pdo->prepare('INSERT INTO villes (nom, region) VALUES (?, ?)');
$req->execute(['Meknès', 'Fès-Meknès']);
echo "Ville n° " . $pdo->lastInsertId();   // Ville n° 7
Exemple simple

Le formulaire à trous

La banque vous tend un chèque déjà imprimé avec des cases vides : vous ne pouvez écrire que dans les cases, pas modifier le texte imprimé. Une requête préparée, c'est le chèque : le SQL est fixé d'avance, l'utilisateur ne remplit que les valeurs.

Exemple simple

La fiche produit

Sur un site marchand, l'adresse produit.php?id=9 affiche le tapis berbère. Le même fichier sert pour tous les produits : seul l'identifiant change, et c'est la base qui fournit le reste.

3Manipuler

Terminez la recherche du catalogue et la page de détail. Les deux reçoivent une valeur du navigateur : utilisez des requêtes préparées.
Pas à pas dans le lab ↗ Cliquez sur Exécuter : PHP 8.3 démarre dans votre navigateur (environ 5 Mo à télécharger, une seule fois).

La page s'affichera ici.

Le navigateur demandera /recherche.php au serveur, qui exécutera le PHP et renverra du HTML.

Entre le navigateur et le serveur aucun échange pour l'instant

Consignes 0 / 4

  1. Chercher « argan » trouve l'huile d'argan, avec une requête préparée
  2. Chercher « caftan » affiche « Aucun produit »
  3. produit.php?id=9 affiche Tapis berbère, Textile et 1 200,00 DH
  4. produit.php?id=99 répond 404 « Produit introuvable »

4Pratiquer

Exercice 1 · Filtrer par prix et catégorie

Ajoutez au formulaire une liste déroulante des catégories (lue dans la base) et un prix maximum. La requête combine les deux avec des marqueurs nommés.

Voir le corrigé
<?php
require __DIR__ . '/db.php';
$cats = $pdo->query('SELECT id, nom FROM categories ORDER BY nom')->fetchAll();
$cat = (int) ($_GET['cat'] ?? 0);
$max = (float) ($_GET['max'] ?? 10000);

$req = $pdo->prepare('SELECT nom, prix FROM produits
                      WHERE categorie_id = :cat AND prix <= :max ORDER BY prix');
$req->execute(['cat' => $cat, 'max' => $max]);
$produits = $req->fetchAll();
?>
<form>
  <select name="cat">
    <?php foreach ($cats as $c): ?>
      <option value="<?= $c['id'] ?>" <?= $c['id'] == $cat ? 'selected' : '' ?>><?= htmlspecialchars($c['nom']) ?></option>
    <?php endforeach; ?>
  </select>
  <input type="number" name="max" value="<?= $max ?>">
  <button>Filtrer</button>
</form>

Exercice 2 · Les commandes d'un client

Une page commandes.php?client=1 affiche le nom du client puis ses commandes (date, statut, montant total calculé en SQL avec SUM et GROUP BY).

Voir le corrigé
<?php
require __DIR__ . '/db.php';
$id = (int) ($_GET['client'] ?? 0);
$req = $pdo->prepare('SELECT c.id, c.date_commande, c.statut,
                             SUM(l.quantite * l.prix_unitaire) AS total
                      FROM commandes c JOIN lignes_commande l ON l.commande_id = c.id
                      WHERE c.client_id = ?
                      GROUP BY c.id, c.date_commande, c.statut
                      ORDER BY c.date_commande');
$req->execute([$id]);
foreach ($req->fetchAll() as $cmd) {
    echo "Commande n° {$cmd['id']} du {$cmd['date_commande']} ({$cmd['statut']}) : {$cmd['total']} DH<br>";
}

5Vérifier

1. Dans une requête préparée, que devient la valeur passée à execute() ?
2. Pour chercher les noms qui contiennent « sav », que passe-t-on à execute() avec « nom LIKE ? » ?
3. Que renvoie fetch() quand aucune ligne ne correspond ?