Guida completa a PDO

Giovanni Canella

06/08/2013

17886

PDO (PHP Data Object), è un estensione per l'accesso e gestione dei database, introdotta nella versione 5.1 di PHP, con lo scopo di creare un unica interfaccia che andasse bene per la maggior parte dei database in circolazione.

Infatti prima del suo avvento, i programmatori hanno sempre utilizzato le funzioni dedicate solamente a quel tipo di database, per esempio se si utilizzava MySQL, si faceva affidamento a mysql_*/mysqli_*, oppure per PostgreSQL, pg_*. Quindi è ben facile capire che se bisognava cambiare per qualsiasi motivo hosting, o piattaforma, con una equipaggiata con SQLite o simili, bisognava riscrivere completamente la parte delle query nella nostra applicazione.

Modifica tabelle database

Questo caso non si presenta con PDO, che come dice la documentazione ufficiale ci mette a disposizione un data abstraction layer, ovvero un livello di astrazione per l'accesso ai dati, cioè che indipendentemente dal database in uso puoi usare le stesse funzioni per eseguire query e ottenere dati. Questo perchè vengono chiamate solo le sotto-classi inerenti.

Inoltre supporta una vasta gamma di database: ecco una lista completa:

  • Cubrid,
  • FreeTDS,
  • Microsoft SQL Server,
  • Sybase,
  • Firebird,
  • Interbase 6,
  • IBM DB2,
  • IBM Informix Dynamic Server,
  • MySQL 3.x/4.x/5.x,
  • Oracle Call Interface,
  • ODBC v3,
  • PostgreSQL,
  • SQLite v2/v3,
  • Microsoft SQL Server,
  • SQL Azure,
  • 4D.

PDO, oltre ai vantaggi sopra elencati, è già predisposto per la programmazione OOP (Object Oriented Programming), mancante nelle vecchie funzioni, e come se non bastasse è notevolmente più sicuro, grazie ai prepared statement, o in italiano dichiarazioni precompilate, dove non c'è alcun pericolo di incombere in alcun tipo di SQL Injection, ma questo discorso lo affronterò nelle prossime pagine.

Con le vecchie funzioni mysql_*, per connettersi a un database la procedura bene o male era la seguente:

$connessione = mysql_connect(localhost, "username", "password", "database");
     
if (mysql_connect_error($connessione)) {
     die("Errore nella connessione al database!");
}

Con PDO il procedimento resta praticamente lo stesso: per prima cosa si inizializza l'istanza della classe PDO, passando 3 parametri obbliatori nel costruttore:

  1. Stringa di connessione contenente: host su cui è installato MySQL, nome del database,
  2. L'username del database,
  3. La password relativa all'utente.

Se non ci sono errori, la nostra applicazione si connetterà correttamente, altrimenti grazie al costrutto try / catch, stamperemo a video un messaggio di errore.

