10. Applicazioni a livelli
10.1. Introduction

Scopriremo ora come strutturare un’applicazione PHP a livelli:

In questo schema, è il livello a sinistra che prende l’iniziativa di utilizzare il livello a destra. Il ruolo dei livelli è il seguente:
- [1]: il livello denominato [dao] (Data Access Objects) si occupa degli scambi con i data warehouse esterni [4] (file, database, servizi web…). Questo livello è talvolta denominato [DAL] (Data Access Layer), un termine che descrive meglio la sua funzione. Questo livello può leggere dati [3] o scriverne [2]. Viene interpellato dal livello [métier] [6] e restituisce risultati [7];
- [5]: un livello denominato [métier] che raggruppa le procedure «di business» alle quali vengono forniti tutti i dati di cui hanno bisogno. Si tratta generalmente del livello più stabile di un progetto poiché non dipende dalle modalità di acquisizione dei dati. Questi ultimi provengono da due fonti:
- [9]: i dati forniti dallo script PHP;
- [6,7]: i dati richiesti al livello [dao].
- [8]: lo script principale funge da coordinatore. In un’applicazione da console, esso:
- crea i livelli [métier] e [dao];
- esegue l’algoritmo dell’applicazione. Questo algoritmo è quello di un «direttore d’orchestra»: non contiene alcun codice «di business» né di accesso ai dati. Lo script principale si limita a richiamare le procedure dei livelli [métier] e [9]. Ignora completamente il livello [dao] e i dati esterni. Può fornire al livello [métier] i dati [9]. In un’applicazione da console, questi possono provenire da file di configurazione o dall’utente dello script. Riceve risultati [10] dal livello [métier]. Potrebbe essere necessario memorizzare alcuni risultati: a tal fine utilizza nuovamente le procedure del livello [métier], che a loro volta si rivolgeranno al livello [dao], il quale eseguirà l’operazione;
- tra i risultati, lo script principale può ricevere delle eccezioni. È suo compito gestire le eccezioni che vengono segnalate da tutti i livelli;
Questa suddivisione in livelli ha lo scopo di facilitare l’evoluzione dell’applicazione. A tal fine, ogni livello implementa un’interfaccia. Supponiamo che:
- il livello [dao] sia implementato da una classe [Dao] che a sua volta implementa un'interfaccia [IDao];
- il livello [métier] sia implementato da una classe [Métier] che a sua volta implementa un'interfaccia [IMétier];
Le procedure del livello [métier] utilizzeranno quindi l'interfaccia [IDao] anziché la classe [Dao]. Ciò consente di aggiornare il livello [Dao] senza modificare il livello [Métier]. Supponiamo che nella versione 1 il livello [Dao1] utilizzi dati provenienti da un database. A seguito di un aggiornamento, nella versione 2 ([Dao2]) tali dati vengono ora forniti da un servizio web. Ci si assicurerà che le due classi [Dao1] e [Dao2] implementino la stessa interfaccia [IDao] e che generino le stesse eccezioni. Se ciò è verificato, il livello [Métier] che opera con l’interfaccia [IDao] rimarrà invariato;
Lo stesso ragionamento vale per il livello [Métier].
Vediamo un'implementazione con classi e interfacce:

L’applicazione implementerà la seguente struttura a livelli:

