Skip to content

19. Ćwiczenie praktyczne – wersja 9

W tej wersji ulepszymy serwer w następujący sposób:

  • obecnie przy każdym żądaniu dane urzędu skarbowego są wyszukiwane w bazie danych. Zastosujemy sesję:
    • przy pierwszym żądaniu użytkownika dane z urzędu skarbowego są wyszukiwane w bazie danych i zapisywane w sesji;
    • przy kolejnych żądaniach tego samego użytkownika dane z urzędu skarbowego są pobierane z sesji. Można oczekiwać niewielkiego skrócenia czasu wykonania, ponieważ zapytania do bazy danych są kosztowne;
  • serwer będzie rejestrował w pliku tekstowym ważne momenty:
    • pomyślne lub nieudane uwierzytelnienie;
    • poprawność lub niepoprawność parametrów przesłanych przez klienta;
    • wynik obliczenia podatku;
    • różne przypadki błędów;
  • w przypadku błędu krytycznego do administratora aplikacji zostanie wysłana wiadomość e-mail;

Klient również będzie musiał zostać zmodyfikowany, aby obsługiwał plik cookie sesji, który zostanie mu wysłany.

19.1. Serwer

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

Image

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

Image

19.1.1. Narzędzia

Image

19.1.1.1. Klasa [Logger]

Klasa [Logger] będzie wykorzystywana do zapisywania logów w pliku tekstowym:


<?php

namespace Application;

class Logger {
  // atrybut
  private $resource;

  // konstruktor
  public function __construct(string $logsFilename) {
    // otwarcie pliku
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  // zapisanie komunikatu w logach
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }

  // zamknięcie pliku dziennika
  public function close() {
    fclose($this->resource);
  }

}

Komentarze

  • wiersz 7: zasób pliku logów;
  • wiersz 10: konstruktor klasy otrzymuje jako parametr nazwę pliku dziennika;
  • wiersz 12: otwieramy plik tekstowy w trybie dodawania (a+): plik zostanie otwarty, a jego zawartość zachowana. Zapisy będą dokonywane na końcu obecnej zawartości;
  • wiersze 13–15: jeśli nie udało się otworzyć pliku, zgłaszany jest wyjątek;
  • wiersze 19–21: metoda [write] umożliwia zapisanie komunikatu [$message] w pliku dziennika, poprzedzonego datą i godziną;
  • wiersze 24–16: metoda [close] umożliwia zamknięcie pliku dziennika;

