Skip to content

18. Ćwiczenie praktyczne – wersja 8

Wrócimy do przykładowej aplikacji – wersja 5 (akapit „link”) i przekształcimy ją w aplikację klient-serwer.

18.1. Introduction

Architektura wersji 5 wyglądała następująco:

Image

  • warstwa o nazwie [dao] (Data Access Objects) odpowiada za komunikację z bazą danych MySQL oraz lokalnym systemem plików;
  • warstwa o nazwie [métier] dokonuje obliczeń podatku;
  • główny skrypt pełni rolę koordynatora: instancjonuje warstwy [dao] i [métier], a następnie komunikuje się z warstwą [métier] w celu wykonania niezbędnych czynności;

Przeniesiemy tę architekturę do następującej architektury klient-serwer:

Image

  • W warstwie [2] przejmujemy warstwę [dao] z wersji 5, usuwając z niej metody dostępu do lokalnego systemu plików. Metody te zostaną przeniesione do warstwy [dao] klienta [6, 7];
  • w warstwie [3], warstwa [métier] pozostanie warstwą z wersji 5 bez metod [executeBatchImpôts, saveResults], które zostaną przeniesione do warstwy [dao] i [7] klienta;
  • w warstwie [4] należy napisać skrypt serwerowy, który będzie musiał:
    • utworzyć warstwy [métier] oraz [dao] i [3, 2];
    • komunikować się ze skryptem klienckim [5, 7];
  • w przypadku [7] należy napisać warstwę kliencką [dao]:
    • będzie ona klientem HTTP skryptu serwerowego [4, 5];
    • będzie ona wykorzystywać metody dostępu do lokalnego systemu plików warstwy [dao] z wersji 5;
  • w [8] warstwa kliencka [métier] będzie zgodna z interfejsem [InterfaceMetier] z wersji 5. Jej implementacja będzie jednak inna. W wersji 5 warstwa [métier] wykonywała obliczenia podatku. Tutaj obliczenia te wykonuje warstwa [métier] serwera. Warstwa [métier] będzie zatem wywoływać warstwy [dao] i [7] w celu nawiązania komunikacji z serwerem i poproszenia go o obliczenie podatku;
  • w warstwie [9] skrypt konsolowy będzie musiał utworzyć instancję warstw klienckich [dao, métier] i uruchomić ich wykonywanie;

18.2. Serwer

Zajmujemy się częścią serwerową aplikacji.

Image

Architektura ta zostanie zaimplementowana za pomocą następujących skryptów:

Image

18.2.1. Elementy wymieniane między warstwami

Image

Entities wymieniane między warstwami to te z wersji 5 opisane w akapicie „link”.

18.2.2. Warstwa [dao]

Image

Warstwa [dao] implementuje następujący interfejs [InterfaceServerDao]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceServerDao {

  // odczyt danych z urzędu skarbowego
  public function getTaxAdminData(): TaxAdminData;
}
  • wiersz 9: metoda [getTaxAdminData] pobiera dane z urzędu skarbowego z bazy danych;

Interfejs [InterfaceServerDao] jest zaimplementowany przez następującą klasę [ServerDao]:


<?php

// przestrzeń nazw
namespace Application;

// definicja klasy ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
  // obiekt typu TaxAdminData zawierający dane dotyczące przedziałów podatkowych
  private $taxAdminData;
  // obiekt typu [Database] zawierający cechy charakterystyczne BD
  private $database;

  // konstruktor
  public function __construct(string $databaseFilename) {
    // zapisywana jest konfiguracja bazy danych JSON
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // przygotowuje się atrybut
    $this->taxAdminData = new TaxAdminData();
    try {
      // otwieramy połączenie z bazą danych
      $connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
      // chcemy, aby przy każdym błędzie SGBD zgłaszany był wyjątek
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // rozpoczynamy transakcję
      $connexion->beginTransaction();
      // wypełniamy tabelę przedziałów podatkowych
      $this->getTranches($connexion);
      // wypełnia się tabelę stałych
      $this->getConstantes($connexion);
      // transakcja zostaje pomyślnie zakończona
      $connexion->commit();
    } catch (\PDOException $ex) {
      // czy trwa jakaś transakcja?
      if (isset($connexion) && $connexion->inTransaction()) {
        // zakończenie transakcji z powodu niepowodzenia
        $connexion->rollBack();
      }
      // przekazujemy wyjątek do kodu wywołującego
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // zamyka się połączenie
      $connexion = NULL;
    }
  }

  // odczyt danych z bazy
  private function getTranches($connexion): void {

  }

  // odczyt tabeli stałych
  private function getConstantes($connexion): void {

  }

  // zwraca dane umożliwiające obliczenie podatku
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }

}

