Kodokon kodokon.com

PDO : requêtes préparées et transactions

Configurez PDO comme en production, éliminez structurellement l'injection SQL et fiabilisez vos écritures avec les transactions.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

PDO est l'interface d'accès unifiée aux bases de données : MySQL, PostgreSQL ou SQLite se pilotent avec la même API. Trois réglages séparent une connexion naïve d'une connexion de production : ERRMODE_EXCEPTION pour que chaque erreur SQL lève une exception au lieu d'échouer en silence, FETCH_ASSOC par défaut pour des résultats propres, et EMULATE_PREPARES à false pour des requêtes préparées réellement exécutées par le serveur. Ajoutez charset=utf8mb4 directement dans le DSN - c'est le seul endroit fiable pour le déclarer.

PHP
<?php

declare(strict_types=1);

$dsn = 'mysql:host=localhost;dbname=shop'
    . ';charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

$pdo = new PDO($dsn, 'app_user', 'secret', $options);
Connexion de production : exceptions, fetch associatif, préparation native.

La requête préparée est votre seule défense structurelle contre l'injection SQL. Le principe : la structure de la requête, avec ses marqueurs :email ou ?, part au serveur d'abord ; les données suivent séparément et ne sont jamais interprétées comme du SQL. Concaténer une variable dans une requête, même « échappée », reste une faute professionnelle. Les marqueurs nommés rendent le code lisible ; les positionnels suffisent pour les requêtes courtes.

PHP
<?php

declare(strict_types=1);

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION,
);

$pdo->exec(
    'CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        email TEXT NOT NULL
    )'
);

$insert = $pdo->prepare(
    'INSERT INTO users (email) VALUES (:email)'
);
$insert->execute(['email' => 'lea@example.com']);

$query = $pdo->prepare(
    'SELECT id, email FROM users
     WHERE email = :email'
);
$query->execute(['email' => 'lea@example.com']);

var_dump($query->fetch(PDO::FETCH_ASSOC));
Exemple autonome sur SQLite : préparation, liaison, exécution.

Une transaction garantit l'atomicité : soit toutes les écritures aboutissent, soit aucune. Le pattern canonique : beginTransaction(), le travail dans un try, commit() en fin de bloc, rollBack() dans le catch - puis on relance l'exception, car masquer l'échec serait pire que l'échec lui-même. Gardez les transactions courtes : chaque verrou tenu pendant un appel réseau ou un traitement long dégrade la concurrence de toute l'application.

PHP
<?php

declare(strict_types=1);

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION,
);

$pdo->exec(
    'CREATE TABLE accounts (
        id INTEGER PRIMARY KEY,
        balance INTEGER NOT NULL
    )'
);
$pdo->exec(
    'INSERT INTO accounts (balance)
     VALUES (100), (20)'
);

$pdo->beginTransaction();

try {
    $debit = $pdo->prepare(
        'UPDATE accounts
         SET balance = balance - :n
         WHERE id = :id'
    );
    $debit->execute(['n' => 50, 'id' => 1]);

    $credit = $pdo->prepare(
        'UPDATE accounts
         SET balance = balance + :n
         WHERE id = :id'
    );
    $credit->execute(['n' => 50, 'id' => 2]);

    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

echo 'Transfer done';
Virement atomique : tout aboutit, ou tout est annulé.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Pourquoi une requête préparée bloque-t-elle l'injection SQL ?
    • Elle échappe automatiquement les apostrophes dans les chaînes
    • La structure SQL et les données voyagent séparément : les données ne sont jamais interprétées comme du code SQL
    • Elle chiffre la requête entre PHP et le serveur
  2. Que peut-on passer via un marqueur :param d'une requête préparée ?
    • Une valeur : chaîne, nombre ou null
    • Un nom de table ou de colonne
    • Une clause ORDER BY complète
  3. Dans le pattern transactionnel canonique, que fait-on dans le bloc catch ?
    • commit() pour sauvegarder ce qui a réussi
    • rollBack(), puis on relance l'exception
    • Rien : la transaction expire d'elle-même