10.2. Oggetti scambiati tra i livelli
Normalmente, i livelli scambiano oggetti diversi. In questo caso scambieranno la seguente classe [Personne.php]:
<?php
// una persona
class Personne {
// identificativo
private $id;
// costruttore
public function __construct(int $id) {
$this->id = $id;
}
// toString
public function __toString(): string {
return "[Personne($this->id)]";
}
}
Commenti
- riga 6: la persona ha un solo attributo, il suo identificativo [$id]. Supponiamo che questo identifichi una persona unica in un data warehouse;
- righe 9-11: il costruttore che permette di creare un tipo [Personne] con il relativo identificativo;
- righe 14-16: il metodo [__toString] che visualizza l’identificativo della persona;
10.3. Livello [dao]
L’interfaccia [IDao] implementata dal livello [dao, 1] è la seguente [IDao.php]:
<?php
// livello [dao] ------------------------
interface IDao {
// prelievo di una persona da un magazzino esterno
// si comunica l'identificativo della persona
public function get(int $id): Personne;
// salvataggio di una persona in un magazzino esterno
public function save(Personne $p): void;
}
Questa interfaccia è implementata dalla seguente classe [Dao1] [Dao1.php]:
<?php
class Dao1 implements IDao {
// salvataggio di una persona in un magazzino esterno
public function save(Personne $p): void {
print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
}
// recupero di una persona da un magazzino esterno
public function get(int $id): Personne {
print "[Dao1] : Récupération de la personne d'identité ($id) en base de données [locale]\n";
return new Personne($id);
}
}
Commenti
- la classe non interagisce con un data warehouse. Ci si limita a visualizzare dei messaggi per seguire l’esecuzione del codice (righe 7 e 12);
- riga 13: viene restituita correttamente una persona con l’identificativo passato come parametro al metodo (riga 11);
Implementiamo inoltre l’interfaccia [IDao] con la seguente classe [Dao2] [Dao2.php]:
<?php
class Dao2 implements IDao {
// salvataggio di una persona in un archivio esterno
public function save(Personne $p): void {
print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
}
// recupero di una persona da un magazzino esterno
public function get(int $id): Personne {
print "[Dao2] : Récupération de la personne d'identité ($id) en base de données [distante]\n";
return new Personne($id);
}
}
La classe [Dao2] è simile alla classe [Dao1], tranne per il fatto che abbiamo modificato i messaggi visualizzati.
10.4. Livello [métier]
Il livello [métier, 2] offre la seguente interfaccia [IMétier] [IMetier.php]:
<?php
// livello [métier] ------------------------
interface IMétier extends IDao {
// si elabora una persona identificata dal proprio ID
public function doSomething(Personne $p): void;
}
Commenti
- L’interfaccia [IMétier] estende l’interfaccia [IDao]. Ciò non è affatto obbligatorio. Lo facciamo qui perché l’esempio è semplice;
- riga 7: il metodo [doSomething] è specifico del livello [Métier];
L'interfaccia [IMétier] sarà implementata dalla seguente classe [Métier] [Métier.php]:
<?php
class Métier implements IMétier {
// livello [dao]
private $dao;
// getter / setter
public function setDao(IDao $dao): void {
$this->dao = $dao;
}
public function getDao(): IDao {
return $this->dao;
}
// si gestisce una persona
public function doSomething(Personne $p): void {
// elaborazione
print "Métier : Traitement métier de la personne $p\n";
}
// salvataggio della persona
public function save(Personne $p): void {
print "[Métier] : Sauvegarde de la personne $p\n";
// si richiede al livello [dao] di eseguire il salvataggio
$this->dao->save($p);
}
public function get(int $id): Personne {
print "[Métier] : récupération de la personne d'identifiant $id\n";
// si richiede la persona al livello [dao]
$personne = $this->dao->get($id);
return $personne;
}
}
Commenti
- riga 5: il livello [métier] deve avere un riferimento al livello [dao] per poter utilizzare i metodi di quest'ultimo;
- righe 8-10: il metodo [setDao] consente di assegnare al livello [métier] il riferimento del livello [dao]. Si noti che il tipo del parametro è [IDao]. In questo modo, il livello [métier] sarà in grado di funzionare con qualsiasi classe che implementi l’interfaccia [IDao]. Se si passa da un livello [Dao1] a un livello [Dao2] ed entrambi implementano l’interfaccia [IDao], il livello [métier] non deve essere riscritto;
- righe 17-20: implementazione di [IMetier::doSomething];
- righe 23-27: implementazione di [IMetier::save]. Questo metodo deve salvare una persona in un data warehouse. Il livello [métier] non è in grado di farlo. Si rivolge al livello [dao] per eseguire questo salvataggio;
- righe 29-34: implementazione di [IMetier::get]. Questo metodo deve recuperare da un data warehouse il soggetto il cui identificativo gli viene passato come parametro. Il livello [métier] non è in grado di eseguire questa operazione. Si rivolge al livello [dao] affinché svolga il lavoro (riga 32);
Conclusione
Non appena il livello [métier] ha bisogno di accedere ai dati memorizzati nel data warehouse, deve passare attraverso il livello [dao], che è stato creato proprio per accedere a tali dati.
10.5. Script principale
Scriveremo due script, che fungeranno da coordinatori per questa applicazione. Il primo, [main1.php], utilizzerà il livello [Dao1], mentre il secondo, [main2.php], utilizzerà il livello [Dao2]. Vogliamo dimostrare che ciò non ha alcuna incidenza sul codice del livello [Métier].
Lo script [main1.php] è il seguente:
<?php
// rigoroso rispetto dei parametri delle funzioni
declare (strict_types=1);
// inclusione di classi e interfacce
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao1.php";
require_once __DIR__."/Métier.php";
// test ----------------
// creazione dei livelli
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// utilizzo del livello [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
Commenti
- ricordiamo alcuni punti: lo script [main1.php] è il «direttore d’orchestra» dell’applicazione. Crea la struttura a livelli dell’applicazione (righe 15-17) e poi inizia a interagire con il livello [métier] (righe 19-21). La struttura a livelli è infatti la seguente:

Secondo questo schema, lo script [main1.php] deve rivolgersi esclusivamente al livello [métier]. Non deve rivolgersi al livello [dao], anche se ciò è teoricamente possibile.
I risultati dell'esecuzione sono i seguenti:
[Métier] : récupération de la personne d'identifiant 4
[Dao1] : Récupération de la personne d'identité (4) en base de données [locale]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao1] : Sauvegarde de la personne [Personne(4)] en base de données [locale]
Commenti
- la riga 10 del codice ha provocato la scrittura delle righe 1 e 2 dei risultati;
- la riga 20 del codice ha provocato la scrittura della riga 3 dei risultati;
- la riga 21 del codice ha determinato la scrittura delle righe 4 e 5 dei risultati;
Lo script [main2.php] è il seguente:
<?php
// rigoroso rispetto dei parametri delle funzioni
declare (strict_types=1);
// inclusione di classi e interfacce
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao2.php";
require_once __DIR__."/Métier.php";
// test ----------------
// creazione dei livelli
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// utilizzo del livello [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
Commenti
- righe 36-38: la struttura a livelli utilizza ora il livello [Dao2];
I risultati dell'esecuzione sono i seguenti:
[Métier] : récupération de la personne d'identifiant 4
[Dao2] : Récupération de la personne d'identité (4) en base de données [distante]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao2] : Sauvegarde de la personne [Personne(4)] en base de données [distante]
Lo strato [Dao1] simula un accesso a un database locale, mentre lo strato [Dao2] simula un accesso a un database remoto. Finché questi due livelli rispettano l’interfaccia [IDao], si nota che il codice del livello [Métier] non ha dovuto essere modificato.
Applicheremo quanto appena appreso all’esercizio sul calcolo delle imposte che ci funge da filo conduttore.