Kod ten został przedstawiony w akapicie „link”.

18.2.3. Warstwa [métier]

Image

Image

Warstwa [métier] implementuje następujący interfejs [InterfaceServerMetier]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceServerMetier {

  // obliczanie podatków podatnika
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}

Interfejs [InterfaceServerMetier] jest zaimplementowany przez następującą klasę [ServerMetier]:


<?php

// przestrzeń nazw
namespace Application;

class ServerMetier implements InterfaceServerMetier {
  // warstwa DAO
  private $dao;
  // dane administracji podatkowej
  private $taxAdminData;

  //---------------------------------------------
  // ustawianie warstwy [dao]
  public function setDao(InterfaceServerDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceServerDao $dao) {
    // zapisuje się odniesienie do warstwy [dao]
    $this->dao = $dao;
    // pobierane są dane umożliwiające obliczenie podatku
    // metoda [getTaxAdminData] może wygenerować wyjątek ExceptionImpots
    // następnie pozwala się na przekazanie wyjątku do kodu wywołującego
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// obliczenie podatku
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // wynik
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

// --------------------------------------------------------------------------
  private function calculerImpot2(string $marié, int $enfants, float $salaire): array {

    // wynik
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=wynagrodzenieRoczne-ulga
  // odliczenie ma wartość minimalną i maksymalną
  private function getRevenuImposable(float $salaire): float {

    // wynik
    return floor($revenuImposable);
  }

// oblicza ewentualną obniżkę
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // wynik
    return ceil($décôte);
  }

// oblicza ewentualną obniżkę
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    ..
    // wynik
    return ceil($réduction);
  }
}

Kod ten został już omówiony w wersji 1 w akapicie [link]. Jego wersja obiektowa z bazą danych została przedstawiona w akapicie [link].

18.2.4. Skrypt serwerowy

Image

Image

Skrypt serwerowy implementuje warstwę [web] [4]. Skrypt [impots-server] jest konfigurowany przez następujący plik jSON [config-server.json]:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
    "databaseFilename": "Data/database.json",
    "taxAdminDataFileName": "Data/taxadmindata.json",
    "relativeDependencies": [
        "/Entities/BaseEntity.php",
        "/Entities/ExceptionImpots.php",
        "/Entities/TaxAdminData.php",
        "/Entities/Database.php",
        "/Dao/InterfaceServerDao.php",
        "/Dao/ServerDao.php",
        "/Métier/InterfaceServerMetier.php",
        "/Métier/ServerMetier.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ]
}
  • wiersz 1: katalog główny, od którego będą mierzone ścieżki do plików;
  • wiersz 2: plik konfiguracyjny bazy danych jSON dla bazy danych MySQL;
  • wiersz 3: plik jSON zawierający dane administracji podatkowej;
  • wiersze 5–14: pliki aplikacji;
  • wiersz 15: zależność od bibliotek zewnętrznych, w tym przypadku Symfony;
  • wiersze 16–20: tabela użytkowników uprawnionych do korzystania z aplikacji;

Pliki jSON i [database.json, taxadmindata.json] należą do wersji 5 opisanej w akapicie „link”.

Skrypt [impots-server] implementuje warstwę [web] w następujący sposób:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// obsługa błędów przez PHP
//ini_set("display_errors", "0");
//
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", "Data/config-server.json");

// pobieramy konfigurację
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// dołączamy zależności niezbędne dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// definicja stałych
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;

// przygotowanie odpowiedzi serwera JSON
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// pobieranie bieżącego żądania
$request = Request::createFromGlobals();
// uwierzytelnianie
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// czy użytkownik istnieje?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
  $trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
  $i++;
}
// ustalanie kodu statusu odpowiedzi
if (!$trouvé) {
  // nie znaleziono – kod 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
  // komunikat o błędzie
  $response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
  $response->send();
  // koniec
  exit;
}
// mamy prawidłowego użytkownika – sprawdzamy otrzymane parametry
$erreurs = [];
// muszą być podane trzy parametry GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// błąd?
if ($erreur) {
  $erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}

