Lezione Precedente Elenco Lezioni Lezione Successiva

Laboratorio di Sistemi Informativi

PHP e MySQL

Il PHP 5 mette a disposizione del programmatore un'insieme di funzioni predefinite (API) per l'interfacciamento a MySQL. In realtà esistono varie API differenti che ci consentono di utilizzare MySQL: noi utilizzeremo la API denominata PDO, abbreviazione di PHP Data Objects. PDO è una libreria generica che consente di accedere a diversi DBMS: gli stessi comandi si usano sia per accedere ad un database MySQL che per un database Postgres o Oracle. In questo si differenzia da altre API specifiche per MySQL, come la libreria mysqli che veniva utilizzata nel corso dell'anno scorso.


ATTENZIONE! Prima di proseguire, si noti che da ora in poi, per eseguire i programmi PHP di esempio, configureremo PHP in modo da visualizzare su schermo tutti i messaggi di errore, compresi quelli di tipo Notice. Per far ciò, sarà sufficiente scaricare il file puntohtaccess, cambiare il nome in .htaccess, e salvarlo nella propria directory public_html. Il file in questione modifica altri parametri di configurazione di PHP, che però studieremo solo in seguito.
ATTENZIONE BIS! D'ora in avanti svilupperemo lezione dopo lezione l'applicazione Mancolista. Sarà quindi necessario disporre delle tabelle relative all'applicazione, e di un utente con gli opportuni diritti per leggere e modificare il loro contenuto. Vi può essere utile il dump SQL con i comandi per la creazione delle tabelle dell'applicazione Mancolista. Nel seguito supporrò che le tabelle siano caricate in uno schema mancolista, in cui l'utente gamato ha accesso in lettura e scrittura. Modificate opportunamente i valori in base alla vostra configurazione.

Accesso a MySQL e la classe PDO

La libreria PDO mette a disposizione una classe, anch'essa denominata PDO, che serve a collegarsi a un server MySQL e ad eseguire operazioni sugli schemi e le tabelle ivi presenti. Per poter fare qualunque cosa su un DBMS, occorre prima di tutto creare un oggetto di classe PDO. La gestione degli oggetti di PHP e simile al Java, e per creare gli oggetti si utilizza l'operatore new.

Il costruttore della classe PDO ha la seguente sintassi:

Una volta creato un oggetto, ovvero instaurata la connessione con MySQL, si possono invocare i metodi della classe PDO. Il metodo che interessa adesso a noi è quello che consente di inviare query a MySQL: Una volta ottenuto un oggetto della classe PDOStatement, si può accedere al risultato della query invocando ripetutamente il metodo fetch di quest'ultimo: Ora, consideriamo la pagina elenco.php, che visualizza nome e descrizione del primo oggetto presente nel database mancolista:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Elenco oggetti Mancolista di Paolo</title>
</head>
<body>
<?php
$conn=new PDO("mysql:host=localhost;dbname=mancolista","amato","?????");
$result=$conn->query("SELECT * FROM oggetti");
$riga=$result->fetch();
echo $riga["nome"]," : ",$riga["descrizione"],"<br>";
?>
</body>
</html>
Lo script inizia con la connessione al server mysql su localhost come utente gamato usando la password ?????. A questi dati fittizi dovrete sostituire quelli del vostro account. Nello stesso costruttore si indica che siamo interesssati ad accedere al database mancolista. Successivamente viene inviata la query SELECT * FROM voli, e il risultato viene messo nella variabile $result. Da lì viene estratta una riga e inserita nella variabile $riga, per poi essere stampata.

Nel caso dell'aula informatica, dovrete rimpiazzare localhost con goemon, mancolista e gamato con il vostro numero di matricola, e ????? con la password del vostro account MySQL. Ovviamente sarà necessario che carichiate le tabelle per l'applicazione Mancolista nel vostro schema personale.

Se potessimo accedere soltanto alla prima riga di una tabella avremmo dei seri problemi a scrivere un programma decente. Tuttavia, come abbiamo già detto, ogni volta che viene chiamato il metodo fetch della classe PDOStatement, viene estratta un riga divera. Possiamo allora modificare il programma in modo che visualizzi tutti gli elementi della tabella oggetti (in rosso la parte modificata):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Elenco oggetti Mancolista di Paolo</title>
</head>
<body>
<?php
$conn=new PDO("mysql:host=localhost;dbname=mancolista","gamato","?????");
$result=$conn->query("SELECT * FROM oggetti");
while ($riga=$result->fetch())
  echo $riga["nome"]," : ",$riga["descrizione"],"<br>";
?>
</body>
</html>

Finché ci sono righe da esaminare, il risultato di $result->fetch() è un array che viene assegnato alla variabile $riga. Dopo l'assegnamento, visto che ci troviamo nella condizione del comando while, il valore viene automaticamente convertito in booleano. Qualunqe array non vuoto viene convertito nel booleano true, quindi la condizione è verificata e il corpo del ciclo while viene eseguito. Quando non ci sono più righe da esaminare, il risultato di $result->fetch() è false. Questo viene assegnato alla variabile $riga e causa la terminazione del ciclo while.

