Skip to content

10. Aplikacje warstwowe

10.1. Introduction

Image

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

Image

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:

Image

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

Image

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:

Image

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.