// pobieramy stan cywilny
if (!$request->query->has("marié")) {
  $erreurs[] = "paramètre marié manquant";
} else {
  $marié = trim(strtolower($request->query->get("marié")));
  $erreur = $marié !== "oui" && $marié !== "non";
  // błąd?
  if ($erreur) {
    $erreurs[] = "paramètre marié [$marié] invalide";
  }
}

// pobieramy liczbę dzieci
if (!$request->query->has("enfants")) {
  $erreurs[] = "paramètre enfants manquant";
} else {
  $enfants = trim($request->query->get("enfants"));
  // liczba dzieci musi być liczbą całkowitą >=0
  $erreur = !preg_match("/^\d+$/", $enfants);
  // błąd?
  if ($erreur) {
    $erreurs[] = "paramètre enfants [$enfants] invalide";
  }
}

// pobieramy roczne wynagrodzenie
if (!$request->query->has("salaire")) {
  $erreurs[] = "paramètre salaire manquant";
} else {
  // wynagrodzenie musi być liczbą całkowitą >=0
  $salaire = trim($request->query->get("salaire"));
  $erreur = !preg_match("/^\d+$/", $salaire);
  // błąd?
  if ($erreur) {
    $erreurs[] = "paramètre salaire [$salaire] invalide";
  }
}

// inne parametry w zapytaniu?
foreach (\array_keys($request->query->all()) as $key) {
  // czy parametr jest prawidłowy?
  if (!\in_array($key, ["marié", "enfants", "salaire"])) {
    $erreurs[] = "paramètre [$key] invalide";}
}

