Menu

PHP PDO: connettersi a MySQL e usare i prepared statement

PDO è il livello di accesso ai database di PHP: connettiti con new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $user, $pass), poi esegui ogni query con valori tramite prepare() ed execute([$value]). Impara connessione, prepared statement, lettura delle righe, insert, transazioni e WHERE IN.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

PDO (PHP Data Objects) è il modo integrato di comunicare con un database. Connettiti con new PDO($dsn, $user, $password), poi esegui ogni query che contiene un valore con prepare() ed execute(), così il valore viene inviato separatamente dall'SQL:

<?php
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'shop_user', 'secret', [
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();      // an array, or false if no row matched

Gli esempi di questa pagina richiedono un server di database, quindi sono mostrati come codice semplice. L'esempio sulla SQL injection più sotto è eseguibile.

Connettersi a MySQL con PDO

Il primo argomento è un DSN: il nome del driver, poi i dettagli della connessione. Metti la connessione in un unico posto e includila dove ti serve.

<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,  // the default since PHP 8.0
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,        // rows as ['id' => 1, ...]
    PDO::ATTR_EMULATE_PREPARES   => false,                   // real prepared statements, real int types
];

try {
    $pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), $options);
} catch (PDOException $e) {
    error_log($e->getMessage());
    http_response_code(500);
    exit('Database unavailable.');
}

return $pdo;
  • charset=utf8mb4 va nel DSN. Senza, accenti ed emoji possono arrivare come ? o byte corrotti.
  • Non stampare mai $e->getMessage() per i visitatori: può contenere l'host e il nome utente. Registralo in un log.
  • Tieni la password fuori dal codice, in una variabile d'ambiente o in un file di configurazione fuori dalla radice web.

