Tutorials

Data Access Object in PHP, tecnica di astrazione del database


(Il codice è compatibile con PHP4)

Articoli da cui ho preso ispirazione e che costituiscono in ogni caso una lettura interessante:

Limiti (importanti): query complesse (es. con join fra più tabelle); tabelle con chiavi primarie composte.

L'articolo tratterà i seguenti argomenti:

Mappatura tabelle-classi: creazione dei ValueObjects (o Entità)

Inizio l'articolo con la creazione di quelle classi PHP che rappresentano una tupla di una tabella del DB.
Queste classi sono molto semplici e rispecchiano esattamente la struttura delle tabelle:

Per ogni tabella esistente, si crea una classe PHP con lo stesso nome della tabella (non necessariamente il nome deve essere uguale, ma è utile per mantenere coerenza nel codice).
Ogni classe deve contenere una variabile per ciascun campo della tabella, ed è importante che le variabili abbiano lo stesso nome del campo. Le classi non devono contenere nient'altro che queste variabili, e non ci deve essere nessun costruttore e nessun valore di default.
Se per esempio volessimo mappare una tabella 'Persona' con i suoi campi "id, nome, cognome, telefono" dovremmo creare una classe fatta così:

class Persona { var $id, $nome, $cognome, $telefono; }

Ciascuna istanza di questa classe è un entità che potrà essere resa "persistente" salvandola nella base dati, oppure potrà essere stata creata recuperando i dati dal database, ed in questo caso essa rappresenterà esattamente una tupla della tabella.


Descrizione dei DAO (Data Access Object)

A fianco delle entità (o Value Objects), dobbiamo creare gli oggetti che comunicano con il database (i DAO), e che hanno le informazioni necessarie al loro funzionamento.
Le informazioni necessarie sono:

In pratica con un DAO metto in relazione un'entità (ad es. la classe Persona) con una tabella presente nel database, aggiungendo altre informazioni come la chiave primaria.
Vediamo com'è fatto il DAO per la tabella Persona:

class DAOPersona extends DAODefault{ function DAOPersona(&$con){ // nome della tabella nel database $this->tablename = 'persona'; // chiave primaria della tabella $this->id = 'id'; // entity che mappa la tabella 'persona' $this->vo = new Persona(); } }

Come si vede nel codice questa classe estende un DAODefault comune ai DAO di tutte le tabelle.
La classe 'DAODefault' è la classe più complicata e la più interessante, e per come l'ho scritta ci consente di aggiungere nuove entity e DAO in maniera molto semplice, senza dover scrivere mai una riga di SQL per ogni tabella in più che si vuole mappare.

Vediamo quindi come è fatta la classe DAODefault:
Per prima cosa vogliamo creare i metodi di base per la comunicazione con il database attraverso le query SQL, le funzioni da implementare sono:

Il problema principale è quello che tutti i metodi devono essere generali, non devono riferirsi a nessuna tabella in particolare, e non possono conoscere a priori tutti i campi da modificare.
Iniziamo implementando il metodo get:

function get($id){ $query = "SELECT * FROM $this->tablename WHERE $this->id=" . mysql_escape_string($id); $result = mysql_query($query); $this->getFromResult($this->vo, $result); return $this->vo; }

Si vede subito che il problema del nome della tabella viene risolto usando la variabile $tablename, che viene inizializzata nel DAO specifico per ciascuna tabella che estenderà questa classe DAODefault.
Stessa cosa avviene per la chiave primaria ($id).

La parte interessante è il metodo getFromResult(), che ha lo scopo di riempire l'entity contenuta nel DAO con i dati recuperati dalla query:

function getFromResult(&$vo, $result) { $row = mysql_fetch_assoc($result); if(!empty($row)){ while (list($chiave,$valore) = each($row)){ $vo->$chiave = $valore; } } }

Ma come fa a riempire l'entity ($vo) con i risultati del database senza sapere nulla sui campi della tabella?
Poichè l'entity rispecchia ESATTAMENTE la struttura della tabella, sarà sufficiente recuperare i nomi dei campi dal risultato della query, quindi salvare nelle variabili di vo ($vo->$chiave) che hanno gli stessi nomi dei campi, i valori del risultato.

Passiamo al secondo metodo: insert($object).

function insert(&$vo) { $properties = get_object_vars($vo); $setfields = ''; while (list($chiave, $valore) = each($properties)){ if ($valore != '') $setfields = $setfields . " $chiave = '" . mysql_escape_string($valore) . "',"; } $setfields = substr($setfields,0,-1); // elimina l'ultima virgola $query = "INSERT INTO $this->tablename SET " . $setfields; mysql_query($query); $vo->pkey = mysql_insert_id(); }

In questo caso l'informazione sui campi presenti nella tabella da aggiornare viene presa dall'entity e dalle sue variabili.
L'elenco dei nomi delle variabili dell'entità da inserire nella tabella, ci consente di costruire la stringa SQL per aggiornare la tabella.

Vediamo il metodo update($object).

function update(&$vo) { $properties = get_object_vars($vo); $setfields = ''; while (list($chiave, $valore) = each($properties)){ if ($valore != '') $setfields = $setfields . " $chiave = '" . mysql_escape_string($valore) . "',"; } $setfields = substr($setfields,0,-1); // elimina l'ultima virgola $query = "UPDATE $this->tablename SET " . $setfields . " WHERE pkey=" . mysql_escape_string($vo->id); mysql_query($query); }

Anche qui il nome dei campi da aggiornare vengono presi dai nomi dalle variabili della entity ($vo).

Ed infine il metodo delete($object).

function delete(&$vo) { $query = "DELETE FROM $this->tablename WHERE $this->id=" . mysql_escape_string($vo->id); mysql_query($query); $vo->pkey = 0; }

In questo caso la funzione è molto semplice e si limita a recuperare la chiave primaria dalla entità per eseguire un DELETE.
Con questo abbiamo concluso i metodi fondamentali.

Possiamo scrivere ora dei metodi per semplificare l'utilizzo del DAO, cominciando dal metodo "save($object)":

function save(&$vo){ if ($vo->pkey == 0){ $this->insert($vo); } else { $this->update($vo); } }

Questo semplice metodo ci consente di usare una sola funzione sia per inserire una nuova entità nel DB che per aggiornarne una già esistente

Ora dobbiamo creare un metodo che ci consenta di fare una ricerca che preveda la restituzione di più di una riga della tabella.
Per questo scopo è stato creato il metodo find($object).
Il parametro è un'entità con alcune delle sue variabili settate. I valori di queste variabili costituiscono il criterio di ricerca.

function find($vo){ // This array will hold the where clause $where = array(); $properties = get_object_vars($vo); if (isset($this->operator)) $operator = $this->operator; else $operator = '='; while (list($chiave,$valore) = each($properties)){ if ($valore != '') { $where[] = " $chiave $operator'" . mysql_escape_string($valore) . "'"; } } $sql = $this->formatquery($where); $rs = mysql_query($sql); return new ReadOnlyResultSet($rs,$this->vo); } function formatquery($where){ $sql = "SELECT * FROM $this->tablename"; // If we have a where clause, build it if (count($where) > 0){ $sql .= " WHERE " . implode(' AND ', $where); } if (isset($this->orderby)) $sql = $sql . " ORDER BY $this->orderby"; if (isset($this->limit)) $sql = $sql . " LIMIT $this->limit"; return $sql; }

Per consentire una ricerca non soltanto per valori uguali (es. WHERE nome='john'), ho creato nel DAO la variabile $operator, che di default è settata a '=' ma che può essere impostata a piacimento, ad esempio nel caso si voglia trovare tutte le persone con più di 40 anni basterà impostare un ipotetica variabile eta dell'entità a 40, e la variabile $operator = '>='.
Ho previsto anche la possibilità di dare un ordine ai risultati settando la variabile $orderby (es. $orderby='eta').

Per comodità è possibile creare un metodo stile "factory" per centralizzare la creazione dei DAO e per gestire le connessioni al DB:

function dao_getDAO($vo_class) { $conn = db_conn(); #get a connection from the pool switch ($vo_class) { case "persona": return new DAOPersona($conn); break; case "foo": return new DAOFoo($conn); break; case "bar": return new DAOBar($conn); break; } }


Creazione dell'oggetto ReadOnlyResultSet

Una volta creata ed eseguita la query SQL nel metodo find, vogliamo poter gestire il risultato in maniera comoda, per questo viene creata la classe "ReadOnlyResultSet" con i metodi per navigare tra i risultati.

Con un semplice ciclo "while($resultSet->hasNext())" è possibile ottenere un'entità per ogni riga del risultato ($entity = $resultSet->next()).
E' inoltre possibile resettare il contatore o indicare l'i-esima riga (offset) da cui prelevare le entità.

class ReadOnlyResultSet { var $rs; var $vo; var $fetched; function ReadOnlyResultSet($rs,$vo){ $this->rs = $rs; $this->vo = $vo; $this->fetched = 0; } function reset(){ if( mysql_num_rows($this->rs) != 0) mysql_data_seek($this->rs, 0); // se rs vuoto, da WARNING $this->fetched = 0; } function setOffset($offset){ if( mysql_num_rows($this->rs) != 0) mysql_data_seek($this->rs, $offset); // se rs vuoto, da WARNING $this->fetched = $offset; } function rowCount(){ return mysql_num_rows($this->rs); } function next(){ $this->fetched += 1; $row = mysql_fetch_array($this->rs); if ($row != false){ $properties = get_object_vars($this->vo); while (list($chiave,$valore) = each($properties)){ $where[] = " $chiave ='" . mysql_escape_string($valore) . "'"; $vo->$chiave = $row[$chiave]; } return $vo; } else { return false; } } function hasNext(){ if (mysql_num_rows($this->rs) > $this->fetched){ return true; } else { return false; } } }


Implementazione della Business Logic

Fin'ora abbiamo creato uno strato software per astrarre la struttura del database, ora vogliamo creare i metodi utili alla nostra applicazione.
Creaiamo quindi una classe che contenga dei metodi per recuperare le informazioni che ci interessano.
Alcune funzioni possono sembrare ridondanti ma se vogliamo che la parte di Visualizzazione rimanga completamente isolata dalla parte di Modello (pattern MVC), queste funzioni risulteranno comode e garantiranno una pulizia maggiore nel codice finale.

class PersonaLogic{ /* returns: Persona */ function getPersona($id) { $dao = dao_getDAO("persona"); return $dao->get($id); } function savePersona($persona){ $dao= dao_getDAO("persona"); $dao->save($persona); } function eliminaPersona($idPersona){ $obj = new Persona(); $obj->pkey = $idPersona; $dao = dao_getDAO("persona"); $dao->delete($obj); } /* returns: readonlyResultSet */ function getTuttePersone($offset = 0, $limit = 10) { $persona = new Persona(); $dao = dao_getDAO("persona"); $dao->orderby = "cognome DESC"; $dao->limit = $limit; $result = $dao->find($persona); $result->setOffset($offset); return $result; } /* returns: readonlyResultSet */ function getPersonePensionabili($eta = 60) { $persona = new Persona(); $persona->eta = $eta; $dao = dao_getDAO("persona"); $dao->orderby = "eta ASC"; $dao->operator = ">="; $dao->limit = 20; $result = $dao->find($persona); return $result; } }


Esempio di utilizzo

Vediamo come si presenta il codice di una pagina php che utilizza quanto abbiamo creato fin'ora.
In questa pagina vogliamo elencare tutte le persone che hanno un età maggiore di 45 anni.

<?php $logic = new PersonaLogic(); /* recupera dati dinamici dal DB */ $listaPersone = $logic->getPersonePensionabili(45); ?> <html> <body> Elenco Delle persone con più di 45 anni: <?php while($listaPersone->hasNext()){ $persona = $listaPersone->next(); echo "Nome: $persona->nome, Cognome: $persona->cognome, Eta: $persona->eta <br>"; } ?> </body> </html>