I valori delle chiavi per $riga (ovvero nome e descrizione) sono gli stessi che si ottengono come intestazione delle colonne quando la query select * from voli è inviata dal monitor di MySQL. Se si modifica la linea
$result=$conn->query("SELECT * FROM oggetti");
con
$result=$conn->query("SELECT nome as n, descrizione FROM oggetti");
occorre anche modificare l'echo finale in
echo $riga["n"]," : ",$riga["descrizione"],"<br>";
altrimenti viene generato un errore, che ci avverte che stiamo tentando di accedere alla chiave nome che non esiste.

Schema generale

Quello che abbiamo visto è un schema generale che si incontra ripetutamente nelle applicazioni web che si interfacciano a un database: connessione, interrogazione, elaborazione dei risultati, chiusura connessione. Ecco un breve riassunto:

$conn=new PDO("mysql:host=.....;dbname=.....",".....",".....");
$result=$conn->query("......query.....");
while ($riga=$result->fetch())
  .... corpo del while .....

Notare che il PHP consente una scorciatoia. Si può usare foreach per accedere alle righe di risultato invece del comando while con il metodo fetch. In pratica, si può rimpiazzare:

while ($riga=$result->fetch())
  .... corpo del while .....
con
foreach ($result as $riga)
   .... corpo del while ....

Qualora non interessi accedere a tutte le righe del risultato, ma solo alla prima, il while e il foreach, sono inutili, e si può rimpiazzare

while ($riga=$result->fetch())
  .... corpo del while .....

con

$riga=$result->fetch()
..... elaborazione su $riga .......

Interrogazioni multiple

Talvolta nella stessa pagina web occorre effettuare più di una interrogazione. Ci sono allora due possibilità:

La seconda soluzione è sicuramente più efficiente, ma richiede un po' di attenzione. Quando viene inviata una interrogazione al server MySQL, a seconda delle versioni della libreria PDO, potrebbe non essere più possibile inviare una nuova interrogazione se la prima non viene prima dichiarata conclusa. Per far ciò, la cosa più semplice è porre a null la variabile che contiene l'oggetto PDOStatement risultato dalla query. Ad esempio:

$conn=new PDO("mysql:host=.....;dbname=.....",".....",".....");
$result=$conn->query("......query 1.....");
... elaborazione query 1.....
$result=null;
$result=$conn->query("......query 2.....");
... elaborazione query 2.....

Gestione degli errori

Cosa succede se i dati che si forniscono ai metodi e costruttori della libreria PDO sono errati? Vedremo che il comportamento di PHP cambia a seconda che il problema si verifichi nel costruttore della classe PDO (ovvero durante la connessione) o negli altri metodi (quando la connessione è stata ormai stabilita).

Errori durante la connessione

Cosa succede se la connessione a MySQL per qualche motivo non ha successo? Ad esempio, modifichiamo l'invocazione del costruttore della classe PDO sostituendo il nome del database mancolista con rancolista (che non esiste). Viene generato un errore simile a questo:

Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42000] [1049] Unknown database 'mancoldsta'' in /home/amato/public_html/prova-manco1.php:8 Stack trace: #0 /home/amato/public_html/prova-manco1.php(8): PDO->__construct('mysql:host=loca...', 'amato', '') #1 {main} thrown in /home/amato/public_html/prova-manco1.php on line 8

Il messaggio è un po' confuso, ma se lo si formatta all'interno di un tag pre appare più chiaro:

Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[42000] [1049] Unknown database 'mancoldsta'' in /home/amato/public_html/prova-manco1.php:8
Stack trace:
#0 /home/amato/public_html/prova-manco1.php(8): PDO->__construct('mysql:host=loca...', 'amato', '')
#1 {main}
  thrown in /home/amato/public_html/prova-manco1.php on line 8

In pratica viene identificato l'errore (Unknown database 'mancoldsta') e la sorgente dell'errore (riga 8 di /home/amato/public_html/prova-manco1.php). Segue uno stack trace che è un elenco, in ordine inverso, delle funzioni e dei metodi che sono stati invocati e non sono ancora terminati. Questo stack trace ci dice che l'errore si è verificato a riga 8 di prova-manco1.php, che a sua volta è stata eseguita direttamente dal programma principale ({main}).

In realtà quello che è successo è che il problema verificatosi durante la connessione ha generato una eccezione di classe PDOException. Le eccezioni PHP sono simili alle eccezioni Java, con la differenza che non bisogna dichiarare in maniera particolare i metodi o le funzioni che lanciano eccezioni. Le eccezioni si possono catturare con le istruzioni try ... catch, ma se non vengono catturate causano la generazione di un errore fatale, e il termine immediato del programma. Per ora questo comportamento ci va bene, per cui non cattureremo l'eccezione. Quando però il software va in produzione (ovvero viene utilizzato dall'utente finale), è meglio ridefinire le eccezioni in modo che diano messaggi di errore meno informativi, e quindi meno utili agli hacker. Ad esempio, si potrebbe cambiare la riga