Uwaga: aplikacja serwerowa może obsługiwać wielu klientów jednocześnie. Mamy jednak tylko jeden plik logów dla wszystkich. Istnieje zatem ryzyko równoczesnego dostępu do zapisu w tym pliku. Należy więc zsynchronizować zapisy, aby zapobiec ich pomieszaniu. W tym celu klasa PHP korzysta z semaforów klasy [https://www.php.net/manual/fr/book.sem.php]. W tym miejscu pominęliśmy kwestię synchronizacji zapisów, jednak należy mieć świadomość istnienia tego problemu.

19.1.1.2. Klasa [SendAdminMail]

Klasa [SendAdminMail] umożliwia wysłanie wiadomości e-mail do administratora aplikacji w przypadku jej awarii:


<?php

namespace Application;

class SendAdminMail {
  // atrybuty
  private $config;
  private $logger;

  // konstruktor
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }

  public function send() {
    // wysyła $this->config['message'] na serwer SMTP $this->config['smtp-server'] na porcie $infos[smt-port]
    // jeśli warunek $this->config['tls'] jest spełniony, zostanie użyty mechanizm TLS
    // wiadomość e-mail jest wysyłana w imieniu $this->config['from']
    // dla odbiorcy $this->config['to']
    // wiadomość ma temat $this->config['subject']
    // do wiadomości dołączono załączniki o nazwach $this->config['attachments']
    // wynik działania metody
    try {
      // utworzenie wiadomości
      $message = (new \Swift_Message())
        // temat wiadomości
        ->setSubject($this->config["subject"])
        // nadawca
        ->setFrom($this->config["from"])
        // odbiorcy z wykorzystaniem słownika (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // treść wiadomości
        ->setBody($this->config["message"])
      ;
      // załączniki
      foreach ($this->config["attachments"] as $attachment) {
        // ścieżka do załącznika
        $fileName = __DIR__ . $attachment;
        // sprawdzamy, czy plik istnieje
        if (file_exists($fileName)) {
          // dołączamy dokument do wiadomości
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // błąd
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // protokół TLS?
      if ($this->config["tls"] === "TRUE") {
        // TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
          ->setUsername($this->config["user"])
          ->setPassword($this->config["password"]);
      } else {
        // brak TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // menedżer wysyłki
      $mailer = new \Swift_Mailer($transport);
      // wysłanie wiadomości
      $mailer->send($message);
      // koniec
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // błąd
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }

}

Komentarze

  • wiersz 11: konstruktor otrzymuje dwa parametry:
    • [$config]: tablica asocjacyjna zawierająca wszystkie informacje niezbędne do wysłania wiadomości e-mail;
    • [$logger]: moduł rejestrujący, umożliwiający rejestrowanie kluczowych momentów procesu wysyłania wiadomości e-mail;

Tablica asocjacyjna będzie miała następującą postać:

1
2
3
4
5
6
7
8
9
{
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
}
  • wiersze 16–76: metoda [send] umożliwia wysłanie wiadomości e-mail. Kod ten został przedstawiony i opisany w akapicie [link];

19.1.2. Warstwa [dao]

Image

Skrypt [ServeurDaoWithSession.php] wygląda następująco:


<?php

// przestrzeń nazw
namespace Application;

// definicja klasy ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {

  // konstruktor
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // najprostszy przypadek
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // przekazujemy kontrolę klasie nadrzędnej
      parent::__construct($databaseFilename);
    }
  }

}

Komentarze

  • wiersz 7: klasa [ServerDaoWithSession] z wersji 09 rozszerza klasę [ServerDao] z wersji 08. Klasa [ServerDao] potrafi bowiem korzystać z bazy danych. Pozostaje nam tylko uwzględnić przypadek, w którym dane z urzędu skarbowego zostały już pobrane:
  • wiersz 10: funkcja otrzymuje teraz dwa parametry:
    • [string $databaseFilename]: nazwa pliku zawierającego informacje umożliwiające połączenie z bazą danych, jeśli dane z urzędu skarbowego nie zostały jeszcze pobrane, w przeciwnym razie NULL;
    • [TaxAdminData $taxAdminData]: dane z urzędu skarbowego, jeśli zostały już pobrane, w przeciwnym razie NULL;

Podczas uruchamiania sesji internetowej warstwa [dao] zostanie utworzona z obiektem [$databaseFilename] (nie NULL) oraz obiektem [taxAdminData] (NULL). Dane urzędu skarbowego zostaną wówczas wyszukane w bazie i zapisane w sesji. Podczas kolejnych zapytań w ramach tej samej sesji warstwa [dao] zostanie utworzona z obiektem [databaseFilename], NULL oraz obiektem [taxAdminData] pochodzącym z sesji, a nie z obiektem NULL. Nie nastąpi zatem żadne wyszukiwanie w bazie danych.

19.1.3. Skrypt serwera

Skrypt serwera [impots-server.php] jest skonfigurowany przez następujący plik: jSON [config-server.json]:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
    "databaseFilename": "Data/database.json",
    "relativeDependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Entities/TaxAdminData.php",
        "/../version-08/Entities/Database.php",
        "/../version-08/Dao/InterfaceServerDao.php",
        "/../version-08/Dao/ServerDao.php",
        "/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Data/logs.txt"
}

Skrypt serwera [impots-server.php] zmienia się 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");

// pobieranie konfiguracji
$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";
}
//
// zależności Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

// sesja
$session = new Session();
$session->start();

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

// tworzenie pliku logów
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // wewnętrzny błąd serwera
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // zakończono
  exit;
}

// pierwszy wpis w dzienniku
$logger->write("\n---nouvelle requête\n");