// błędy?
if ($erreurs) {
  // wysyłamy kod błędu 400 do klienta
  $response->setStatusCode(Response::HTTP_BAD_REQUEST);
  $response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// mamy wszystko, co potrzebne do pracy
// tworzenie architektury serwera
$msgErreur = "";
try {
  // tworzenie warstwy [dao]
  $dao = new ServerDao($config["databaseFilename"]);
  // tworzenie warstwy [métier]
  $métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// zauważono błąd
  $msgErreur = utf8_encode($ex->getMessage());
}
// błąd?
if ($msgErreur) {
  // wysyłamy kod błędu 500 do klienta
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  $response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// obliczanie podatku
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// zwracamy odpowiedź
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();

Komentarze

  • wiersz 16: odczytuje się plik konfiguracyjny;
  • wiersze 18–26: ładowane są wszystkie zależności;
  • wiersz 29: nazwa pliku [database.json];
  • wiersze 32–33: deklaruje się klasy bibliotek zewnętrznych, które będą używane;
  • wiersze 36–38: przygotowuje się odpowiedź o nazwie jSON;
  • wiersze 40–52: sprawdzamy, czy użytkownik wysyłający żądanie należy do grupy uprawnionych użytkowników;
  • wiersze 54–63: jeśli tak nie jest, wysyłany jest kod HTTP 401, oznaczający odmowę dostępu. Po otrzymaniu tego kodu oraz nagłówka HTTP [WWW-Authenticate => Basic realm=] większość przeglądarek wyświetla okno uwierzytelniania z prośbą o uwierzytelnienie się;
  • wiersz 59: odpowiedź serwera jSON wyjaśnia przyczynę błędu. Wszystkie odpowiedzi serwera będą stanowić ciąg znaków jSON z tablicy [‘réponse’=>’qq chose’];
  • wiersze 64–117: sprawdzana jest poprawność żądania:
    • żądanie o numerze GET zawierające dokładnie trzy parametry;
    • parametr [marié], którego wartość musi wynosić „tak” lub „nie”;
    • parametr [enfants], którego wartość musi być liczbą całkowitą >=0;
    • parametr [salaire], którego wartość musi być liczbą całkowitą >=0;
  • wiersz 65: za każdym razem, gdy wykryty zostanie błąd, do tablicy [$erreurs] dodawany jest komunikat o błędzie;
  • wiersze 120–126: jeśli wystąpi błąd, wysyłany jest kod HTTP [400 Bad Request] do klienta (wiersz 122);
  • wiersz 123: odpowiedź serwera o kodzie jSON wyjaśnia przyczynę błędu;
  • od wiersza 132 wszystko zostało sprawdzone. Można utworzyć instancję warstw [dao, métier]. Utworzenie tej instancji wiąże się z kosztem i należy to zrobić tylko wtedy, gdy mamy pewność, że żądanie jest prawidłowe;
  • wiersze 130–138: tworzy się architekturę serwera. Tworzenie warstwy [dao] może spowodować wygenerowanie wyjątku typu [ExceptionImpots]. Jeśli wystąpi ten wyjątek, odnotowuje się błąd;
  • wiersze 135–138: jeśli wystąpił wyjątek, wysyłamy do klienta kod HTTP 500. Kod ten oznacza, że serwer uległ awarii;
  • wiersz 143: odpowiedź wyjaśnia przyczynę błędu;
  • wiersz 148 : obliczenie podatku jest przekazywane do warstwy [métier];
  • wiersze 150–151: wysłanie odpowiedzi;

Przetestujmy ten skrypt w przeglądarce. Wywołajmy zabezpieczoną stronę URL poprzez [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

Image

  • w [1], żądany zabezpieczony URL;
  • na [2], trzy parametry [marié, enfants, salaire];
  • w [3] serwer Apache Laragon wysłał certyfikat SSL z podpisem własnym. Przeglądarka to wykryła i wyświetla ostrzeżenie dotyczące bezpieczeństwa: uznaje, że strona serwera nie jest godna zaufania;
  • w przypadku [4] kontynuujemy;

Image

  • w [6] kontynuujemy;

Image

  • w [7] przeglądarka wyświetla okno, w którym użytkownik może się uwierzytelnić;
  • w [9,10] wpisujemy [admin] i [admin];

Image

  • w [13] pojawia się odpowiedź serwera jSON;

Przeprowadźmy kilka testów błędów:

Wysyłamy żądanie o URL i [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]

Otrzymujemy następujący wynik:

Image

Odcinamy SGBD MySQL i wysyłamy żądanie URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

Image

18.2.5. Testy [Codeception]

Za każdym razem, gdy będziemy tworzyć nową wersję serwera, będziemy testować warstwy [métier] i [dao], tak jak to robiliśmy od wersji 04 (patrz akapity link i link).

Najpierw powiążemy projekt [scripts-web] z testami [Codeception]. W tym celu należy postępować zgodnie z procedurą opisaną dla projektu [scripts-console] w akapicie link. Otrzymujemy projekt [scripts-web] z folderem [Test Files]:

Image

Utworzymy test dla warstwy [dao] oraz test dla warstwy [métier].

18.2.5.1. Testy warstwy [dao]

Image

Test [ServerDaoTest] będzie wyglądał następująco:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// definicja stałych
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");

// pobieramy konfigurację
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// dołączanie zależności niezbędnych dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// test -----------------------------------------------------

class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    // element nadrzędny
    parent::__construct();
    // pobieramy konfigurację
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // tworzenie warstwy [dao]
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // testy
  public function testTaxAdminData() {

  }

}

Komentarze

  • wiersze 9–24: tworzone jest to samo środowisko pracy, co w przypadku serwera [impots-server.php]. Odbywa się to w wierszach 9–12 poprzez zdefiniowanie dwóch stałych, od których zależy to środowisko;
  • wiersze 32–40: tworzymy instancję warstwy [dao], która ma zostać przetestowana, tak jak zrobiono to w skrypcie serwera [impots-server.php];
  • od tego momentu panują takie same warunki jak w skrypcie serwerowym [impots-server.php]: można rozpocząć testy;
  • wiersze 43–45: metoda [testTaxAdminData] jest zgodna z opisem w akapicie „link”;

Wyniki testu są następujące:

Image

18.2.5.2. Testy warstwy [métier]

Image

Test [ServerMetierTest] będzie wyglądał następująco:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// definicja stałych
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// pobieranie konfiguracji
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// dołączanie zależności niezbędnych dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// klasa testowa
class ServerMetierTest extends \Codeception\Test\Unit {
  // warstwa biznesowa
  private $métier;

  public function __construct() {
    parent::__construct();
    // pobieramy konfigurację
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // tworzenie warstwy [dao]
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    // tworzenie warstwy [métier]
    $this->métier = new ServerMetier($dao);
  }

  // testy
  public function test1() {

  }

  public function test2() {

  }

  ..

  public function test11() {

  }

}

Komentarze

  • wiersze 9–24: tworzymy takie samo środowisko pracy jak w przypadku serwera [impots-server.php]. Odbywa się to w wierszach 9–12 poprzez zdefiniowanie dwóch stałych, od których zależy środowisko;
  • wiersze 30–38: tworzymy instancję warstwy [métier] do przetestowania, tak jak zrobiono to w skrypcie serwera [impots-server.php];
  • od tego momentu panują takie same warunki jak w skrypcie serwerowym [impots-server.php]: można rozpocząć testy;
  • wiersze 40–53: metody [test1, test2…, test11] są opisane w akapicie „link”;

Wyniki testu są następujące:

Image

18.3. Klient

Zajmujemy się częścią kliencką aplikacji.

Image

Architektura ta zostanie zaimplementowana za pomocą następujących skryptów:

Image

18.3.1. Elementy wymieniane między warstwami

Image

Wszystkie powyższe elementy zostały już opisane i wykorzystane:

18.3.2. Warstwa [dao]

Image

Warstwa [dao] implementuje następujący interfejs [InterfaceClientDao]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceClientDao {

  // odczyt danych podatników
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // obliczanie podatków podatnika
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // zapisywanie wyników
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
  • wiersz 9: funkcja [getTaxPayersData] wczytuje do pamięci dane podatników z pliku [$taxPayersFilename]. W przypadku wystąpienia błędów są one rejestrowane w pliku [$errorsFilename];
  • wiersz 12: funkcja [calculerImpots] oblicza podatek podatnika;
  • wiersz 15: funkcja [saveResults] zapisuje w pliku [$resultsFilename] dane z tablicy [$taxPayersData], które przedstawiają wyniki kilku obliczeń podatku;

Interfejs [InterfaceClientDao] jest zaimplementowany przez następującą klasę [ClientDao]:


<?php

namespace Application;

// zależności
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // wykorzystanie funkcji Trait
  use TraitDao;
  // atrybuty
  private $urlServer;
  private $user;

  // konstruktor
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }

  // obliczanie podatku
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // tworzy się klienta HTTP
    $httpClient = HttpClient::create([
        'auth_basic' => [$this->user["login"], $this->user["passwd"]],
        "verify_peer" => false
    ]);
    // wysyłamy zapytanie do serwera
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
    ]]);
    // odbieranie odpowiedzi
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // logi
    // wyświetla się „$json=json\n”;
    // pobieramy status odpowiedzi
    $statusCode = $response->getStatusCode();
    // błąd?
    if ($statusCode !== 200) {
      // wystąpił błąd – zgłaszamy wyjątek
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    // zwracamy odpowiedź
    return $réponse;
  }

}

