10. Gelaagde toepassingen
10.1. Introduction

We gaan nu ontdekken hoe we een PHP-toepassing in lagen kunnen structureren:

In dit schema neemt de linkerlaag het initiatief om de rechterlaag te gebruiken. De rol van de lagen is als volgt:
- [1]: de laag met de naam [dao] (Data Access Objects) zorgt voor de uitwisseling met externe gegevensopslagplaatsen [4] (bestanden, databases, webservices…). Deze laag wordt soms [DAL] (Data Access Layer) genoemd, een term die de rol van de laag beter beschrijft. Deze laag kan gegevens uit [3] lezen of naar [2] schrijven. Ze wordt aangeroepen door de laag [métier] [6] en levert resultaten terug aan deze laag [7];
- [5]: een laag met de naam [métier] die ‘bedrijfsspecifieke’ procedures bundelt waaraan alle benodigde gegevens worden aangeleverd. Dit is doorgaans de meest stabiele laag van een project, omdat deze niet afhankelijk is van de manier waarop de gegevens worden verkregen. Deze gegevens hebben twee bronnen:
- [9]: de gegevens die worden aangeleverd door het script PHP;
- [6,7]: de gegevens die worden opgevraagd bij de laag [dao].
- [8]: het hoofdscript fungeert als regisseur. In een console-applicatie zal het:
- de lagen [métier] en [dao] aanmaken;
- het algoritme van de applicatie uitvoeren. Dit algoritme is dat van een dirigent: er zit geen ‘bedrijfsspecifieke’ code of code voor gegevenstoegang in. Het hoofdscript beperkt zich tot het aanroepen van de procedures van de lagen [métier] en [9]. Het negeert de laag [dao] en de externe gegevens volledig. Het kan de laag [métier] voorzien van gegevens uit de laag [9]. In een console-applicatie kunnen deze afkomstig zijn uit configuratiebestanden of van de gebruiker van het script. Hij ontvangt [10]-resultaten van de laag [métier]. Het kan nodig zijn om bepaalde resultaten op te slaan: hiervoor maakt het opnieuw gebruik van de procedures van de laag [métier], die op hun beurt de laag [dao] aanspreken, die het werk zal uitvoeren;
- onder de resultaten kan het hoofdscript uitzonderingen ontvangen. Het is zijn taak om de uitzonderingen af te handelen die vanuit alle lagen worden doorgegeven;
Deze indeling in lagen is bedoeld om de verdere ontwikkeling van de applicatie te vergemakkelijken. Daartoe implementeert elke laag een interface. Stel dat:
- de laag [dao] wordt geïmplementeerd door een klasse [Dao], die op haar beurt een interface [IDao] implementeert;
- de laag [métier] wordt geïmplementeerd door een klasse [Métier] die een interface [IMétier] implementeert;
De procedures van de laag [métier] zullen dan de interface [IDao] gebruiken in plaats van de klasse [Dao]. Hierdoor kan de laag [Dao] worden aangepast zonder de laag [Métier] te beïnvloeden. Stel dat in versie 1 de laag [Dao1] gebruikmaakt van gegevens uit een database. Na een aanpassing worden deze gegevens in versie 2, [Dao2], nu geleverd door een webservice. We zorgen ervoor dat de twee klassen [Dao1] en [Dao2] dezelfde interface [IDao] implementeren en dat ze dezelfde uitzonderingen genereren. Als dit is gecontroleerd, blijft de laag [Métier], die met de interface [IDao] werkt, ongewijzigd;
Dezelfde redenering geldt voor de laag [Métier].
Laten we eens kijken naar een implementatie met klassen en interfaces:

De applicatie zal de volgende gelaagde structuur implementeren:

10.2. Objecten die tussen lagen worden uitgewisseld
Normaal gesproken wisselen de lagen verschillende objecten uit. Hier zullen ze de volgende klasse uitwisselen: [Personne.php]:
<?php
// een persoon
class Personne {
// identificatie
private $id;
// maker
public function __construct(int $id) {
$this->id = $id;
}
// toString
public function __toString(): string {
return "[Personne($this->id)]";
}
}
Opmerkingen
- regel 6: de persoon heeft slechts één attribuut, namelijk zijn identificatiecode [$id]. We gaan ervan uit dat deze verwijst naar één unieke persoon in een datawarehouse;
- regels 9-11: de constructor waarmee een type [Personne] met zijn identificatiecode kan worden aangemaakt;
- regels 14-16: de methode [__toString] die de identificatiecode van de persoon weergeeft;
10.3. Laag [dao]
De interface [IDao], geïmplementeerd door de laag [dao, 1], is als volgt: [IDao.php]:
<?php
// laag [dao] ------------------------
interface IDao {
// ophalen van een persoon in een extern magazijn
// de identificatiegegevens van de persoon worden doorgegeven
public function get(int $id): Personne;
// een persoon opslaan in een extern magazijn
public function save(Personne $p): void;
}
Deze interface wordt geïmplementeerd door de volgende klasse [Dao1] [Dao1.php]:
<?php
class Dao1 implements IDao {
// opslaan van een persoon in een extern magazijn
public function save(Personne $p): void {
print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
}
// ophalen van een persoon uit een extern magazijn
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);
}
}
Opmerkingen
- de klasse communiceert niet met een datawarehouse. Er worden alleen berichten weergegeven om de uitvoering van de code te volgen (regels 7 en 12);
- regel 13: er wordt inderdaad een persoon geretourneerd met de ID die als parameter aan de methode is doorgegeven (regel 11);
We implementeren ook de interface [IDao] met de volgende klasse [Dao2] [Dao2.php]:
<?php
class Dao2 implements IDao {
// opslaan van een persoon in een extern magazijn
public function save(Personne $p): void {
print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
}
// ophalen van een persoon uit een extern magazijn
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);
}
}
De klasse [Dao2] is vergelijkbaar met de klasse [Dao1], behalve dat we de weergegeven berichten hebben aangepast.
10.4. Laag [métier]
De laag [métier, 2] biedt de volgende interface [IMétier] [IMetier.php]:
<?php
// laag [métier] ------------------------
interface IMétier extends IDao {
// een persoon op basis van zijn ID verwerken
public function doSomething(Personne $p): void;
}
Opmerkingen
- De interface [IMétier] breidt de interface [IDao] uit. Dit is helemaal niet verplicht. We doen dit hier omdat het voorbeeld eenvoudig is;
- regel 7: de methode [doSomething] is specifiek voor de laag [Métier];
De interface [IMétier] wordt geïmplementeerd door de volgende klasse [Métier]: [Métier.php]:
<?php
class Métier implements IMétier {
// laag [dao]
private $dao;
// getter / setter
public function setDao(IDao $dao): void {
$this->dao = $dao;
}
public function getDao(): IDao {
return $this->dao;
}
// er wordt gebruik gemaakt van een persoon
public function doSomething(Personne $p): void {
// verwerking
print "Métier : Traitement métier de la personne $p\n";
}
// de persoon opslaan
public function save(Personne $p): void {
print "[Métier] : Sauvegarde de la personne $p\n";
// de laag [dao] wordt gevraagd om de persoon op te slaan
$this->dao->save($p);
}
public function get(int $id): Personne {
print "[Métier] : récupération de la personne d'identifiant $id\n";
// de persoon wordt opgevraagd bij de laag [dao]
$personne = $this->dao->get($id);
return $personne;
}
}
Opmerkingen
- regel 5: de laag [métier] moet een verwijzing naar de laag [dao] hebben om de methoden daarvan te kunnen gebruiken;
- regels 8-10: met de methode [setDao] kan aan de laag [métier] de verwijzing naar de laag [dao] worden toegekend. Merk op dat het type van de parameter [IDao] is. Daardoor kan de laag [métier] werken met elke klasse die de interface [IDao] implementeert. Als we overschakelen van een laag [Dao1] naar een laag [Dao2] en beide de interface [IDao] implementeren, hoeft de laag [métier] niet te worden herschreven;
- regels 17-20: implementatie van [IMetier::doSomething];
- regels 23-27: implementatie van [IMetier::save]. Deze methode moet een persoon opslaan in een datawarehouse. De laag [métier] kan dit niet. Deze wendt zich tot de laag [dao] om deze opslag uit te voeren;
- regels 29-34: implementatie van [IMetier::get]. Deze methode moet uit een datawarehouse de persoon ophalen waarvan de ID als parameter wordt doorgegeven. De laag [métier] kan dit niet uitvoeren. Zij doet een beroep op de laag [dao] om de taak uit te voeren (regel 32);
Conclusie
Zodra de laag [métier] toegang nodig heeft tot de gegevens die zijn opgeslagen in het datawarehouse, moet deze via de laag [dao] gaan, die is aangemaakt om toegang te krijgen tot deze gegevens.
10.5. Hoofdscript
We gaan twee scripts schrijven die als orkestleiders voor deze applicatie fungeren. Het eerste script, [main1.php], maakt gebruik van de laag [Dao1], terwijl het tweede script, [main2.php], gebruikmaakt van de laag [Dao2]. We willen laten zien dat dit geen invloed heeft op de code van de laag [Métier].
Het script [main1.php] ziet er als volgt uit:
<?php
// strikte naleving van de functieparameters
declare (strict_types=1);
// opname van klassen en interfaces
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 ----------------
// aanmaken van lagen
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// gebruik van de laag [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
Opmerkingen
- Laten we enkele punten nog eens op een rijtje zetten: het script [main1.php] is de regisseur van de applicatie. Het creëert de laagstructuur van de applicatie (regels 15-17) en begint vervolgens te communiceren met de laag [métier] (regels 19-21). De gelaagde structuur ziet er als volgt uit:

Volgens dit schema mag het script [main1.php] zich alleen richten tot de laag [métier]. Het mag zich niet richten tot de laag [dao], ook al is dat theoretisch mogelijk.
De resultaten van de uitvoering zijn als volgt:
[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]
Opmerkingen
- regel 10 van de code heeft ervoor gezorgd dat de regels 1 en 2 van de resultaten zijn geschreven;
- regel 20 van de code heeft ervoor gezorgd dat regel 3 van de resultaten is geschreven;
- regel 21 van de code heeft ervoor gezorgd dat de regels 4 en 5 van de resultaten zijn geschreven;
Het script [main2.php] is als volgt:
<?php
// strikte naleving van functieparameters
declare (strict_types=1);
// opname van klassen en interfaces
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 ----------------
// aanmaken van lagen
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// gebruik van de laag [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
Opmerkingen
- regels 36-38: de laagstructuur maakt nu gebruik van de laag [Dao2];
De uitvoerresultaten zijn als volgt:
[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]
De laag [Dao1] simuleert toegang tot een lokale database, terwijl de laag [Dao2] toegang tot een externe database simuleert. Zolang deze twee lagen voldoen aan de interface [IDao], zien we dat de code van de laag [Métier] niet hoefde te worden gewijzigd.
We gaan wat we zojuist hebben geleerd toepassen op de oefening over belastingberekening, die als rode draad dient.