try {
      $connessione = new PDO(mysql:host="localhost";dbname="nome_database", "username", "password");
} catch(PDOException $e) {
      die("Errore durante la connessione al database!: " '. $e->getMessage());
}

Inoltre possiamo anche specificare parametri opzionali, attraverso la funzione. setAttribute(). Qui di seguito elenco quelli che secondo me sono quelli che ci possono risultare più utili:

Per la gestione degli errori ci sono tre opzioni da attribuire al parametro PDO::ATTR_ERRMODE:

  1. PDO::ERRMODE_SILENT, la modalità di default, dove non vengono mostrati messaggi di errori, ma in modo non visibile vengono impostati, nel caso ci fossero, il codice errore e il relativo messaggio, e accessibili rispettivamente tramite i seguenti metodi: errorCode(), errorInfo().
  2. PDO::ERRMODE_WARNING, in aggiunta alla prima modalità, imposta anche un messaggio di "attenzione", utile durante il testing dell'applicazione, che una volta mostrato non blocca il normale flusso del programma.
  3. PDO::ERRMODE_EXCEPTION, la modalità più "aggressiva" che in aggiunta a quelle precendenti blocca l'esecuzione del programma mostrando automaticamente a video un messaggio di errore. Inoltre annulla anche tutte le transazioni, che vedremo nelle prossime pagine. Questa modalità è consigliata in ambito di debug, dato che mostra un elenco dettagliato di dove si trova l'errore, perchè è stato mostrato ecc, ma non per altro.

es. 

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

Per la gestione del fetch, si attribuisce al parametro PDO::ATTR_DEFAULT_FETCH_MODE, le seguenti opzioni:

  1. PDO::FETCH_BOTH, (default) restituisce un array con indice (a partire da 0) sia il nome della colonna che il numero della colonna
    array(
        [Nome_Articolo] => "Prova", 
        [0] => "Prova"
    ) 
    ,
  2. PDO::FETCH_ASSOC, restituisce un array con indice quello del nome della colonna
    array(
        [Nome_Articolo] => "Prova"
    )

     

  3. PDO::FETCH_NUM, restituisce un array con indice del numero della colonna, a partire dalla colonna 0
    array(
        [0] => "Prova"
    )

     

  4. PDO::FETCH_OBJ, restituisce un oggetto anonimo avente come nome il nome della colonna.
    stdClass Object (
        [Nome_Articolo] => "Prova"
    )

     

Per le impostazioni generali:

  1. PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, (disponibile solo per MySQL), se impostato su TRUE, permette di eseguire più query alla volta, se su FALSE, il contrario.
  2.  PDO::ATTR_TIMEOUT, imposta il tempo limite per le query (espresso in secondi). 

Maggiori opzioni nella documentazione ufficiale.

In PDO ci sono diversi metodi per eseguire una query, e ognuno ha un suo specifico scopo, ma vediamoli in dettaglio:

query()

Questo è il metodo più semplice per effettuare una query, ma bisogna fare attenzione: va utilizzato solo "internamente" con dati di cui sappiamo la provenienza e la loro affidabilità, dato che saremo vulnerabili ad eventuali attacchi SQL Injection.

foreach($connessione->query("SELECT * FROM Prodotti WHERE Prodotto_ID = 1 LIMIT 1") as $riga) {
       echo $riga["Nome"];
       echo $riga["Prezzo"];
}

Se si vuole fare l'escape dei dati inseriti, PDO mette a disposizione il metodo quote(), analogo al vecchio mysql_real_escape_string. L'output di questa stringa:

$connessione->quote("Sono una 'stringa' con 'apici singoli'");

è:

'Sono una 'stringa' con 'apici singoli''

prepare()

Tramite questo metodo possiamo effettuare i prepared statement, e effettuare query in modo sicuro al riparo da tutte le minacce esterne, comprese SQL Injection, dato che i dati vengono trattati in modo diverso: infatti nelle normali query, il "corpo" e i dati vengono uniti e inviati al server, e si ha la possibilità di alterare la struttura del codice, mentre con questo nuovo metodo, prima viene inviata la query grezza con i placeholder, ovvero variabili che andranno poi sostituite con quelle da noi desiderate, successivamente vengono inviati i dati. In questo modo risulta impossibile da parte di un malintenzionato, alterare il corpo della query.

es. (senza prepared statement), la seguente query:

SELECT * FROM Utenti_Sito WHERE Utente_ID = $ID

può essere alterata in questo modo, se come id immettiamo: 1; DROP TABLE Utenti_Sito;

SELECT * FROM Utenti_Sito WHERE Utente_ID = 1; DROP TABLE Utenti_Sito;

il che comporterebbe l'eliminazione dell'INTERA tabella!

Spiegato ciò vediamo come eseguire un prepared statement:

$result = $connessione->prepare("SELECT * FROM Prodotti WHERE id = :id LIMIT 1");

$result->execute(array(
	":id" => $id
));

echo "Prova prepare()"

while($riga = $result->fetch()) { 
      echo "Nome prodotto: " . $riga["nome"];
      echo "Prezzo: " . $riga["prezzo"] . "€";
}

Prova prepare()

Possiamo suddivere il seguente codice in 3 parti:

  1. Inviamo al server la query grezza,
  2. Inviamo i dati relativi a quest'ultima,
  3. Tramite un ciclo while, estraiamo i dati.

exec()

Questa funzione è simile a query(), dato che esegue una query in una sola chiamata, ma invece di restituire i valori di quest'ultima, restituisce il numero di righe coinvolte. Quindi non va utilizzato con il comando SELECT, dato che non restituirebbe nulla, bensì con UPDATE e DELETE, per esempio per vedere quanti elementi sono stati eliminati. Inoltre vale lo stesso discorso fatto per query(), ovvero che non c'è nessun meccanismo contro eventuali attacchi, quindi si consiglia sempre di utilizzare dati sicuri.

es.

$numero = $connessione->exec("DELETE FROM Prodotti");
			
echo "Sono stati elimati " . $numero . " prodotti ";

Tutte queste funzioni restituiscono un oggetto PDOStatement, con cui possiamo accedere ai seguenti metodi:

  • PDOStatement::closeCursor, chiude la connessione al database in modo che subito dopo si possano eseguire altre query. Questa funzione è utile per evitare di modificare la proprietà vista a pagina 1, PDO::MYSQL_ATTR_USE_BUFFERED_QUERY (ripeto solo disponibile per MySQL), semplicemente anteponendola prima di un'altra istruzione. es. 
    $prima_query = $connessione->prepare("SELECT Nome FROM Prodotti");
    
    $seconda_query = $connessione->prepare('SELECT Prezzo FROM Prodotti');
    
    $prima_query->execute();
    $prima_query->fetch();
    
    // Chiudi la connessione in modo da eseguire la seconda query
    $stmt->closeCursor();
    
    $seconda_query->execute();

    In pratica abbiamo inviato due query separate al database ($prima_query, $seconda_query), e succcessivamente eseguito e ottenuto il valore della prima. Ora grazie a closeCursor(), chiudiamo la connessione e eseguiamo la seonda query, messa in "attesa". Altrimenti ci sarebbe mostrato il seguente messaggio d'errore (sempre in base alle impostazioni sulla gestione degli errori). 

    Cannot execute queries while unbuffered queries are active
     

  • PDOStatement::setFetchMode, imposta la modalità di fetch. Svolge la stessa funzione di PDO::ATTR_DEFAULT_FETCH_MODE, con l'unica differenza che lo fa "al volo".
  • PDOStatement::rowCount, restituisce il numero di righe coinvolte durante una query, con i comandi UPDATE, DELETE, INSERT, e con non tutti i database anche SELECT.
  • PDOStatement::debugDumpParams, mostra un array con le informazioni contenute nell'ultimo prepared statement, compresi i valori che abbiamo assegnato ai placeholder. Ecco un esempio su come è strutturato:

    debugDumpParams()
     
  • PDOStatement::fetchAll, Svolge la stessa funzione di fetch(), però a differenza di quest'ultimo che elabora un risultato alla volta, fetchAll() restituisce un array con indice 0, contenente tutti i dati della nostra query. es: 
    $risultati = $connessione->fetchAll("SELECT * FROM Prodotti")

    produrrà: 

    Array (
        [0] => Array(
                [id] => 1
                [nome] => Pesce
                [prezzo] => 5
                [sconto] => 0
            )
    
        [1] => Array (
                [id] => 2
                [nome] => Carne
                [prezzo] => 20
                [sconto] => 10
            )
    )

    Per estrarre i risultati, basta usare un ciclo foreach:

    foreach ($risultati as $risultato) {
        echo $risultato["id"] . "<br>";
        echo $risultato["nome"] . "<br>";
        echo $risultato["prezzo"] . "<br>";
    }

    Ora che abbiamo visto queste due funzioni:  Quale devo scegliere tra fetch() e fetchAll()?? La risposta è semplice: dipende dai nostri "gusti", infatti entrambi svolgono la stessa funzione, e sta a noi decidere quale metodo adottare. Da un lato prestazionale, fetchAll è più veloce ma consuma più memoria rispetto a fetch().

  • PDOStatement::errorCode(), restituisce il codice di errore della query eseguita. (nella documentazione ufficiale, un elenco di tutti gli errori per i database MySQL).

  • PDOStatement::errorInfo(), restituisce un array di 3 elementi, con indice 0:
    Indice Contenuto
    0 Errore SQL di 5 caratteri 
    1 Codice di errore specifico database
    2 Errore specifico database

    es. 
    Array (
        [0] => 42S02
        [1] => -204
        [2] => [IBM][CLI Driver][DB2/LINUX] SQL0204N  "DANIELS.BONES" is an undefined name.  SQLSTATE=42704
    )

     

Dopo aver visto le basi di PDO, passiamo alle funzionalità un po' più avanzate, una di queste sono le transazioni, che permettono di tornare indietro e annullare l'ultima query SQL eseguita. Normalmente questo non sarebbe possibile, perchè il database regola le chiamate tramite il meccanismo definito auto-commit, ovvero che una query viene eseguita automaticamente senza possibilità di annularla.

Tramite il metodo beginTransaction(), possiamo disabilitare temporaneamente questo meccanismo e poter di conseguenza annullare la query, con rollBack(). Vediamo un esempio:

// Disabilito l'auto-commit
$connessione->beginTransaction();

try {
      $elimina = $connessione->exec("UPDATE Prodotti SET nome = 'Tavolo' WHERE id = 1");
      $connessione->commit();
} catch(Exception $e) {
      // Riabilito l'auto-commit
      $connessione->rollBack();
      die("Errore imprevisto " . $e->getMessage());
}

Come prima cosa disabilitiamo l'auto-commit, e dentro un blocco try / catch, lanciamo la query con exec(), e se non ci sono errori con commit(), applichiamo le modifiche e disabilitiamo l'auto-commit, altrimenti mostriamo un messaggio di errore e in ogni caso lo disabilitiamo.

Volessimo riabilitarlo basta impartire nuovamente:

$connessione->beginTransaction();

Questo è un tipo di utilizzo, ma non è l'unico: infatti potremmo utilizzare per esempio questa funzione per annullare le modifiche dopo l'eliminazione della tabella se per sbaglio abbiamo cliccato su un eventuale pulsante elimina da noi creato. 

 

Conclusioni

ConsigliatoIn conclusione dopo aver visto le caratteristiche principali di PDO, possiamo dire che è milgiore sotto tutti i punti di vista rispetto alle vecchie funzioni "proprietarie", soprattutto per quanto riguarda la sicurezza, infatti grazie ai prepared statement (per evitare SQL Injection), e alle transazioni (per assicurarsi l'integrità dei dati).

I più letti

Ti potrebbero interessare