// pobieranie bieżącego żądania
$request = Request::createFromGlobals();
// uwierzytelnianie tylko przy pierwszym uruchomieniu
if (!$session->has("user")) {
  // log
  $logger->write("Autentification en cours…\n");
  // uwierzytelnianie

  }
  // czy znaleziono użytkownika?
  if (!$trouvé) {
    // nie znaleziono – kod 401 HTTP_UNAUTHORIZED
    sendResponse(
      $response,
      ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
      Response::HTTP_UNAUTHORIZED,
      ["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
      $logger
    );
    // zakończono
    exit;
  } else {
    // w sesji odnotowano uwierzytelnienie użytkownika
    $session->set("user", TRUE);
    // log
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // log
  $logger->write("Authentification prise en session…\n");
}
// mamy prawidłowego użytkownika – sprawdzamy otrzymane parametry
$erreurs = [];
// powinny być trzy parametry GET


// błędy?
if ($erreurs) {
// wysyłamy do klienta kod błędu 400 HTTP_BAD_REQUEST
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // zakończono
  exit;
} else {
  // logi
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// mamy wszystko, co potrzebne do pracy
// tworzenie warstwy [dao]
if (!$session->has("taxAdminData")) {
  // dane są pobierane z bazy danych
  $logger->write("données fiscales prises en base de données\n");
  try {
    // tworzenie warstwy [dao]
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    // dane są umieszczane w sesji
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // odnotowuje się błąd
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // zakończono
    exit;
  }
} else {
  // dane zostały pobrane z sesji
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // logi
  $logger->write("données fiscales prises en session\n");
}
// tworzenie warstwy [métier]
$métier = new ServerMetier($dao);
// obliczenie podatku
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// przekazanie odpowiedzi
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
// koniec
exit;

function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // wysyłanie wiadomości e-mail do administratora
  // SendAdminMail przechwytuje wszystkie wyjątki i samodzielnie je rejestruje
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // wysyłamy kod błędu 500 do klienta
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}

// funkcja wysyłania odpowiedzi HTTP do klienta
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $response: odpowiedź HTTP
  // $result: tabela wyników
  // $statusCode: status odpowiedzi HTTP
  // $headers: nagłówki HTTP do umieszczenia w odpowiedzi
  // $logger: moduł rejestrujący aplikacji
  //
  // status HTTTP
  $response->setStatusCode($statusCode);
  // treść
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // nagłówki
  $response->headers->add($headers);
  // wysyłanie
  $response->send();
  // log
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
}

Komentarze

  • wiersze 34–35: rozpoczyna się sesja;
  • wiersze 38–40: przygotowuje się odpowiedź jSON;
  • wiersze 42–50: próba utworzenia pliku logów. W przypadku wystąpienia wyjątku wywoływana jest metoda [doInternalServer] (wiersze 132–140);
  • wiersz 132: metoda [doInternalServer] przyjmuje cztery parametry:
    • [$message]: komunikat do zarejestrowania w dzienniku. Musi być zakodowany w formacie UTF-8;
    • [$response]: obiekt [Response], który zawiera odpowiedź serwera skierowaną do klienta;
    • [$logger]: obiekt [Logger] służący do tworzenia logów;
    • [$infos]: informacje umożliwiające wysłanie wiadomości e-mail do administratora aplikacji;
  • wiersze 135–137: wysyłamy wiadomość e-mail do administratora aplikacji;
  • wiersz 139: wysyłana jest odpowiedź do klienta:
    • $response: odpowiedź HTTP;
    • $result: serwer wysyła ciąg jSON z tablicy [‘réponse’=>["erreur" => $message]];
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], kod 500;
    • $headers: [], brak nagłówków HTTP do dodania do odpowiedzi;
    • $logger: moduł logowania aplikacji;
  • wiersz 58: dzięki utworzonej sesji uwierzytelnianie klienta nastąpi tylko raz:
    • po uwierzytelnieniu klienta do sesji zostanie dodany klucz [user] (wiersz 78);
    • przy kolejnym żądaniu tego samego klienta wiersz 58 pozwala uniknąć niepotrzebnego uwierzytelniania;
  • wiersz 103: dzięki utworzonej sesji dane z bazy będą wyszukiwane tylko raz:
    • przy pierwszym żądaniu zostanie przeprowadzone wyszukiwanie w bazie danych (wiersz 108). Pobrane dane są następnie umieszczane w sesji (wiersz 110) i powiązane z kluczem [taxAdminData];
    • przy kolejnych żądaniach klucz [taxAdminData] zostanie znaleziony w sesji (wiersz 103), a następnie dane z dysku zostaną bezpośrednio przekazane do warstwy [dao] (wiersz 119);
  • wiersze 111–116: wyszukiwanie danych podatkowych w bazie może zakończyć się niepowodzeniem. W takim przypadku do klienta wysyłany jest kod [500 Internal Server Error];
  • wiersz 113: komunikat o błędzie związany z wyjątkiem sterownika MySQL jest zakodowany jako ISO 8859-1. Przekształca się go na UTF-8, aby został poprawnie zarejestrowany w dzienniku;
  • pozostała część kodu jest niemal identyczna jak w poprzedniej wersji;
  • wiersze 143–164: funkcja [sendResponse] wysyła wszystkie odpowiedzi do klienta;
  • wiersze 144–148: znaczenie parametrów;
  • wiersz 153: odpowiedzią jest zawsze ciąg znaków jSON z tablicy [‘résultat’=>qqChose];
  • wiersz 156: czasami do odpowiedzi należy dodać nagłówki HTTP. Tak jest w wierszu 71;
  • wiersz 158: odpowiedź zostaje wysłana;
  • wiersze 160–163: odpowiedź jest rejestrowana w dzienniku, a moduł rejestrujący jest zamykany;

