10. Aplikacje warstwowe
10.1. Introduction

Teraz dowiemy się, jak zorganizować aplikację PHP w warstwach:

Na tym schemacie to warstwa po lewej stronie inicjuje korzystanie z warstwy po prawej stronie. Role poszczególnych warstw są następujące:
- [1]: warstwa o nazwie [dao] (Data Access Objects) odpowiada za wymianę danych z zewnętrznymi magazynami danych [4] (pliki, bazy danych, usługi internetowe…). Warstwa ta jest czasami nazywana [DAL] (Data Access Layer) – termin ten lepiej opisuje jej rolę. Warstwa ta może odczytywać dane [3] lub zapisywać dane [2]. Jest ona wywoływana przez warstwę [métier] [6] i zwraca jej wyniki [7];
- [5]: warstwa o nazwie [métier], która skupia procedury „biznesowe”, którym dostarcza się wszystkie potrzebne dane. Jest to zazwyczaj najbardziej stabilna warstwa projektu, ponieważ nie zależy od sposobu pozyskiwania danych. Dane te pochodzą z dwóch źródeł:
- [9]: dane dostarczane przez skrypt PHP;
- [6,7]: dane pobierane z warstwy [dao].
- [8]: główny skrypt pełni rolę koordynatora. W aplikacji konsolowej:
- tworzy warstwy [métier] i [dao];
- realizuje algorytm aplikacji. Algorytm ten pełni rolę koordynatora: nie zawiera żadnego kodu biznesowego ani kodu dostępu do danych. Główny skrypt ogranicza się do wywoływania procedur warstw [métier] i [9]. Całkowicie ignoruje warstwę [dao] oraz dane zewnętrzne. Może dostarczać warstwie [métier] dane z warstwy [9]. W aplikacji konsolowej dane te mogą pochodzić z plików konfiguracyjnych lub od użytkownika skryptu. Otrzymuje wyniki [10] z warstwy [métier]. Może zaistnieć potrzeba zapisania niektórych wyników: w tym celu ponownie wykorzystuje procedury warstwy [métier], które z kolei zwracają się do warstwy [dao], która wykona zadanie;
- wśród wyników skrypt główny może otrzymać wyjątki. Jego rolą jest obsługa wyjątków zgłaszanych ze wszystkich warstw;
Ten podział na warstwy ma na celu ułatwienie rozbudowy aplikacji. W tym celu każda warstwa implementuje interfejs. Załóżmy, że:
- warstwa [dao] jest implementowana przez klasę [Dao], która implementuje interfejs [IDao];
- warstwa [métier] jest implementowana przez klasę [Métier], która implementuje interfejs [IMétier];
Procedury warstwy [métier] będą wówczas korzystać z interfejsu [IDao] zamiast klasy [Dao]. Pozwala to na rozbudowę warstwy [Dao] bez ingerencji w warstwę [Métier]. Załóżmy, że w wersji 1 warstwa [Dao1] korzysta z danych z bazy danych. W wyniku aktualizacji dane te są teraz dostarczane przez usługę internetową w wersji 2, [Dao2]. Należy upewnić się, że obie klasy [Dao1] i [Dao2] implementują ten sam interfejs [IDao] oraz że generują te same wyjątki. Jeśli zostanie to zweryfikowane, warstwa [Métier], która współpracuje z interfejsem [IDao], pozostanie niezmieniona;
To samo rozumowanie dotyczy warstwy [Métier].
Przyjrzyjmy się implementacji z wykorzystaniem klas i interfejsów:

Aplikacja zaimplementuje następującą strukturę warstwową:

10.2. Obiekty wymieniane między warstwami
Zazwyczaj warstwy wymieniają między sobą różne obiekty. W tym przypadku będą wymieniać następującą klasę: [Personne.php]:
<?php
// osoba
class Personne {
// identyfikator
private $id;
// konstruktor
public function __construct(int $id) {
$this->id = $id;
}
// toString
public function __toString(): string {
return "[Personne($this->id)]";
}
}
Komentarze
- wiersz 6: osoba posiada tylko jeden atrybut, czyli swój identyfikator [$id]. Założymy, że identyfikator ten odnosi się do jednej konkretnej osoby w hurtowni danych;
- wiersze 9–11: konstruktor umożliwiający utworzenie typu [Personne] wraz z jego identyfikatorem;
- wiersze 14–16: metoda [__toString], która wyświetla identyfikator osoby;
10.3. Warstwa [dao]
Interfejs [IDao] zaimplementowany przez warstwę [dao, 1] ma następujący wygląd: [IDao.php]:
<?php
// warstwa [dao] ------------------------
interface IDao {
// odbiór osoby z magazynu zewnętrznego
// podaje się identyfikator osoby
public function get(int $id): Personne;
// zapisanie osoby w zewnętrznym magazynie
public function save(Personne $p): void;
}
Interfejs ten jest zaimplementowany przez następującą klasę [Dao1]: [Dao1.php]:
<?php
class Dao1 implements IDao {
// zapisanie osoby w zewnętrznym magazynie
public function save(Personne $p): void {
print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
}
// pobieranie osoby z zewnętrznego magazynu
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);
}
}
Uwagi
- klasa nie współpracuje z hurtownią danych. Wyświetlane są jedynie komunikaty umożliwiające śledzenie przebiegu kodu (wiersze 7 i 12);
- wiersz 13: zwracana jest osoba o identyfikatorze przekazanym jako parametr do metody (wiersz 11);
Implementujemy również interfejs [IDao] za pomocą następującej klasy [Dao2]: [Dao2.php]:
<?php
class Dao2 implements IDao {
// zapisanie osoby w zewnętrznym magazynie
public function save(Personne $p): void {
print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
}
// pobieranie osoby z zewnętrznego magazynu
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);
}
}
Klasa [Dao2] jest podobna do klasy [Dao1], z tą różnicą, że zmieniliśmy wyświetlane komunikaty.
10.4. Warstwa [métier]
Warstwa [métier, 2] udostępnia następujący interfejs [IMétier] [IMetier.php]:
<?php
// warstwa [métier] ------------------------
interface IMétier extends IDao {
// przetwarzanie osoby zidentyfikowanej na podstawie jej identyfikatora
public function doSomething(Personne $p): void;
}
Uwagi
- Interfejs [IMétier] rozszerza interfejs [IDao]. Nie jest to wcale obowiązkowe. Robimy to tutaj, ponieważ przykład jest prosty;
- wiersz 7: metoda [doSomething] jest specyficzna dla warstwy [Métier];
Interfejs [IMétier] zostanie zaimplementowany przez następującą klasę [Métier]: [Métier.php]:
<?php
class Métier implements IMétier {
// warstwa [dao]
private $dao;
// metody getter / setter
public function setDao(IDao $dao): void {
$this->dao = $dao;
}
public function getDao(): IDao {
return $this->dao;
}
// wykorzystuje się dane osoby
public function doSomething(Personne $p): void {
// przetwarzanie
print "Métier : Traitement métier de la personne $p\n";
}
// zapis danych osoby
public function save(Personne $p): void {
print "[Métier] : Sauvegarde de la personne $p\n";
// zwracamy się do warstwy [dao] o zapisanie danych
$this->dao->save($p);
}
public function get(int $id): Personne {
print "[Métier] : récupération de la personne d'identifiant $id\n";
// żądanie osoby od warstwy [dao]
$personne = $this->dao->get($id);
return $personne;
}
}
Uwagi
- wiersz 5: warstwa [métier] musi posiadać odwołanie do warstwy [dao], aby móc korzystać z jej metod;
- wiersze 8–10: metoda [setDao] pozwala przypisać warstwie [métier] odwołanie do warstwy [dao]. Należy zauważyć, że typem parametru jest [IDao]. Dzięki temu warstwa [métier] będzie mogła współpracować z każdą klasą implementującą interfejs [IDao]. Jeśli przejdziemy z warstwy [Dao1] do warstwy [Dao2] i obie implementują interfejs [IDao], warstwa [métier] nie wymaga przepisywania;
- wiersze 17–20: implementacja interfejsu [IMetier::doSomething];
- wiersze 23–27: implementacja interfejsu [IMetier::save]. Metoda ta musi zapisać dane osoby w hurtowni danych. Warstwa [métier] nie potrafi tego wykonać. Zwraca się do warstwy [dao] w celu wykonania tego zapisu;
- wiersze 29–34: implementacja metody [IMetier::get]. Metoda ta ma za zadanie pobrać z hurtowni danych osobę o identyfikatorze przekazanym jako parametr. Warstwa [métier] nie potrafi tego wykonać. Zwraca się do warstwy [dao], aby ta wykonała zadanie (wiersz 32);
Wniosek
Gdy tylko warstwa [métier] potrzebuje dostępu do danych przechowywanych w hurtowni danych, musi skorzystać z warstwy [dao], która została utworzona w celu uzyskania dostępu do tych danych.
10.5. Skrypt główny
Napiszemy dwa skrypty, które będą koordynować działanie tej aplikacji. Pierwszy z nich, [main1.php], będzie korzystał z warstwy [Dao1], natomiast drugi, [main2.php], będzie korzystał z warstwy [Dao2]. Chcemy pokazać, że nie ma to żadnego wpływu na kod warstwy [Métier].
Skrypt [main1.php] wygląda następująco:
<?php
// ścisłe przestrzeganie parametrów funkcji
declare (strict_types=1);
// dołączanie klas i interfejsów
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 ----------------
// tworzenie warstw
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// korzystanie z warstwy [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
Komentarze
- przypomnijmy kilka kwestii: skrypt [main1.php] pełni rolę koordynatora aplikacji. Tworzy on strukturę warstwową aplikacji (wiersze 15–17), a następnie rozpoczyna komunikację z warstwą [métier] (wiersze 19–21). Struktura warstwowa wygląda następująco:

Zgodnie z tym schematem skrypt [main1.php] powinien komunikować się wyłącznie z warstwą [métier]. Nie powinien komunikować się z warstwą [dao], nawet jeśli teoretycznie jest to możliwe.
Wyniki wykonania są następujące:
[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]
Komentarze
- wiersz 10 kodu spowodował zapisanie wierszy 1 i 2 wyników;
- wiersz 20 kodu spowodował zapisanie wiersza 3 wyników;
- wiersz 21 kodu spowodował zapisanie wierszy 4 i 5 wyników;
Skrypt [main2.php] wygląda następująco:
<?php
// ścisłe przestrzeganie parametrów funkcji
declare (strict_types=1);
// dołączanie klas i interfejsów
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 ----------------
// tworzenie warstw
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// korzystanie z warstwy [métier]
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
Uwagi
- wiersze 36–38: struktura warstwowa wykorzystuje teraz warstwę [Dao2];
Wyniki wykonania są następujące:
[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]
Warstwa [Dao1] symuluje dostęp do lokalnej bazy danych, natomiast warstwa [Dao2] symuluje dostęp do zdalnej bazy danych. O ile obie te warstwy są zgodne z interfejsem [IDao], widać, że kod warstwy [Métier] nie wymagał zmian.
Zastosujemy to, czego się właśnie nauczyliśmy, do ćwiczenia dotyczącego obliczania podatków, które służy nam jako wątek przewodni.