Komentarze

  • wiersz 10: dodajemy klasę [TraitDao] (patrz akapit „link”), która implementuje metody [getTaxPayersData] i [saveResults]. Pozostaje więc do zaimplementowania tylko metoda [calculerImpots]. Jest ona zaimplementowana w wierszach 22–49;
  • wiersze 16–19: konstruktor klasy [ClientDao] przyjmuje dwa parametry:
    • URL [$urlServer] z serwera obliczającego podatek;
    • tablicę [$user] zawierającą klucze „login” i „passwd”, która określa użytkownika wysyłającego żądanie;
  • wiersz 22: metoda [calculerImpots] odbiera trzy parametry, które mają zostać wysłane do serwera obliczającego podatki;
  • wiersze 24–27: tworzy się klienta HTTP z:
    • wiersz 25: dane identyfikacyjne użytkownika wysyłającego żądanie;
    • wiersz 26: opcja, dzięki której klient HTTP nie będzie sprawdzał ważności certyfikatu SSL przesłanego przez serwer;
  • wiersze 29–34: wysyłane jest zapytanie do serwera z trzema oczekiwanymi przez niego parametrami;
  • wiersz 36: odbierana jest odpowiedź jSON z serwera. Jeśli nie podamy parametru [false] w metodzie [Response::getContent], to w przypadku, gdy status odpowiedzi serwera mieści się w przedziale [3xx-5xx] (sytuacja błędu), obiekt [Response] zgłasza wyjątek, gdy tylko spróbuje się uzyskać treść odpowiedzi [Response::getContent] lub jej nagłówki HTTP i [Response::getHeaders]. W tym przypadku, niezależnie od statusu odpowiedzi HTTP, chcemy mieć dostęp do jej treści, choćby po to, by ją zarejestrować w dzienniku (wiersz 40);
  • wiersze 37–38: odpowiedź serwera to ciąg znaków jSON z tablicy [‘réponse’=>qqChose]. Pobieramy [qqChose];
  • wiersz 40: rejestrujemy odpowiedź jSON w trybie programistycznym;
  • wiersz 42: pobieramy kod statusu odpowiedzi;
  • wiersze 44–49: jeśli kod statusu HTTP nie wynosi 200, oznacza to, że nasz serwer napotkał problem. Następnie generujemy wyjątek typu [ExceptionImpots] z komunikatem zawierającym odpowiedź jSON z serwera, uzupełnioną o kod HTTP z odpowiedzi;
  • wiersz 51: zwracamy wynik, którym jest tablica asocjacyjna z kluczami [impôt, surcôte, décôte, réduction, taux];

