In questa lezione impari a
- collegarsi al database con mysqli_connect e il set di caratteri giusto
- tenere i dati di accesso in un file di configurazione
- leggere i risultati con mysqli_fetch_all, mysqli_fetch_assoc e mysqli_fetch_row
- mostrare una SELECT in una tabella HTML
- riconoscere lo stesso codice scritto a oggetti e con PDO
Ora si uniscono le due metà del corso: le pagine PHP chiedono i dati a MySQL con le query SQL che abbiamo imparato. Da questa lezione lavoriamo nella cartella C:\xampp\htdocs\biblioteca e sul database biblioteca (importato nella lezione 1). Alla fine del corso questa cartella conterrà l'applicazione completa.
mysqli o PDO?
PHP ha due modi per parlare con MySQL:
- mysqli (MySQL improved): funziona con MySQL e MariaDB. Si può scrivere in due stili: a funzioni (
mysqli_connect,mysqli_query…) e a oggetti (new mysqli,$conn->query…). Fanno esattamente le stesse cose. - PDO (PHP Data Objects): un'interfaccia unica che funziona con molti database (MySQL, PostgreSQL, SQLite, SQL Server…).
Tutti e tre vanno bene e tutti e tre permettono le query preparate. In questo corso usiamo mysqli a funzioni, lo stile dei manuali e delle tracce d'esame. Sotto gli esempi principali trovi, da aprire, lo stesso codice a oggetti e con PDO: così sai leggere qualunque codice ti capiti davanti.
Su vecchi manuali e siti potresti trovare le funzioni
mysql_query()(senza la i). Sono state eliminate in PHP 7: un codice che le usa non funziona più.
La connessione, passo per passo
Anche qui partiamo dalla forma più semplice e la miglioriamo un pezzo alla volta.
1. La connessione più semplice
Crea prova.php:
<?php
$conn = mysqli_connect('localhost', 'root', '', 'biblioteca');
$risultato = mysqli_query($conn, 'SELECT titolo FROM libri ORDER BY titolo');
while ($riga = mysqli_fetch_assoc($risultato)) {
echo $riga['titolo'] . '<br>';
}
mysqli_connect riceve server, utente, password e nome del database (con XAMPP: localhost, root, password vuota) e restituisce la connessione, che salviamo in $conn. Tutte le altre funzioni mysqli_… la ricevono come primo parametro: è il "filo" che collega la pagina a MySQL. Le due righe sotto eseguono una query e stampano i titoli: le vediamo bene tra poco.
Apri http://localhost/biblioteca/prova.php: compaiono i 14 titoli. Funziona, ma ci sono tre problemi, che risolviamo uno alla volta.
2. Le lettere accentate
Guarda Se questo è un uomo. A seconda di come è configurato il server, può comparire come "Se questo è un uomo": PHP e MySQL non si sono messi d'accordo sul set di caratteri. Per non dipendere dalla configurazione, subito dopo la connessione lo si dichiara:
<?php
$conn = mysqli_connect('localhost', 'root', '', 'biblioteca');
mysqli_set_charset($conn, 'utf8mb4'); // le lettere accentate arrivano giuste
3. Quando qualcosa va storto
Spegni MySQL dal pannello di XAMPP e ricarica la pagina:
Fatal error: Uncaught mysqli_sql_exception: No connection could be made because the target machine actively refused it
in C:\xampp\htdocs\biblioteca\prova.php:2
Il programma si ferma con un'eccezione (un errore che interrompe lo script). Per chi sviluppa è utilissimo; ma su un sito vero il visitatore vedrebbe percorsi e dettagli del server, e il messaggio può contenere anche il nome dell'utente del database. Si "cattura" l'eccezione con try / catch:
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // errori di MySQL → eccezioni
try {
$conn = mysqli_connect('localhost', 'root', '', 'biblioteca');
mysqli_set_charset($conn, 'utf8mb4');
} catch (mysqli_sql_exception $e) {
error_log($e->getMessage()); // il dettaglio va nel registro degli errori del server
die('Il database non è raggiungibile. Hai avviato MySQL nel pannello di XAMPP?');
}
- Il codice dentro
tryviene eseguito; se lancia un'eccezione (database spento, password sbagliata), l'esecuzione salta alcatch. - Nel
catchscriviamo il dettaglio nel registro del server conerror_loge mostriamo al visitatore un messaggio comprensibile;diestampa il messaggio e ferma lo script. mysqli_report(...)dice a mysqli di trasformare ogni errore di MySQL in un'eccezione, anche quelli delle query: così una query sbagliata ferma il programma e spiega perché, invece di farlo andare avanti in silenzio con un risultato sbagliato. Da PHP 8.1 è già il comportamento predefinito, ma scriverlo rende il codice uguale ovunque.
4. Un file solo per la connessione
Queste righe servono in ogni pagina che usa il database. Copiarle ovunque è scomodo, e quando il sito andrà su un server vero (con un altro utente e una password) bisognerebbe cambiarle in tutti i file. Le dividiamo in due file.
config.php contiene solo i dati di accesso:
<?php
// Dati di accesso al database. Con XAMPP l'utente è "root" senza password.
const DB_HOST = 'localhost';
const DB_NOME = 'biblioteca';
const DB_UTENTE = 'root';
const DB_PASSWORD = '';
const crea una costante: un valore che non cambia, scritto per convenzione in maiuscolo e usato senza dollaro (DB_NOME).
db.php contiene la connessione del passo 3, con le costanti al posto dei valori:
<?php
require_once __DIR__ . '/config.php';
// Gli errori di MySQL diventano eccezioni: se una query sbaglia, PHP si ferma e dice perché
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$conn = mysqli_connect(DB_HOST, DB_UTENTE, DB_PASSWORD, DB_NOME);
mysqli_set_charset($conn, 'utf8mb4');
} catch (mysqli_sql_exception $e) {
// Al visitatore non si mostrano i dettagli (potrebbero contenere informazioni riservate)
error_log($e->getMessage());
die('Il database non è raggiungibile. Hai avviato MySQL nel pannello di XAMPP?');
}
Da qui in avanti ogni pagina che usa il database inizia con require_once 'db.php'; e poi usa $conn. Per spostare il sito su un altro server si cambia solo config.php.
⚠️
config.phpcontiene la password del database. Non si carica mai su GitHub o in uno zip da condividere, e sul server va protetto. Su XAMPP la password è vuota, ma l'abitudine va presa subito.
__DIR__è la cartella del file corrente.require_once __DIR__ . '/config.php'trova il file anche sedb.phpviene incluso da una pagina che sta in un'altra cartella.
Lo stesso a oggetti (mysqli)
<?php
require_once __DIR__ . '/config.php';
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$conn = new mysqli(DB_HOST, DB_UTENTE, DB_PASSWORD, DB_NOME);
$conn->set_charset('utf8mb4');
} catch (mysqli_sql_exception $e) {
error_log($e->getMessage());
die('Il database non è raggiungibile. Hai avviato MySQL nel pannello di XAMPP?');
}
Invece di passare $conn a una funzione (mysqli_set_charset($conn, …)), si chiama il metodo dell'oggetto con la freccia: $conn->set_charset(…). È l'unica differenza, e vale per tutto il resto della lezione.
Lo stesso con PDO
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NOME . ';charset=utf8mb4', DB_UTENTE, DB_PASSWORD);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // errori come eccezioni
} catch (PDOException $e) {
error_log($e->getMessage());
die('Il database non è raggiungibile. Hai avviato MySQL nel pannello di XAMPP?');
}
PDO riceve una stringa di connessione (DSN) che contiene tipo di database, server, nome e set di caratteri: mysql:host=localhost;dbname=biblioteca;charset=utf8mb4. Per collegarsi a un altro database (per esempio SQLite) cambia solo questa stringa.
La prima SELECT
Crea elenco.php:
<?php
require_once 'db.php';
$risultato = mysqli_query($conn, 'SELECT titolo, anno, genere FROM libri ORDER BY titolo');
$libri = mysqli_fetch_all($risultato, MYSQLI_ASSOC);
?>
<!DOCTYPE html>
<html lang="it">
<head><meta charset="UTF-8"><title>Elenco dei libri</title></head>
<body>
<h1>Elenco dei libri</h1>
<p><?= count($libri) ?> libri nel catalogo.</p>
<table border="1">
<tr><th>Titolo</th><th>Anno</th><th>Genere</th></tr>
<?php foreach ($libri as $l): ?>
<tr>
<td><?= htmlspecialchars($l['titolo']) ?></td>
<td><?= $l['anno'] ?></td>
<td><?= htmlspecialchars($l['genere']) ?></td>
</tr>
<?php endforeach; ?>
</table>
</body>
</html>
Apri http://localhost/biblioteca/elenco.php: compaiono i 14 libri. Due righe di PHP e il database è in pagina.
mysqli_query($conn, $sql)esegue la query e restituisce il risultato.mysqli_fetch_all($risultato, MYSQLI_ASSOC)ne estrae tutte le righe in un array di array associativi: la stessa struttura dell'array$libriscritto a mano nella lezione 8. Infatti ilforeachè identico. SenzaMYSQLI_ASSOCle righe avrebbero indici numerici ($l[0],$l[1]…): meno leggibile.- I testi del database passano da
htmlspecialchars(lezione 9). Dalla prossima lezione useremo la funzione cortah().
mysqli_query()va bene solo per le query scritte interamente da noi, senza nessun dato che arrivi dall'utente. Appena nella query entra un valore da un form o dall'indirizzo, si usano le query preparate della prossima lezione. Non è un consiglio: è la regola più importante di tutto il corso.
Lo stesso a oggetti (mysqli)
<?php
require_once 'db.php';
$risultato = $conn->query('SELECT titolo, anno, genere FROM libri ORDER BY titolo');
$libri = $risultato->fetch_all(MYSQLI_ASSOC);
Si può anche scrivere tutto in una riga: $libri = $conn->query('SELECT …')->fetch_all(MYSQLI_ASSOC);
Lo stesso con PDO
<?php
require_once 'db.php';
$libri = $pdo->query('SELECT titolo, anno, genere FROM libri ORDER BY titolo')->fetchAll(PDO::FETCH_ASSOC);
Per le istruzioni che non restituiscono righe (INSERT, UPDATE, DELETE, CREATE TABLE) PDO ha anche $pdo->exec($sql), che restituisce il numero di righe modificate. Come query(), va usato solo con SQL scritto interamente da noi.
Le query con le JOIN
In PHP la query SQL è una stringa, quindi può essere lunga quanto serve e andare a capo. Ecco i prestiti aperti, con la JOIN su tre tabelle della lezione 6:
<?php
require_once 'db.php';
$sql = 'SELECT s.nome, s.cognome, s.classe, l.titolo, p.data_prestito
FROM prestiti p
JOIN studenti s ON p.studente_id = s.id
JOIN libri l ON p.libro_id = l.id
WHERE p.data_restituzione IS NULL
ORDER BY p.data_prestito';
$prestiti = mysqli_fetch_all(mysqli_query($conn, $sql), MYSQLI_ASSOC);
foreach ($prestiti as $p) {
echo htmlspecialchars("{$p['nome']} {$p['cognome']} ({$p['classe']}): {$p['titolo']}"),
' dal ', date('d/m/Y', strtotime($p['data_prestito'])), '<br>';
}
Prima prova la query in phpMyAdmin, poi copiala in PHP. Se in PHP qualcosa non va, sai già che la query è giusta e l'errore è nel codice PHP (o viceversa). È il metodo che fa risparmiare più tempo.
Una riga, tutte le righe, un conteggio
| Funzione | Restituisce | Quando si usa |
|---|---|---|
mysqli_fetch_all($r, MYSQLI_ASSOC) |
un array con tutte le righe (vuoto se non ce ne sono) | elenchi e tabelle |
mysqli_fetch_assoc($r) |
una riga, cioè un array associativo, oppure null se non ce ne sono più |
quando la query trova al massimo una riga (es. un libro dato l'id) |
mysqli_fetch_row($r)[0] |
il valore della prima colonna della riga | i conteggi: SELECT COUNT(*) ... |
mysqli_num_rows($r) |
quante righe ha restituito la query | "trovati 3 libri" |
<?php
require_once 'db.php';
$totale = mysqli_fetch_row(mysqli_query($conn, 'SELECT COUNT(*) FROM libri'))[0]; // 14
$recente = mysqli_fetch_assoc(mysqli_query($conn, 'SELECT titolo, anno FROM libri ORDER BY anno DESC LIMIT 1'));
$generi = array_column(
mysqli_fetch_all(mysqli_query($conn, 'SELECT DISTINCT genere FROM libri ORDER BY genere'), MYSQLI_ASSOC),
'genere'
);
echo "Libri: $totale<br>";
echo "Il più recente: {$recente['titolo']} ({$recente['anno']})<br>"; // La chiave a stella (1978)
echo 'Generi: ' . implode(', ', $generi); // Avventura, Fantascienza, ...
array_column($righe, 'genere') prende da ogni riga solo la colonna genere e restituisce un array semplice: comodo per i menu a tendina.
I tipi. Con
mysqli_querytutti i valori arrivano come stringhe, anche i numeri:COUNT(*)restituisce'14'. Per stamparli non cambia nulla; quando un numero serve per un calcolo o per un confronto con===, si converte:(int) $totale. Con le query preparate della prossima lezione, invece, le colonne intere arrivano già comeint.
Il modo classico dei manuali è scorrere il risultato riga per riga con un while: mysqli_fetch_assoc restituisce una riga a ogni chiamata e null quando sono finite, e il ciclo si ferma.
<?php
$risultato = mysqli_query($conn, 'SELECT titolo FROM libri ORDER BY titolo');
echo mysqli_num_rows($risultato) . ' libri<br>';
while ($riga = mysqli_fetch_assoc($risultato)) {
echo htmlspecialchars($riga['titolo']) . '<br>';
}
Nel corso useremo soprattutto mysqli_fetch_all + foreach, perché separa bene la parte che legge i dati da quella che scrive l'HTML; ma il while lo troverai spessissimo.
Lo stesso a oggetti (mysqli) e con PDO
| Operazione | mysqli a funzioni | mysqli a oggetti | PDO |
|---|---|---|---|
| Eseguire | mysqli_query($conn, $sql) |
$conn->query($sql) |
$pdo->query($sql) |
| Tutte le righe | mysqli_fetch_all($r, MYSQLI_ASSOC) |
$r->fetch_all(MYSQLI_ASSOC) |
$r->fetchAll(PDO::FETCH_ASSOC) |
| Una riga | mysqli_fetch_assoc($r) |
$r->fetch_assoc() |
$r->fetch(PDO::FETCH_ASSOC) |
| Un conteggio | mysqli_fetch_row($r)[0] |
$r->fetch_row()[0] |
$r->fetchColumn() |
| Numero di righe | mysqli_num_rows($r) |
$r->num_rows |
count($righe) |
| Nessuna riga | null |
null |
false |
Quando la query sbaglia
Prova a scrivere male il nome di una colonna: SELECT titlo FROM libri. Con mysqli_report attivo, PHP si ferma e mostra:
Fatal error: Uncaught mysqli_sql_exception: Unknown column 'titlo' in 'field list'
Il messaggio contiene la causa esatta: in sviluppo è prezioso. Su un sito pubblico invece, con display_errors disattivato, il visitatore vede una pagina d'errore generica e il dettaglio finisce nel registro del server.
| Messaggio | Causa probabile |
|---|---|
No connection could be made / Connection refused |
MySQL non è avviato in XAMPP |
Unknown database 'biblioteca' |
il database non esiste: importa biblioteca.sql |
Access denied for user |
utente o password sbagliati in config.php |
Table 'biblioteca.libro' doesn't exist |
nome della tabella sbagliato (è libri) |
You have an error in your SQL syntax |
errore di sintassi nella query: provala in phpMyAdmin |
Nei vecchi manuali troverai
mysqli_query($conn, $sql) or die(mysqli_error($conn));. Faceva lo stesso lavoro quando gli errori non erano eccezioni; conmysqli_reportnon serve più.
Esercizi
Esercizio 1 – Gli autori. Crea autori.php che mostra, in una tabella, nome, cognome e nazionalità di tutti gli autori in ordine di cognome. Se la nazionalità manca, scrivi "—".
Mostra la soluzione
<?php
require_once 'db.php';
$risultato = mysqli_query($conn, 'SELECT nome, cognome, nazionalita FROM autori ORDER BY cognome, nome');
$autori = mysqli_fetch_all($risultato, MYSQLI_ASSOC);
?>
<!DOCTYPE html>
<html lang="it">
<head><meta charset="UTF-8"><title>Autori</title></head>
<body>
<h1>Autori</h1>
<table border="1">
<tr><th>Nome</th><th>Cognome</th><th>Nazionalità</th></tr>
<?php foreach ($autori as $a): ?>
<tr>
<td><?= htmlspecialchars($a['nome']) ?></td>
<td><?= htmlspecialchars($a['cognome']) ?></td>
<td><?= htmlspecialchars($a['nazionalita'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</table>
</body>
</html>
Una colonna NULL del database arriva in PHP come null: l'operatore ?? la sostituisce con il trattino.
Esercizio 2 – Il cruscotto. Crea cruscotto.php con tre "riquadri" numerici: libri nel catalogo, copie totali, prestiti aperti. Sotto, l'elenco dei generi con il numero di titoli di ciascuno (la query con GROUP BY della lezione 6).
Mostra la soluzione
<?php
require_once 'db.php';
$libri = mysqli_fetch_row(mysqli_query($conn, 'SELECT COUNT(*) FROM libri'))[0];
$copie = mysqli_fetch_row(mysqli_query($conn, 'SELECT SUM(copie) FROM libri'))[0];
$aperti = mysqli_fetch_row(mysqli_query($conn, 'SELECT COUNT(*) FROM prestiti WHERE data_restituzione IS NULL'))[0];
$risultato = mysqli_query($conn, 'SELECT genere, COUNT(*) AS titoli FROM libri GROUP BY genere ORDER BY titoli DESC');
$generi = mysqli_fetch_all($risultato, MYSQLI_ASSOC);
?>
<!DOCTYPE html>
<html lang="it">
<head><meta charset="UTF-8"><title>Cruscotto</title></head>
<body>
<h1>Cruscotto</h1>
<p>📚 <strong><?= $libri ?></strong> libri · 📦 <strong><?= $copie ?></strong> copie · 🔄 <strong><?= $aperti ?></strong> prestiti aperti</p>
<ul>
<?php foreach ($generi as $g): ?>
<li><?= htmlspecialchars($g['genere']) ?>: <?= $g['titoli'] ?></li>
<?php endforeach; ?>
</ul>
</body>
</html>
Con il database del corso: 14 libri, 35 copie, 6 prestiti aperti.
Esercizio 3 – Traduci. Riscrivi le prime righe PHP dell'esercizio 2 (connessione esclusa) prima con mysqli a oggetti e poi con PDO.
Mostra la soluzione
<?php
// mysqli a oggetti
$libri = $conn->query('SELECT COUNT(*) FROM libri')->fetch_row()[0];
$copie = $conn->query('SELECT SUM(copie) FROM libri')->fetch_row()[0];
$aperti = $conn->query('SELECT COUNT(*) FROM prestiti WHERE data_restituzione IS NULL')->fetch_row()[0];
$generi = $conn->query('SELECT genere, COUNT(*) AS titoli FROM libri GROUP BY genere ORDER BY titoli DESC')
->fetch_all(MYSQLI_ASSOC);
<?php
// PDO
$libri = $pdo->query('SELECT COUNT(*) FROM libri')->fetchColumn();
$copie = $pdo->query('SELECT SUM(copie) FROM libri')->fetchColumn();
$aperti = $pdo->query('SELECT COUNT(*) FROM prestiti WHERE data_restituzione IS NULL')->fetchColumn();
$generi = $pdo->query('SELECT genere, COUNT(*) AS titoli FROM libri GROUP BY genere ORDER BY titoli DESC')
->fetchAll(PDO::FETCH_ASSOC);
L'HTML resta identico. fetch_row()[0] di mysqli fa il lavoro di fetchColumn() di PDO.