Gli altri database cambiano solo il DSN: pgsql:host=localhost;dbname=shop per PostgreSQL, sqlite:/var/data/app.db per un file SQLite. Ognuno richiede il suo driver PDO installato (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m elenca quelli che hai.

Prepared statement con ? e :name

Un segnaposto indica dove va un valore. Usa i segnaposto posizionali ? con una lista, oppure quelli con nome con un array associativo:

<?php
// Positional
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = ? AND price < ?');
$stmt->execute(['books', 20]);

// Named: easier to read when there are many values
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = :cat AND price < :max');
$stmt->execute(['cat' => 'books', 'max' => 20]);

// bindValue when you want to set the type explicitly
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY id LIMIT :limit');
$stmt->bindValue('limit', 10, PDO::PARAM_INT);
$stmt->execute();

Non mettere le virgolette intorno a un segnaposto: WHERE name = '?' confronta con il testo letterale ?. Un prepared statement si può anche eseguire più volte con valori diversi, quindi preparalo una volta prima di un ciclo, non al suo interno.

Perché la concatenazione di stringhe nell'SQL è pericolosa

Questo è l'unico blocco della pagina che viene eseguito: costruisce la query nel modo sbagliato e in quello giusto per lo stesso input. Cambia $email ed eseguilo di nuovo.

Nella query concatenata, l'apice nell'input chiude la stringa, e OR '1'='1' rende vera la condizione per ogni riga: la query restituisce tutti gli utenti. In un form di login significa accedere senza conoscere la password, e se il driver accetta più istruzioni in una chiamata, un input come '; DROP TABLE users; -- esegue una seconda istruzione scelta dall'attaccante. Nella versione preparata il database riceve l'SQL e il valore separatamente, quindi l'apice è solo un carattere dentro un'email che non corrisponde a nessuno.

Le funzioni di escape come addslashes() non sono una soluzione: mancano casi che dipendono dal set di caratteri della connessione. Usa i segnaposto per ogni valore, compresi i numeri e i valori che "arrivano dal tuo stesso codice".

Leggere righe: fetch, fetchAll e fetchColumn

<?php
// One row
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch();                 // ['id' => 42, 'name' => 'Ada', ...] or false
if ($user === false) {
    http_response_code(404);
}

// All rows
$rows = $pdo->query('SELECT id, name FROM users ORDER BY name')->fetchAll();

// Loop row by row without building an array of all rows
$stmt = $pdo->query('SELECT id, name FROM users');
foreach ($stmt as $row) {
    echo $row['name'], "\n";
}

// One value
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();

// Two columns as key => value
$names = $pdo->query('SELECT id, name FROM users')->fetchAll(PDO::FETCH_KEY_PAIR);  // [42 => 'Ada', ...]

query() va bene quando l'SQL non contiene valori provenienti dall'esterno. Appena è coinvolta una variabile, passa a prepare().

Insert, update e delete

<?php
$stmt = $pdo->prepare('INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$email, $name, password_hash($password, PASSWORD_DEFAULT)]);
$id = (int) $pdo->lastInsertId();

$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute([$newName, $id]);
echo $stmt->rowCount();                 // rows changed

$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();

lastInsertId() restituisce una stringa, quindi convertila. Con MySQL, rowCount() dopo un UPDATE conta le righe effettivamente modificate, quindi impostare un nome al valore che ha già dà 0. Salva le password con password_hash, mai in chiaro.

Transazioni

Una transazione fa riuscire o fallire insieme più istruzioni. Se qualcosa lancia un'eccezione, annulla con il rollback e non viene salvato nulla:

<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([100, $from]);
    $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([100, $to]);
    $pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')->execute([$from, $to, 100]);

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

Questo presuppone che gli errori lancino eccezioni (il default da PHP 8.0), quindi un'istruzione fallita salta al catch. Con MySQL, le tabelle devono usare InnoDB; MyISAM ignora le transazioni. La pagina su try/catch tratta Throwable e il rilancio.

WHERE IN con un array

Un singolo ? contiene un singolo valore, quindi IN (?) con un array non funziona. Costruisci un segnaposto per valore:

<?php
$ids = [3, 8, 15];

if ($ids) {
    $placeholders = implode(',', array_fill(0, count($ids), '?'));   // "?,?,?"
    $stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
    $stmt->execute($ids);
    $products = $stmt->fetchAll();
} else {
    $products = [];                     // IN () would be a syntax error
}

La stringa inserita nell'SQL è fatta solo di punti interrogativi e virgole, quindi la query resta sicura.

Errori comuni con i segnaposto

  • Nomi di tabelle e colonne. ORDER BY ? ordina per una stringa costante, non per la colonna. Scegli gli identificatori da un elenco consentito: $sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, poi metti $sort nell'SQL.
  • LIMIT ? con i prepared statement emulati. Con ATTR_EMULATE_PREPARES di default su MySQL, execute(['10']) invia LIMIT '10', un errore di sintassi. Collegalo con PDO::PARAM_INT oppure disattiva l'emulazione.
  • Lo stesso segnaposto con nome due volte. WHERE first = :q OR last = :q funziona solo con i prepared statement emulati. Con quelli reali ottieni SQLSTATE[HY093]: Invalid parameter number; usa :q1 e :q2.
  • Dimenticare che fetch() restituisce false. $user['name'] su false è un warning e null; verifica prima il risultato.

Domande frequenti

Come mi connetto a MySQL con PDO?

Crea un oggetto PDO con un DSN, un utente e una password: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Una connessione fallita lancia una PDOException; catturala, registra il messaggio e mostra al visitatore un errore generico.

I prepared statement prevengono la SQL injection?

Sì, per i valori: con prepare('... WHERE id = ?') ed execute([$id]) il valore viene inviato come dato e non può mai cambiare la query. I segnaposto non possono rappresentare nomi di tabelle, nomi di colonne o ASC/DESC, quindi verificali con un elenco di nomi consentiti prima di metterli nell'SQL.

Qual è la differenza tra fetch e fetchAll in PDO?

fetch() restituisce la riga successiva (oppure false quando non ce ne sono più), quindi è adatto a una singola riga o a un ciclo su un risultato grande. fetchAll() restituisce tutte le righe in un array in una volta. fetchColumn() restituisce un valore della riga successiva, comodo per SELECT COUNT(*).

Come uso WHERE IN con un array in PDO?

Costruisci un ? per valore e passa l'array a execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Verifica prima che l'array non sia vuoto, perché IN () è un errore di sintassi.

Meglio PDO o mysqli?

Usa PDO a meno che non ti serva una funzionalità esclusiva di MySQL. La sua API è la stessa per MySQL, PostgreSQL, SQLite e altri, supporta i segnaposto con nome e da PHP 8.0 lancia eccezioni sugli errori di default. mysqli funziona solo con MySQL e MariaDB.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA