PHP MySQL Prepared Statements: Guida Completa
I prepared statements aumentano sicurezza ed efficienza delle app PHP con database. Questa guida copre MySQLi e PDO con esempi pratici.
I prepared statements sono la tecnica più importante per scrivere codice PHP sicuro che interagisce con un database. Separano il comando SQL dai dati su cui opera, il che blocca completamente le SQL injection e velocizza anche le query eseguite ripetutamente. Questa guida spiega cosa sono i prepared statements, perché sono importanti e come usarli con le estensioni MySQLi e PDO.
Questa pagina tratta:
- Cos'è un prepared statement e perché esiste il modello "compilare una volta, eseguire molte volte"
- Scrivere query
INSERTeSELECTpreparate con MySQLi - Gli stessi pattern con PDO (segnaposto con nome)
- Errori comuni: segnalazione degli errori, binding del tipo errato e riutilizzo degli statement
Cosa sono i Prepared Statements?
Un prepared statement è una query SQL inviata al database in due fasi:
- Prepare — si invia l'SQL con
?(o:nome) come segnaposto al posto dei valori reali. Il database analizza, compila e ottimizza questo template una sola volta. - Execute — si inviano i valori effettivi separatamente. Il database li inserisce nel piano già compilato e lo esegue.
Poiché i valori viaggiano su un canale diverso rispetto al testo SQL, il database non confonde mai i dati con i comandi. Un valore come ' OR '1'='1 viene trattato come una stringa letterale da cercare, non come SQL da eseguire — ed è esattamente per questo che gli attacchi di injection falliscono contro i prepared statements.
Perché Usare i Prepared Statements?
- Sicurezza. L'input dell'utente non può mai alterare la struttura della query. È la difesa consigliata contro le SQL injection e il motivo per cui non si dovrebbero mai costruire query concatenando variabili in una stringa.
- Prestazioni. La query viene analizzata e compilata una sola volta. Se la si esegue molte volte (ad esempio inserendo 1.000 righe in un ciclo), il database riutilizza lo stesso piano invece di ri-analizzarla ogni volta.
- Codice più pulito. I segnaposto eliminano la necessità di escape manuale con
mysqli_real_escape_string()e la gestione delle virgolette. Si fa il binding di una variabile e il gioco è fatto.
Regola pratica: nel momento in cui qualsiasi parte di una query proviene dall'input dell'utente — un campo di un modulo, un parametro URL, un cookie — usa un prepared statement.
I Passaggi
Ogni prepared statement segue lo stesso ciclo di vita:
- Connessione al database.
- Prepare dell'SQL con i segnaposto.
- Bind delle variabili ai segnaposto.
- Execute dello statement.
- Fetch dei risultati (per le query
SELECT). - Close dello statement.
INSERT Preparato con MySQLi
MySQLi utilizza segnaposto posizionali ?. Si fa il binding con mysqli_stmt_bind_param(), dove il primo argomento è una stringa di tipo: s per string, i per integer, d per double/float, b per blob.
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // throw on errors
$conn = mysqli_connect("localhost", "username", "password", "database");
$stmt = mysqli_prepare($conn, "INSERT INTO users (name, email) VALUES (?, ?)");
// "ss" => both placeholders are strings, in order
mysqli_stmt_bind_param($stmt, "ss", $name, $email);
$name = "John";
$email = "[email protected]";
mysqli_stmt_execute($stmt); // inserts John
$name = "Jane";
$email = "[email protected]";
mysqli_stmt_execute($stmt); // reuses the same compiled statement, inserts Jane
mysqli_stmt_close($stmt);
mysqli_close($conn);bind_param fa il binding per riferimento, quindi è possibile modificare $name/$email e chiamare di nuovo execute() senza ri-fare il binding — i nuovi valori vengono acquisiti automaticamente. Questo è il vantaggio del "compilare una volta, eseguire molte volte" in azione.
SELECT Preparato con MySQLi
Per una SELECT, si esegue lo statement e poi si leggono le righe. Il modo più pulito è mysqli_stmt_get_result(), che restituisce un normale result set su cui si può iterare:
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$conn = mysqli_connect("localhost", "username", "password", "database");
$stmt = mysqli_prepare($conn, "SELECT id, name FROM users WHERE email = ?");
mysqli_stmt_bind_param($stmt, "s", $email);
$email = "[email protected]";
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while ($row = mysqli_fetch_assoc($result)) {
echo $row["id"] . ": " . $row["name"] . "\n";
}
mysqli_stmt_close($stmt);
mysqli_close($conn);Prepared Statements con PDO
PDO è l'altra estensione comune per i database e molti sviluppatori la preferiscono perché funziona su diversi sistemi di database e supporta i segnaposto con nome (:email), che sono più facili da leggere.
<?php
$pdo = new PDO(
"mysql:host=localhost;dbname=database;charset=utf8mb4",
"username",
"password",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// INSERT with named placeholders
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
$stmt->execute([":name" => "John", ":email" => "[email protected]"]);
// SELECT and fetch
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE email = :email");
$stmt->execute([":email" => "[email protected]"]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
echo $row["id"] . ": " . $row["name"] . "\n";
}Si noti che con PDO non è necessario dichiarare i tipi — si passa un array associativo di valori direttamente a execute(). Impostare PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION fa sì che PDO lanci un'eccezione in caso di errore, in modo che i problemi non vengano mai ignorati silenziosamente.
Errori Comuni
- Dimenticare la segnalazione degli errori. Per impostazione predefinita MySQLi può fallire silenziosamente. Chiamare
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)(o usare le eccezioni PDO) in modo che una query errata lanci un'eccezione invece di restituirefalse. - Binding con il numero errato di tipi. La stringa di tipo in
bind_paramdeve avere esattamente un carattere per ogni?."ss"per due segnaposto,"si"per una string seguita da un integer. - Inserire un segnaposto dove SQL non lo consente. È possibile fare il binding dei valori, non degli identificatori.
WHERE id = ?funziona;ORDER BY ?oSELECT * FROM ?non funziona — i nomi di tabelle e colonne devono essere codificati nel sorgente o inclusi in una whitelist. - Concatenare "solo questo" valore. Non esiste un'eccezione sicura. Se proviene da un utente, va fatto il binding.
Conclusione
I prepared statements dividono una query in un template SQL compilato più i valori che lo completano. Questa separazione è ciò che li rende sia sicuri (resistenti alle injection) sia veloci (analizzati una volta, eseguiti molte volte). È possibile usare i segnaposto ? di MySQLi o i segnaposto con nome :value di PDO, ma bisogna sempre fare il binding dell'input dell'utente invece di costruire l'SQL manualmente.
Continua con i capitoli correlati: Connetti a MySQL, Inserisci Dati, Seleziona Dati, e il riferimento mysqli_prepare().