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=utf8mb4va 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$sortnell'SQL. LIMIT ?con i prepared statement emulati. ConATTR_EMULATE_PREPARESdi default su MySQL,execute(['10'])inviaLIMIT '10', un errore di sintassi. Collegalo conPDO::PARAM_INToppure disattiva l'emulazione.- Lo stesso segnaposto con nome due volte.
WHERE first = :q OR last = :qfunziona solo con i prepared statement emulati. Con quelli reali ottieniSQLSTATE[HY093]: Invalid parameter number; usa:q1e:q2. - Dimenticare che
fetch()restituiscefalse.$user['name']sufalseè un warning enull; 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.