19.1.4. Testy [Codeception]

Image

Przetestujemy tylko warstwę [dao], która jako jedyna uległa zmianie.

Kod testu [ServerDaoTest] jest następujący:


<?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-09");
// ś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";
}

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

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

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

  // testy
  public function testTaxAdminData() {

  }

}
  • wiersze 9–24: tworzymy środowisko wykonawcze identyczne z tym ze skryptu serwerowego [impots-server];
  • wiersz 38: w celu utworzenia warstwy [dao] instancjonuje się klasę [ServerDaoWithSession];

Wynik testów jest następujący:

Image

19.2. Klient

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

Image

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

Image

W nowej wersji zmieniają się jedynie:

  • plik konfiguracyjny [config-client.json];
  • warstwa kliencka [dao];

19.2.1. warstwa [dao]

Warstwa [Dao] ulega następującym zmianom:


<?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;
  private $sessionCookie;

  // 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 {
    // plik cookie sesji?
    if (!$this->sessionCookie) {
      // tworzymy klienta HTTP
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // wysyłamy żądanie do serwera bez pliku cookie sesji
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // wysyłamy żądanie do serwera z plikiem cookie sesji
      // tworzymy klienta HTTP
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // odbieramy odpowiedź
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // logi
    print "$json=json\n";
    // pobierany jest status odpowiedzi
    $statusCode = $response->getStatusCode();
    // błąd?
    if ($statusCode !== 200) {
      // wystąpił błąd – generowany jest wyjątek
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    if (!$this->sessionCookie) {
      // pobieramy plik cookie sesji
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // plik cookie sesji?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // zwracamy odpowiedź
    return $réponse;
  }

}

Komentarze

Modyfikacja warstwy [dao] polega na dodaniu obsługi sesji:

  • wiersz 14: plik cookie sesji;
  • wiersze 25–39: podczas pierwszego żądania ten plik cookie nie istnieje: wówczas wysyłamy żądanie do serwera, przekazując dane uwierzytelniające (wiersz 28);
  • wiersze 40–53: przy kolejnych żądaniach zazwyczaj dysponujemy plikiem cookie sesji. Nie wysyłamy więc danych uwierzytelniających (wiersze 42–44);
  • wiersze 69–82: odpowiedź serwera na pierwsze żądanie będzie zawierała plik cookie sesji. Pobieramy go. Kod ten został już wykorzystany i wyjaśniony w akapicie „link”;
  • wiersz 78: pobrany plik cookie sesji jest zapisywany w atrybucie klasy [$sessionCookie];

Uwaga: można było zachować poprzednią wersję warstwy [dao] i przeprowadzać uwierzytelnianie przy każdym żądaniu, ponieważ wiąże się to z nieznacznym kosztem. Ze względów dydaktycznych chcieliśmy jednak przypomnieć, w jaki sposób klient HTTP może zarządzać sesją.

19.2.2. Plik konfiguracyjny

Plik konfiguracyjny jSON zmienia się w następujący sposób:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/TaxPayerData.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Utilities/Utilitaires.php",
        "/../version-08/Dao/InterfaceClientDao.php",
        "/../version-08/Dao/TraitDao.php",
        "/Dao/ClientDao.php",
        "/../version-08/Métier/InterfaceClientMetier.php",
        "/../version-08/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-09/impots-server.php"
}

Zmienia się jedynie element URL w wierszu 24.

19.3. Kilka testów

19.3.1. Test 1

Najpierw uruchamiamy klienta w środowisku bez błędów. Wyniki są takie same jak w poprzednich wersjach. Jednak teraz po stronie serwera mamy plik dziennika o nazwie [logs.txt]:


04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête

  • wiersze 3–7: podczas pierwszego żądania następuje uwierzytelnienie i wyszukiwanie danych w bazie;
  • wiersze 9–14: podczas kolejnego żądania nie następuje już uwierzytelnianie, a dane są pobierane z sesji. Sytuacja ta powtarza się podczas kolejnych żądań (wiersze 15 i kolejne);

19.3.2. Test 2