18.3.3. Warstwa [métier]

Image

Image

Warstwa [métier] [8] implementuje następujący interfejs [InterfaceClientMetier]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceClientMetier {

  // obliczanie podatków podatnika
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // obliczanie podatków w trybie wsadowym
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • wiersz 9: funkcja [calculerImpots] oblicza podatek;
  • wiersz 12: funkcja [executeBatchImpots] oblicza podatek dla podatników, których dane znajdują się w pliku [$taxPayersFileName], zapisuje uzyskane wyniki w pliku [$resultsFileName], a napotkane błędy w pliku [$errorsFileName];

Interfejs [InterfaceClientMetier] jest zaimplementowany przez następującą klasę [ClientMetier]:


<?php

// przestrzeń nazw
namespace Application;

class ClientMetier implements InterfaceClientMetier {
  // atrybut
  private $clientDao;

  // konstruktor
  public function __construct(InterfaceClientDao $clientDao) {
    // zapisywanie odniesienia w warstwie [dao]
    $this->clientDao = $clientDao;
  }
  
  // obliczanie podatku
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }

  // obliczanie podatków w trybie wsadowym
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // pozwala się na przekazywanie wyjątków pochodzących z warstwy [dao]
    // pobieranie danych podatników
    $taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabela wyników
    $results = [];
    // przetwarzamy je
    foreach ($taxPayersData as $taxPayerData) {
      // obliczamy podatek
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // uzupełnianie [$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // wpisuje się wynik do tabeli wyników
      $results [] = $taxPayerData;
    }
    // zapisywanie wyników
    $this->clientDao->saveResults($resultsFileName, $results);
  }

}

Komentarze

  • wiersze 11–14: konstruktor klasy [ClientMetier] otrzymuje jako parametr odwołanie do warstwy [dao];
  • wiersze 17–19: obliczenie podatku jest przekazywane do warstwy [dao];
  • wiersze 20–38: funkcja [executeBatchImpots] została opisana w akapicie „link”;

18.3.4. Skrypt główny

Image

Image

Skrypt kliencki [MainImpotsClient.php] implementuje warstwy [console] i [9]. Jest on konfigurowany przez następujący plik: jSON i [conf-client.json]:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "Entities/BaseEntity.php",
        "Entities/TaxPayerData.php",
        "Entities/ExceptionImpots.php",
        "Utilities/Utilitaires.php",
        "Dao/InterfaceClientDao.php",
        "Dao/TraitDao.php",
        "Dao/ClientDao.php",
        "Métier/InterfaceClientMetier.php",
        "Métier/ClientMetier.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php"
    ],
    "user": {
        "login": "admin",
        "passwd": "admin"
    },
    "urlServer": "https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php"
}
  • wiersz 1: katalog główny klienta;
  • wiersz 2: plik jSON zawierający dane podatników;
  • wiersz 3: plik jSON zawierający wyniki;
  • wiersz 4: plik jSON zawierający błędy;
  • wiersze 6–19: różne zależności projektu klienta;
  • wiersze 20–23: użytkownik wysyłający zapytania do serwera obliczającego podatki;
  • wiersz 24: zabezpieczony plik URL serwera obliczającego podatki;

