Vai al contenuto

PHP e MySQL: la connessione con mysqli

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 try viene eseguito; se lancia un'eccezione (database spento, password sbagliata), l'esecuzione salta al catch.
  • Nel catch scriviamo il dettaglio nel registro del server con error_log e mostriamo al visitatore un messaggio comprensibile; die stampa 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.php contiene 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 se db.php viene 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 $libri scritto a mano nella lezione 8. Infatti il foreach è identico. Senza MYSQLI_ASSOC le 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 corta h().

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_query tutti 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à come int.

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; con mysqli_report non 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.