Teraz wyłączmy bazę danych MySQL. Po stronie klienta otrzymujemy następujący wynik w konsoli:


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é

Po stronie serwera pojawiają się następujące logi [logs.txt]:


04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête

Aby uzyskać wiadomość e-mail otrzymaną przez administratora aplikacji, używamy skryptu [imap-03.php] z akapitu dotyczącego powiązania z następującym plikiem konfiguracyjnym [config-imap-01.json]:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Otrzymujemy następujący wynik:

Image

Plik [message_1.txt] zawiera następujący tekst:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.

19.3.3. Test 3

Teraz sprawmy, aby plik [logs.txt] nie mógł zostać utworzony. W tym celu wystarczy utworzyć folder o nazwie [logs.txt]:

Image

Po wykonaniu tej czynności uruchommy klienta.

Po stronie klienta otrzymujemy następujące wyniki w konsoli:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé

Po stronie serwera nie ma żadnych logów, ale administrator otrzymuje następującą wiadomość e-mail:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

Echec lors de la création du fichier de logs [Data/logs.txt]

19.3.4. Test 4

Tym razem w pliku konfiguracyjnym klienta podajmy błędne dane uwierzytelniające dla łączącego się klienta.

Klient wyświetla następujące wyniki w konsoli:


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

Po stronie serwera pojawiają się następujące logi:


---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}

19.3.5. Test 5

Wprowadźmy ponownie prawidłowy identyfikator użytkownika [admin, admin] do pliku konfiguracyjnego klienta.

Teraz spróbujmy uzyskać dostęp do serwera URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] bezpośrednio w przeglądarce, bez podawania parametrów:

W pliku logów serwera [logs.txt] znajdują się następujące wiersze:


---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}

19.4. Testy [Codeception]

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

Image

19.4.0.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-09");

// ś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";
}
//
// używa
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;

// klasa testowa
class ClientMetierTest extends Unit {
  
}

Uwagi

  • w porównaniu z klasą testową wersji 08 zmienia się jedynie wiersz 10, który określa katalog główny testowanego klienta;

Wyniki testu są następujące:

Image

Warto zapoznać się z logami serwera [logs.txt]:


04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête

---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}

Można zauważyć, że dane urzędu skarbowego są zawsze pobierane z bazy danych, a nigdy z sesji. Wróćmy do kodu przeprowadzonego testu:


<?php

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

// przestrzeń nazw
namespace Application;



// klasa testowa
class ClientMetierTest extends Unit {
  // warstwa biznesowa
  private $métier;

  public function __construct() {
    parent::__construct();
    // pobieranie konfiguracji
    $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 test2() {

  }

  public function test3() {

  }



}

W klasie testowej [Codeception] konstruktor jest wywoływany dla każdego testu.

  • wiersz 21: dla każdego testu tworzony jest zatem nowy obiekt [ClientDao] z plikiem cookie sesji NULL. To wyjaśnia, dlaczego ten klient nie korzysta z żadnej sesji;

Ten przykład pokazuje, że sesja nie jest odpowiednim miejscem do przechowywania danych administracji podatkowej. Dane te są bowiem wspólne dla wszystkich użytkowników aplikacji. Tutaj jednak są one powielane w każdej z ich sesji.

W programowaniu internetowym rozróżnia się trzy rodzaje widoczności danych współdzielonych:

  • dane współdzielone przez wszystkich użytkowników aplikacji internetowej. Są to zazwyczaj dane tylko do odczytu. PHP nie dysponuje natywnie taką pamięcią;
  • dane współdzielone przez żądania tego samego klienta. Dane te są przechowywane w sesji. Mówimy wówczas o sesji klienta, aby określić pamięć klienta. Wszystkie żądania danego klienta mają dostęp do tej sesji. Mogą w niej przechowywać i odczytywać informacje. W powyższych skryptach sesja ta jest zaimplementowana przez obiekt Symfony [HttpFoundation\Session\Session];
  • pamięć żądania lub kontekst żądania. Żądanie użytkownika może być przetwarzane przez kilka kolejnych akcji. Kontekst żądania pozwala akcji 1 przekazać informacje do akcji 2. W poprzednich skryptach żądanie jest zaimplementowane przez obiekt Symfony [HttpFoundation\Request], a jego pamięć przez atrybut [HttpFoundation\Request::attributes];

Image

Istnieją biblioteki zewnętrzne, które zapewniają obiektowi PHP pamięć aplikacji. Nowa wersja ćwiczenia praktycznego pokazuje wykorzystanie jednej z nich.