$conn=new PDO("mysql:host=localhost;dbname=mancolista","gamato","?????");
nel programma di prima con:
try {
  $conn=new PDO("mysql:host=localhost;dbname=mancolista","gamato","?????");
} catch (PDOException $e) {
  die("Errore di connessione");
}

dove la funzione die è simile ad echo, ma termina anche l'esecuzione del programma.

Errori successivi alla connessione

Il comportamento di default di PHP per errori che si verificano quando la connessione a MySQL è già stabilita è leggermente diverso. Ad esempio, se nel comando $conn->query() modifichiamo il nome della tabella oggetti in soggetti (che non esiste), otteniamo un errore simile a questo:

Fatal error: Call to a member function fetch() on a non-object in /home/amato/public_html/prova-manco1.php on line 10

L'errore non è stato segnalato sul comando $conn->query, dove effettivamente si è verificato, ma sul comando $result->fetch(). Quello che succede è che $conn->query fallisce in maniera silenziosa, restituendo il valore false. Quando si tenta di eseguire il comando $result->fetch(), il PHP si lamenta dicendo che $result non contiene un oggetto, come è necessario per poter accedere al metodo fetch.

Il problema è che questo messaggio non da nessuna informazione su quale sia l'errore originale nella query SQL che ha causato il problema. Durante la fase di scrittura del codice è utile che i messaggi di errori siano il più possibile chiari.

Per ovviare a questi problemi ci sono varie strade, ma quella che mi sembra più conveniente è far sì che anche gli errori successivi alla connessione, quando si verificano, generi delle eccezioni. Per far ciò, subito dopo la creazione dell'oggetto di classe PDO, si può dare il comando:

$conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

Adesso, se riproviamo il programma di primo (con l'errore sulla query) otteniamo:

Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[42S02]: Base table or view not found: 1146 Table 'mancolista.oggedtti' doesn't exist' in /home/amato/public_html/prova-manco1.php:10
Stack trace:
#0 /home/amato/public_html/prova-manco1.php(10): PDO->query('SELECT * FROM o...')
#1 {main}
  thrown in /home/amato/public_html/prova-manco1.php on line 10

Questa volta l'errore PHP viene generato proprio sulla riga incriminata, e inoltre ci viene anche fornito il messaggio di errore SQL Table 'mancolista.oggedtti' doesn't exist' che ci fa capire qual è il problema.

Volendo, è poi possibile gestire in proprio queste eccezioni, come fatto sopra per quelle verificatesi durante la connessione.

Parametri di configurazione

Ogni volta che utilizziamo la classe PDO ci tocca fornire una serie di parametri più o meno fissi: il nome del server MySQL, la username, la password e lo schema da utilizzare. Vorremmo evitare un inutile e dannosa duplicazione di dati. Allo stato attuale, infatti, se cambia la password di accesso al database, siamo costretti a modificare tutte le invocazioni del costruttore PDO in tutte le pagine web.

La cosa migliore è definire delle costanti con il comando define, da utilizzare al posto dei dati vero. Questa è la sintassi del comando define:

Ad esempio, se do il comando

define('DSN','mysql:hostname=localhost;dbname=mancolista');

successivamente posso usare la costante DSN (senza virgolette e senza dollaro iniziale) al posto della stringa mysql:hostname=localhost;dbname=mancolista', come in:
$conn = new PDO(DSN,"gamato",'?????')

La cosa sarebbe poco utile se dovessimo ripetere la definizione della costante in ogni pagina web. Per fortuna il PHP ha due comandi che ci consentono, all'interno di una pagina, di leggere il contenuto di un altro file e di interpretarlo come se si trovasse all'interno della pagina stessa:

Notare che quando il file incluso viene letto il sistema rientra in "modalità HTML". Per cui, se esso contiene codice PHP, è importante inserire i tag <?php e ?>. Definiamo allora una costante per ognuno dei parametri della connessione, e mettiamo le definizioni in un file che chiamiamo config.php. Ecco il suo contenuto:

<?php
define('DSN','mysql:hostname=localhost;dbname=mancolista');
define('USERNAME','gamato');
define('PASSWORD','?????');
?>

Alla fine, il nostro programma diventa:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Elenco oggetti Mancolista di Paolo</title>
</head>
<body>
<?php
require 'config.php';
$conn=new PDO(DSN,USERNAME,PASSWORD);
$conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$result=$conn->query("SELECT * FROM oggetti");
while ($riga=$result->fetch())
  echo $riga["nome"]," : ",$riga["descrizione"],"<br>";
?>
</body>
</html>

Esercizi e Soluzioni

Esercizio 1

Salvare l'ultimo esempio di pagina PHP mostrata e il file config.php. Accertarsi che tutto funzioni. Una volta che è tutto a posto, provare a modificarla inserendo degli errori, ed esaminare i risultati che si ottengono.

Esercizio 2

Scrivere una pagina PHP che mostri gli oggetti presenti nel database, indicando per ogni oggetto se è o non è già prenotato.

Esercizio 3

Scrivere una pagina PHP che mostri gli oggetti divisi per categoria, come richiesto dalla specifica del progetto mancolista.

Lezione Precedente Elenco Lezioni Lezione Successiva

Valid HTML 4.01 Transitional Valid CSS!