Kod skryptu [MainImpotsClient.php] jest następujący:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// obsługa błędów przez PHP
//ini_set("display_errors", "0");
//
// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", "../Data/config-client.json");

// pobieramy konfigurację
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// dołączamy zależności niezbędne dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// definicje stałych
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// zależności Symfony
use Symfony\Component\HttpClient\HttpClient;

// tworzenie warstwy [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// tworzenie warstwy [métier]
$clientMetier = new ClientMetier($clientDao);

// obliczanie podatków w trybie wsadowym
try {
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
  // wyświetlenie błędu
  print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// koniec
print "Terminé\n";
exit;

Komentarze

  • wiersz 13: ścieżka do pliku konfiguracyjnego;
  • wiersz 16: przetwarzanie pliku konfiguracyjnego;
  • wiersze 18–26: ładowanie zależności;
  • wiersz 37: utworzenie warstwy [dao]. Przekazujemy konstruktorowi warstwy dwie informacje, których oczekuje:
    • identyfikator URL serwera obliczającego podatki;
    • identyfikatory użytkownika, który będzie wysyłał zapytania;
  • wiersz 39: utworzenie warstwy [métier]. Przekazujemy konstruktorowi warstwy odwołanie do właśnie utworzonej warstwy [dao];
  • wiersz 43: warstwa [métier] otrzymuje polecenie:
    • obliczenie podatków wszystkich podatników z pliku $config["taxPayerDataFileName"];
    • zapisać wyniki w pliku $config["resultsFileName"];
    • zapisać błędy w pliku $config["errorsFileName"];
  • w linii 43 mogą wystąpić wyjątki;
  • wiersz 46: wyświetlenie komunikatu o błędzie wyjątku;

Działanie klienta daje takie same wyniki jak w poprzednich wersjach. Sprawdź następujące pliki:

  • [Data/taxpayersdata.json]: dane podatników, dla których obliczana jest kwota podatku;
  • [Data/results.json]: wyniki dla poszczególnych podatników z pliku [Data/taxpayersdata.json];
  • [Data/errors.json]: błędy, które mogły wystąpić podczas przetwarzania pliku [Data/taxpayersdata.json];

Przyjrzyjmy się możliwym przypadkom błędów. Najpierw zatrzymajmy serwer Laragon. Wyniki w konsoli klienta będą wówczas następujące:


Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé

Teraz uruchommy tylko serwer Apache, a nie serwer SGBD MySQL:

Image

Wyniki w konsoli klienta będą wówczas następujące:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}
Terminé

Teraz uruchommy MySQL, a następnie zmieńmy w [config-client] użytkownika, który się loguje:

1
2
3
4
    "user": {
        "login": "x",
        "passwd": "x"
},

Wyniki w konsoli klienta są wówczas następujące:


L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé

18.3.5. Testy [Codeception]

Podobnie jak w przypadku poprzednich wersji, napiszemy testy [Codeception] dla wersji 08.

Image

18.3.5.1. Test warstwy [métier]

Test [ClientMetierTest.php] wygląda następująco:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);

// przestrzeń nazw
namespace Application;

// definicja stałych
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");

// ścieżka do pliku konfiguracyjnego
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");

// pobieranie konfiguracji
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// dołączanie zależności niezbędnych dla skryptu
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// zależności bezwzględne (biblioteki zewnętrzne)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// klasa testowa
class ClientMetierTest extends \Codeception\Test\Unit {
  // warstwa biznesowa
  private $métier;

  public function __construct() {
    parent::__construct();
    // pobieramy konfigurację
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // tworzenie warstwy [dao]
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // tworzenie warstwy [métier]
    $this->métier = new ClientMetier($clientDao);
  }

  // testy
  public function test1() {

  }

  -------------

  public function test11() {

  }

}

Komentarze

  • wiersze 10–26: definicja środowiska testowego. Wykorzystujemy to samo środowisko, co w głównym skrypcie [MainImpotsClient] opisanym w akapicie „link”;
  • wiersze 33–41: tworzenie warstw [dao] i [métier];
  • wiersz 40: atrybut [$this→métier] odwołuje się do warstwy [métier];
  • wiersze 44–51: metody [test1, test2…, test11] są zgodne z opisem w akapicie „link”;

Wyniki testu są następujące:

Image