Skip to content

11. Ćwiczenie praktyczne – wersja 4

Aplikacja do obliczania podatków będzie wykorzystywać następującą strukturę warstwową:

Image

Wykorzystamy elementy z wersji 3 opisu linku, modyfikując je tak, aby dostosować je do nowej architektury aplikacji. Czasami nazywa się to „refaktoryzacją”. Zakładamy tutaj, że dane niezbędne dla aplikacji znajdują się w plikach tekstowych. Warstwa [Dao] będzie odpowiedzialna za wymianę danych z tymi plikami.

11.1. Struktura drzewa skryptów

Image

11.2. Obiekty wymieniane między warstwami

Zachowamy niektóre obiekty z wersji 3. Przedstawiamy je tutaj dla przypomnienia.

Wyjątek [ExceptionImpots] to wyjątek, który zostanie wygenerowany przez warstwę [Dao] w przypadku napotkania problemu związanego z dostępem do danych lub z ich charakterem (nieprawidłowe dane).


<?php

// przestrzeń nazw
namespace Application;

class ExceptionImpots extends \RuntimeException {

  public function __construct(string $message, int $code=0) {
    parent::__construct($message, $code);
  }
}

Klasa [Utilitaires] zawiera metody przydatne do zarządzania plikami tekstowymi (w tym przypadku jest to jedna metoda):


<?php

// przestrzeń nazw
namespace Application;

// klasa funkcji pomocniczych
abstract class Utilitaires {

  public static function cutNewLinechar(string $ligne): string {
    // usuwa się znacznik końca linii z $ligne, jeśli istnieje
    $longueur = strlen($ligne);  // długość wiersza
    while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
      $ligne = substr($ligne, 0, $longueur - 1);
      $longueur--;
    }
    // koniec – zwracamy wiersz
    return($ligne);
  }
}

Klasa [TaxAdminData] jest klasą, która hermetyzuje dane administracji podatkowej:


<?php

namespace Application;

class TaxAdminData {
  // przedziały podatkowe
  private $limites;
  private $coeffR;
  private $coeffN;
  // stałe do obliczania podatku
  private $plafondQfDemiPart;
  private $plafondRevenusCelibatairePourReduction;
  private $plafondRevenusCouplePourReduction;
  private $valeurReducDemiPart;
  private $plafondDecoteCelibataire;
  private $plafondDecoteCouple;
  private $plafondImpotCouplePourDecote;
  private $plafondImpotCelibatairePourDecote;
  private $abattementDixPourcentMax;
  private $abattementDixPourcentMin;

  // inicjalizacja
  public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
    // pobieranie zawartości pliku danych podatkowych
    $fileContents = \file_get_contents($taxAdminDataFilename);

    // zwracamy obiekt
    return $this;
  }

  private function check($value): \stdClass {

    return $result;
  }

    // toString
  public function __toString() {
    // ciąg JSON obiektu
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

  // metody pobierające i ustawiające
  public function getLimites() {
    return $this->limites;
  }



  public function setLimites($limites) {
    $this->limites = $limites;
    return $this;
  }


}

Dodajemy nową klasę [TaxPayerData], która zawiera dane zapisane w pliku wyników:


<?php

// przestrzeń nazw
namespace Application;

// klasa danych
class TaxPayerData {
  // dane niezbędne do obliczenia podatku podatnika
  private $marié;
  private $enfants;
  private $salaire;
  // wyniki obliczeń podatku
  private $montant;
  private $surcôte;
  private $décôte;
  private $réduction;
  private $taux;

  // setter
  public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel) : TaxPayerData{
    // dane podatnika niezbędne do obliczenia podatku
    $this->marié = $marié;
    $this->enfants = $nbEnfants;
    $this->salaire = $salaireAnnuel;
    // zwracamy zainicjowany obiekt
    return $this;
  }

  // metody pobierające i ustawiające
  public function getMarié() {
    return $this->marié;
  }



  public function setMarié($marié) {
    $this->marié = $marié;
    return $this;
  }



    // toString
  public function __toString() {
    // ciąg JSON obiektu
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

}

Uwaga: należy skorzystać z automatycznego generowania kodu, aby wygenerować konstruktor, metody getter i setter (patrz akapit z linkiem). Należy zwrócić uwagę, że metody setter są typu „fluent”.

11.3. Warstwa [dao]

W tym miejscu skupiamy się na warstwie [1] naszej aplikacji:

Image

11.3.1. Interfejs [InterfaceDao]

Interfejs warstwy [dao] będzie wyglądał następująco: [InterfaceDao.php]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceDao {

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

  // odczyt danych z urzędu skarbowego (przedziały podatkowe)
  public function getTaxAdminData(): TaxAdminData;

  // zapisywanie wyników
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

Uwagi

  • Specyfikacja jest następująca:
    • dane podatników znajdują się w pliku tekstowym;
    • wyniki obliczeń podatkowych zapisuje się w pliku tekstowym;
    • ewentualne błędy zapisuje się w pliku tekstowym;
    • nie wiadomo, w jakiej formie dostępne są dane urzędu skarbowego. Dla każdego nowego formatu interfejs [InterfaceDao] musi zostać zaimplementowany przez nową klasę;
    • metody interfejsu, które napotkają nieodwracalny błąd podczas uzyskiwania dostępu do danych, muszą zgłaszać wyjątek typu [ExceptionImpots];
  • wiersz 9: metoda umożliwiająca pobranie danych podatnika [statut marital, nombre d’enfants, salaire annuel];
    • pierwszy parametr to nazwa pliku tekstowego, w którym znajdują się te dane;
    • drugim parametrem jest nazwa pliku tekstowego, w którym mają zostać zapisane ewentualne błędy;
  • wiersz 12: metoda umożliwiająca uzyskanie danych z urzędu skarbowego. Nie podaje się tu żadnego parametru, ponieważ nie wiadomo, w jaki sposób dane te są przechowywane;
  • wiersz 15: metoda umożliwiająca zapisanie wyników obliczeń podatku w pliku tekstowym, którego nazwę podaje się jako parametr;

Podczas pisania interfejsu [InterfaceDao] wiadomo, że będą istniały różne sposoby implementacji metody [getTaxAdminData] w zależności od sposobu przechowywania danych przez administrację podatkową. Interfejs [InterfaceDao] będzie zatem zaimplementowany przez różne klasy, z których każda będzie obsługiwać konkretny sposób przechowywania tych danych (tabele, pliki tekstowe, baza danych, usługa internetowa). Te klasy pochodne będą jednak miały wspólny kod, czyli kod implementacji metod [getTaxPayersData, saveResults]. Wiadomo, że ten przypadek użycia można zaimplementować na dwa sposoby (patrz akapit „link”):

  1. tworzy się klasę abstrakcyjną C, która zawiera kod wspólny dla klas pochodnych. Klasa C implementuje interfejs I, ale niektóre metody, które muszą zostać zadeklarowane w klasach pochodnych, są w klasie C zadeklarowane jako abstrakcyjne, a zatem sama klasa C jest klasą abstrakcyjną. Następnie tworzy się klasy C1 i C2, pochodne od klasy C, z których każda na swój sposób implementuje metody niezdefiniowane (abstrakcyjne) swojej klasy nadrzędnej C;
  2. tworzymy cechę T niemal identyczną z klasą abstrakcyjną C z poprzedniego rozwiązania. Cecha ta nie implementuje interfejsu I, ponieważ z punktu widzenia składni nie jest to możliwe. Następnie tworzymy klasy C1 i C2, które implementują interfejs I i wykorzystują cechę T. Klasom tym pozostaje jedynie zaimplementować metody interfejsu I, które nie zostały zaimplementowane przez importowaną przez nie cechę T;

W tym przykładzie wykorzystamy cechę [TraitDao].

11.3.2. Cecha [TraitDao]

Kod cechy [TraitDao] jest następujący: [TraitDao.php]:


<?php

// przestrzeń nazw
namespace Application;

trait TraitDao {

  // odczyt danych podatników
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
    // tabela danych podatników
    $taxPayersData = [];
    // tabela błędów
    $errors = [];
    // podczas obsługi plików może wystąpić sporo błędów
    try {
      // odczyt danych użytkownika
      // każdy wiersz ma następujący format: stan cywilny, liczba dzieci, roczne wynagrodzenie
      $taxPayersFile = fopen($taxPayersFilename, "r");
      if (!$taxPayersFile) {
        throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
      }
      // przetwarzamy bieżący wiersz pliku danych użytkownika
      // która ma postać: stan cywilny, liczba dzieci, roczne wynagrodzenie
      $num = 1;         // numer bieżącego wiersza
      $nbErreurs = 0;   // liczba napotkanych błędów
      while ($ligne = fgets($taxPayersFile, 100)) {
        // puste wiersze są pomijane
        $ligne = trim($ligne);
        if (strlen($ligne) == 0) {
          // następny wiersz
          $num++;
          // powrót do początku
          continue;
        }
        // usuwamy ewentualny znak końca wiersza
        $ligne = Utilitaires::cutNewLineChar($ligne);
        // pobieramy 3 pola „żonaty:dzieci:wynagrodzenie”, które tworzą $ligne
        list($marié, $enfants, $salaire) = explode(",", $ligne);
        // sprawdzamy je
        // stan cywilny musi wynosić „tak” lub „nie”
        $marié = trim(strtolower($marié));
        $erreur = ($marié !== "oui" and $marié !== "non");
        if (!$erreur) {
          // liczba dzieci musi być liczbą całkowitą
          $enfants = trim($enfants);
          if (!preg_match("/^\d+$/", $enfants)) {
            $erreur = TRUE;
          } else {
            $enfants = (int) $enfants;
          }
        }
        if (!$erreur) {
          // wynagrodzenie jest liczbą całkowitą bez centów euro
          $salaire = trim($salaire);
          if (!preg_match("/^\d+$/", $salaire)) {
            $erreur = TRUE;
          } else {
            $salaire = (int) $salaire;
          }
        }
        // błąd?
        if ($erreur) {
          $errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
          $nbErreurs++;
        } else {
          // zapisujemy informacje
          $taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
        }
        // następny wiersz
        $num++;
      }
      // czy jesteśmy na końcu pliku?
      if (!feof($taxPayersFile)) {
        // wyjście z pętli z powodu błędu odczytu
        throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
      } else {
        // wyjście z pętli po napotkaniu znacznika końca pliku
        // błędy są zapisywane w pliku tekstowym
        $this->saveString($errorsFilename, implode("\n", $errors));
        // wynik funkcji
        return $taxPayersData;
      }
    } finally {
      // plik zostaje zamknięty, jeśli jest otwarty
      if ($taxPayersFile) {
        fclose($taxPayersFile);
      }
    }
  }

  // zapisywanie wyników
  public function saveResults(string $resultsFilename, array $taxPayersData): void {
    // zapisywanie tablicy [$taxPayersData] w pliku tekstowym [$resultsFileName]
    // jeśli plik tekstowy [$resultsFileName] nie istnieje, zostanie utworzony
    $this->saveString($resultsFilename, implode("\n", $taxPayersData));
  }

  // zapis wyników tabeli w pliku tekstowym
  private function saveString(string $fileName, string $data): void {
    // zapis tabeli [$data] w pliku tekstowym [$fileName]
    // jeśli plik tekstowy [$fileName] nie istnieje, zostanie utworzony
    if (file_put_contents($fileName, $data) === FALSE) {
      throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
    }
  }

}

Uwagi

  • wiersz 6: definiujemy tutaj cechę, a nie klasę;
  • wiersze 9–89: metoda [getTaxPayersData] implementuje metodę o tej samej nazwie z interfejsu [InterfaceDao]. Pobiera ona z pliku tekstowego o nazwie [$taxPayersFilename] dane podatników [statut marital, nombre d’enfants, salaire annuel]. Przekazuje je w postaci tablicy o nazwie [$taxPayersData], zawierającej elementy typu [TaxPayerData] (wiersze 67, 81);
  • metoda [getTaxPayersData] jest bardzo podobna do metody [AbstractBaseImpots::executeBatchImpots] opisanej w akapicie „link”, z następującymi różnicami:
    • metoda [getTaxPayersData] służy wyłącznie do pobierania danych podatników. Nie dokonuje ona obliczeń podatku. Rolę tę pełni tutaj warstwa [métier];
    • podobnie jak metoda [executeBatchImpots], zgłasza ona błędy. Błędy są tu najpierw zapisywane w tablicy [$errors] (wiersz 13), która pod koniec przetwarzania jest zapisywana w pliku tekstowym (wiersz 79). W zależności od sytuacji tablica ta może być pusta lub nie;
    • w przypadku błędu nieodwracalnego generowany jest wyjątek typu [ExceptionImpots] (wiersze 20, 75);
  • wiersz 73: należy zwrócić uwagę na operację wykonywaną po wyjściu z pętli w wierszach 26–71. W rzeczywistości funkcja [fgets] ma tę wadę, że zwraca wartość logiczną FALSE zarówno wtedy, gdy odczyt wierszy natrafił na znacznik końca pliku, jak i wtedy, gdy odczyt ten nie powiódł się z powodu błędu. Aby odróżnić te dwa przypadki, za pomocą funkcji [feof] sprawdzamy, czy dotarliśmy do końca pliku. Jeśli nie dotarliśmy do końca pliku, oznacza to, że wystąpił błąd i wówczas generujemy wyjątek;
  • wiersze 83–88: funkcja [finally] jest wykonywana niezależnie od tego, czy podczas przetwarzania pliku wystąpił wyjątek, czy nie;
  • wiersz 85: jeśli plik został otwarty, to „uchwyt” [$taxPayersFile] tego pliku przyjmuje wartość logiczną TRUE, w przeciwnym razie – FALSE;
  • wiersze 99–105: prywatna metoda [saveString], wykorzystana w wierszu 79 do zapisania tablicy błędów w pliku tekstowym;
  • wiersz 99: metoda [saveString] przyjmuje dwa parametry:
    • [string $filename], czyli nazwa pliku tekstowego używanego do zapisywania danych;
    • [string $data], czyli ciąg znaków, który ma zostać zapisany w pliku tekstowym. Ciąg ten będzie zbiorem wierszy zakończonych znakiem końca wiersza \n;
  • wiersz 102: funkcja PHP [file_puts_contents] zapisuje ciąg znaków w pliku tekstowym. Odpowiada ona za otwarcie pliku, zapisanie w nim ciągu znaków oraz zamknięcie pliku. Zwraca wartość logiczną FALSE, jeśli wystąpił błąd;
  • wiersz 103: jeśli wystąpi błąd, zgłaszany jest wyjątek;
  • wiersze 92–96: implementacja metody [saveResults] z interfejsu [InterfaceDao]. Ponownie wykorzystuje się metodę prywatną [saveString]. W tym przypadku drugim parametrem metody [saveString] jest ciąg znaków utworzony na podstawie tablicy [$taxPayersData], której elementy są typu [TaxPayerData]. Można się zastanawiać, jaki będzie wynik tej operacji:

implode("\n", $taxPayersData)

W klasie [TaxPayerData] (akapit „link”) zdefiniowaliśmy następującą metodę [__toString]:


public function __toString() {
    // ciąg JSON obiektu
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

Operacja


implode("\n", $taxPayersData)

połączy każdy element tablicy [$taxPayersData], przekształcony w ciąg znaków za pomocą metody [__toString], ze znakiem końca linii \n. W rezultacie powstanie ciąg znaków o postaci:

json1\njson2\n…

Wniosek

Klasa [TraitDao] zaimplementowała dwie metody interfejsu [InterfaceDao], [getTaxPayersData] oraz [saveResults]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceDao {

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

  // odczyt danych z urzędu skarbowego (progi podatkowe)
  public function getTaxAdminData(): TaxAdminData;

  // zapisywanie wyników
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

Pozostaje nam jeszcze zaimplementować metodę [getTaxAdminData], która pobiera dane z urzędu skarbowego.

11.3.3. Klasa [ImpotsWithTaxAdminDataInJsonFile]

Klasa [ImpotsWithTaxAdminDataInJsonFile] implementuje interfejs [InterfaceDao] w następujący sposób:


<?php

// przestrzeń nazw
namespace Application;

// definicja klasy ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
  // użycie cechy
  use TraitDao;
  // obiekt typu TaxAdminData zawierający dane dotyczące przedziałów podatkowych
  private $taxAdminData;

  // konstruktor
  public function __construct(string $taxAdminDataFilename) {
    // chcemy zainicjować atrybut [$this->taxAdminData]
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
  }

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

Komentarze

  • wiersz 7: klasa [ImpotsWithTaxAdminDataInJsonFile] implementuje interfejs [InterfaceDao];
  • wiersz 9: klasa [ImpotsWithTaxAdminDataInJsonFile] wykorzystuje cechę [traitDao], o której wiadomo, że implementuje metody [getTaxPayersData] i [saveResults] zinterfejsu [InterfaceDao]. Klasie [ImpotsWithTaxAdminDataInJsonFile] pozostaje zatem jedynie zaimplementować metodę [getTaxAdminData], która pobiera dane z urzędu skarbowego;
  • wiersz 11: atrybut typu [TaxAdminData] zwracany przez metodę [getTaxAdminData] z wierszy 20–22. Atrybut ten jest inicjowany przez konstruktor z wierszy 14–17;

Zakończyliśmy pracę nad warstwą [dao] naszej aplikacji: mamy klasę, która w pełni implementuje interfejs [InterfaceDao], który sobie wyznaczyliśmy. Możemy teraz przejść do warstwy [métier].

11.4. Warstwa [métier]

Teraz zaimplementujemy warstwę [2] naszej architektury:

Image

11.4.1. Interfejs [InterfaceMétier]

Interfejs warstwy [métier] będzie wyglądał następująco:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceMetier {

  // 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;
}

Uwagi

  • wiersz 9: interfejs [InterfaceMétier] potrafi obliczyć kwotę podatku dla konkretnego podatnika, pod warunkiem podania następujących informacji: stan cywilny, liczba dzieci, roczne wynagrodzenie. Metoda [calculerImpot] nie korzysta z warstwy [dao], więc nie generuje wyjątków;
  • wiersz 9: interfejs [InterfaceMétier] może również obliczyć kwotę podatku dla grupy podatników, których dane są zebrane w pliku tekstowym o nazwie [$taxPayersFileName]. Wyniki zapisuje w pliku tekstowym o nazwie [$resultsFileName]. Metoda [executeBatchImpots] musi zwracać się do warstwy [dao], która odpowiada za dostęp do systemu plików. Wyjątki mogą wówczas być przekazywane z warstwy [dao], których metoda [executeBatchImpots] nie przechwytuje: pozwala ona na ich przekazanie do głównego skryptu. Błędy niekrytyczne są rejestrowane w pliku tekstowym o nazwie [$errorsFileName];
  • wiersz 9: metoda [calculerImpot] jest metodą czysto [métier]. Nie zajmuje się ona pochodzeniem wykorzystywanych danych;
  • wiersz 12: metoda [executeBatchImpots] zwraca się do warstwy [dao] w celu odczytu i zapisu danych w plikach tekstowych. Będzie ona wielokrotnie wywoływać metodę biznesową [calculerImpot];

11.4.2. Klasa [Metier]

Klasa [Metier] implementuje interfejs [InterfaceMetier] w następujący sposób:


<?php

// przestrzeń nazw
namespace Application;

class Metier implements InterfaceMetier {
  // warstwa DAO
  private $dao;
  // dane administracji podatkowej
  private $taxAdminData;

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

  public function __construct(InterfaceDao $dao) {
    // zapisuje się odniesienie w warstwie [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);
  }

  // obliczanie podatków w trybie wsadowym
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {

    // zapis wyników
    $this->dao->saveResults($resultsFileName, $results);
  }

}

Komentarze

  • wiersz 6: klasa [Metier] implementuje interfejs [InterfaceMetier], tj. metody [calculerImpot] (wiersze 30–34) oraz [executeBatchImpots] (wiersze 66–70);
  • wiersz 8: odwołanie do warstwy [dao]. Jest to absolutnie konieczne, aby warstwa [métier] wiedziała, do kogo się zwrócić, gdy potrzebuje danych zewnętrznych. Atrybut ten zostanie zainicjowany za pomocą metody setter w wierszach 14–17 lub za pomocą konstruktora w wierszach 19–26;
  • wiersz 10: obiekt typu [TaxAdminData], który zawiera dane administracji podatkowej. Dane te są niezbędne dla metody biznesowej [calculerImpot]. Atrybut ten jest inicjowany za pomocą konstruktora w wierszach 19–26;
  • wiersze 19–26: konstruktor inicjuje dwa atrybuty klasy:
    • atrybut [$dao] jest inicjowany przy użyciu odwołania przekazanego jako parametr do konstruktora. Należy zauważyć, że typ tego parametru jest zgodny z typem interfejsu [InterfaceDao], co pozwala na zainicjowanie klasy [Metier] przez dowolną klasę implementującą ten interfejs;
    • atrybut [$taxAdminData] jest inicjowany poprzez wywołanie metody [getTaxAdminData] warstwy [dao];

Można zatem stwierdzić, że podczas wykonywania metod [calculerImpots] i [executeBatchImpots] inicjowane są oba atrybuty: [$dao] i [$taxAdminData].

Metoda [calculerImpots] ma następującą postać:


public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié: tak, nie
    // $enfants: liczba dzieci
    // $salaire: roczne wynagrodzenie
    // $this->taxAdminData: dane z urzędu skarbowego
    //
    // sprawdzamy, czy posiadamy dane z urzędu skarbowego
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // obliczenie podatku z uwzględnieniem dzieci
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // obliczenie podatku bez uwzględnienia dzieci
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // zastosowanie limitu ilorazu rodzinnego
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euro na pierwsze dwoje dzieci, dwukrotność tej kwoty na kolejne
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // bierzemy wyższą stawkę podatku
    if ($impot1 > $impot2) {
      $impot = $impot1;
      $taux = $result1["taux"];
      $surcôte = $result1["surcôte"];
    } else {
      $surcôte = $impot2 - $impot1 + $result2["surcôte"];
      $impot = $impot2;
      $taux = $result2["taux"];
    }
    // obliczenie ewentualnej ulgi
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // obliczenie ewentualnej ulgi podatkowej
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // wynik
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

Uwagi

  • kod ten pochodzi z metody [AbstractBaseImpots::calculerImpot] z wersji 3, opisanej w akapicie „link”. To samo dotyczy metod prywatnych [calculerImpot2, getDecôte, getRéduction, getRevenuImposable];

Metoda [Metier::executeBatchImpots] ma następujący kod:


public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // pozwala na przekazywanie wyjątków pochodzących z warstwy [dao]
    // pobieramy dane podatników
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabela wyników
    $results = [];
    // przetwarzamy je
    foreach ($taxPayersData as $taxPayerData) {
      // oblicza się podatek
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // uzupełniamy [$taxPayerData]
      $taxPayerData->setMontant($result["impôt"]);
      $taxPayerData->setDécôte($result["décôte"]);
      $taxPayerData->setSurCôte($result["surcôte"]);
      $taxPayerData->setTaux($result["taux"]);
      $taxPayerData->setRéduction($result["réduction"]);
      // wpisuje się wynik do tabeli wyników
      $results [] = $taxPayerData;
    }
    // zapisywanie wyników
    $this->dao->saveResults($resultsFileName, $results);
  }

Uwagi

  • wiersz 1: metoda musi wielokrotnie wywoływać metodę [calculerImpot] dla każdego podatnika znalezionego w pliku tekstowym o nazwie [$taxPayersFileName]. Wyniki musi zapisać w pliku tekstowym o nazwie [$resultsFileName]. Występujące błędy niekrytyczne są rejestrowane w pliku tekstowym o nazwie [$errorsFileName]. Metoda ta sama nie generuje wyjątków, ale pozwala na przekazywanie tych, które generuje warstwa [dao];
  • wiersz 4: dane podatników są pobierane z warstwy [dao]. Warstwa ta zwraca tablicę elementów typu [TaxPayerData], który jest klasą atrybutów [marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux] (patrz akapit „link”). Jeśli w tym miejscu wystąpi wyjątek, a nie zostanie on przechwycony przez catch, zostanie on automatycznie przekazany do kodu wywołującego. Oznacza to, że w przypadku wyjątku wiersz 6 nie zostanie wykonany;
  • wiersz 6: tablica wyników typu [TaxPayerData];
  • wiersze 8–22: oblicza się podatek dla każdego elementu tablicy podatników [$taxPayersData]. W tym celu wywoływana jest metoda wewnętrzna [calculerImpot] (wiersz 10);
  • wiersze 15–19: uzyskany wynik służy do zainicjowania atrybutów tabeli [TaxPayerData], które nie zostały jeszcze zainicjowane;
  • wiersz 21: uzyskany wynik jest sumowany w tablicy wyników [$results];
  • wiersz 24: po obliczeniu podatku dla wszystkich podatników wyniki są zapisywane w pliku tekstowym. Zadanie to realizuje warstwa [dao];

Wniosek

Ogólnie rzecz biorąc, warstwa [métier] jest dość prosta do napisania, ponieważ odwołuje się do warstwy [dao], która z kolei zarządza dostępem do danych wraz z towarzyszącym mu zarządzaniem błędami.

11.5. Skrypt główny

Teraz napiszemy skrypt warstwy [3] naszej architektury:

Image

Główny skrypt o nazwie [main.php] wygląda następująco:


<?php

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

// przestrzeń nazw
namespace Application;

// obsługa błędów za pomocą PHP
//ini_set("display_errors", "0");

// dołączanie interfejsów i klas
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// definicja stałych
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // utworzenie warstwy [dao]
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // tworzenie warstwy [métier]
  $métier = new Metier($dao);
  // obliczanie podatku w trybie wsadowym
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // wyświetlenie błędu
  print $ex->getMessage() . "\n";
}
// koniec
print "Terminé\n";
exit;


Komentarze

  • wiersz 24: nazwa pliku z danymi podatników;
  • wiersz 25: nazwa pliku wyników;
  • wiersz 26: nazwa pliku z błędami;
  • wiersz 27: nazwa pliku jSON zawierającego dane administracji podatkowej;
  • wiersz 31: utworzenie warstwy [dao];
  • wiersz 33: utworzenie warstwy [métier] opartej na warstwie [dao];
  • wiersz 35: wykonanie metody [executeBatchImpots] warstwy [métier];
  • wiersze 36–39: jak już wspomniano, warstwa [métier] może zgłaszać wyjątki. Są one tutaj przechwytywane;

11.6. Testy wzrokowe

11.6.1. Test nr 1

Z plikiem podatników o nazwie [taxpayersdata.txt] zawierającym następujące dane:


oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000

otrzymujemy następujący plik błędów [errors.txt]:


la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée

oraz plik wyników o nazwie [resultats.txt]:

1
2
3
4
5
6
7
8
9
{"marié":"oui","enfants":2,"salaire":55555,"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"oui","enfants":2,"salaire":50000,"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":3,"salaire":50000,"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
{"marié":"non","enfants":2,"salaire":100000,"impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":3,"salaire":100000,"impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"marié":"non","enfants":0,"salaire":100000,"impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":2,"salaire":30000,"impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}
{"marié":"non","enfants":0,"salaire":200000,"impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}
{"marié":"oui","enfants":3,"salaire":200000,"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}

11.6.2. Test nr 2

W skrypcie głównym nadajemy plikowi podatników nazwę, która nie istnieje:

const TAXPAYERS_DATA_FILENAME = "taxpayersdata2.txt";

Wyniki wyświetlane w konsoli są wówczas następujące:


Warning: fopen(taxpayersdata2.txt): failed to open stream: No such file or directory in C:\Data\st-2019\dev\php7\poly\scripts-console\impots\version-04\TraitDao.php on line 18
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
Done.
  • wiersz 1: ostrzeżenia (warning) interpretera PHP;
  • wiersz 2: komunikat o błędzie wyjątku wygenerowanego przez warstwę [dao];

Można wyciszyć komunikaty o błędach interpretera PHP:

Image

Wiersz 21 powyższego kodu powoduje, że błędy PHP nie są wyświetlane. Podczas fazy rozwoju konieczne jest ich wyświetlanie. W trybie produkcyjnym należy je ukryć.

Wyniki wykonania są następujące:


Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé

11.7. Testy [Codeception]

Testy wizualne są zdecydowanie niewystarczające:

  • zazwyczaj ograniczamy się do kilku testów;
  • podczas tej weryfikacji wizualnej poświęca się mniej lub bardziej uwagi, przez co mogą umknąć nam pewne szczegóły;

W rzeczywistości zawodowej testy są tworzone przez wyspecjalizowane osoby, dla których jest to główny zakres obowiązków. Starają się one wówczas przeprowadzić testy tak kompleksowo, jak to tylko możliwe. W tym celu korzystają z frameworków testowych.

W tym przypadku wykorzystamy framework Codeception [https://codeception.com/], ponieważ można go zintegrować z NetBeans. Jest to framework oferujący szeroki wachlarz możliwości. Wykorzystamy jednak tylko niektóre z nich. Chodzi o to, aby po każdej nowej wersji ćwiczenia aplikacyjnego mieć szybki sposób na sprawdzenie, czy działa ona poprawnie. Pomyślne wyniki testów dają programiście pewność co do napisanego przez niego kodu. Jest to ważny czynnik.

11.7.1. Instalacja frameworka [Codeception]

Podobnie jak wiele bibliotek PHP, framework [Codeception] instaluje się wraz z [Composer]. Otwieramy więc terminal Laragon (patrz akapit „link”).

Najpierw musimy zainstalować framework testowy PHPUnit [https://phpunit.de/]. Codeception wykorzystuje bowiem w tle framework PHPUnit:

Image

Następnie instalujemy framework Codeception:

Image

To wszystko. Teraz przyjrzyjmy się integracji [Codeception] z NetBeans.

11.7.2. Integracja [CodeCeption] z NetBeans

Image

  • w [1-2] otwieramy właściwości projektu;
  • w [3-4] ustalamy [Codeception] jako jeden z frameworków testowych projektu;

Image

Image

  • W pliku [5-8] inicjuje się framework [Codeception] dla projektu;

Image

  • w pliku [9] utworzono folder [tests] oraz plik konfiguracyjny [codeception.yml] w pliku [10-11]. Plik [11] jest taki sam jak plik [10]. Codeception po prostu utworzył folder [Important Files], aby nadać szczególne znaczenie plikowi [10];
  • w pliku [12-13] powracamy do właściwości projektu;

Image

  • w pliku [14-16] folder [tests] [16] jest oznaczony jako folder testowy projektu;
  • w [16] folder [tests] pojawia się wówczas pod nową nazwą [Test Files]. Obecność tego folderu w projekcie PHP wskazuje, że projekt ten zawiera framework do testów programowych;
  • nasze testy utworzymy w folderze [unit] [17];

11.7.3. Testy warstwy [dao]

Image

  • wszystkie nasze testy utworzymy w folderze [unit] [1];
  • nazwy klas testowych [Codeception] muszą kończyć się słowem kluczowym [Test], w przeciwnym razie klasy te nie zostaną rozpoznane jako klasy testowe;

Nasze klasy testowe [Codeception] będą miały następującą postać: [https://codeception.com/docs/05-UnitTests]:


<?php

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

// przestrzeń nazw
namespace Application;

// ładowanie środowiska testowego


class DaoTest extends \Codeception\Test\Unit {
  // atrybuty testu
  private $attribut1;

  public function __construct() {
    parent::__construct();
    // inicjalizacja środowiska testowego

  }

  // testy
  public function testTaxAdminData() {
    // testy
    $this->assertEquals($expected, $actual);
    $this->assertEqualsWithDelta($expected, $actual, $delta);
    $this->assertTrue($actual);
    $this->assertFalse($actual);
    $this->assertNull($actual);
    $this->assertEmpty($actual);
    $this→assertSame($expected, $actual);

  }

}

Uwagi

  • wiersz 7: klasy testowe będą znajdować się w tej samej przestrzeni nazw co testowana aplikacja;
  • wiersze 9–10: tutaj znajdują się operacje [require] służące do ładowania testowanych klas i interfejsów;
  • wiersz 12: nazwa klasy testowej musi koniecznie kończyć się słowem kluczowym [Test]. Klasa ta musi dziedziczyć po klasie [\Codeception\Test\Unit];
  • wiersze 16–20: konstruktor pozwoli nam zainicjować środowisko testowe;
  • wiersz 23: nazwy metod testowych muszą obowiązkowo zaczynać się od słowa kluczowego [test];
  • wiersze 25–31: można wykorzystać różne metody testowe;

Klasa testowa [DaoTest] 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;

// stałe
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// dołączanie interfejsów i klas
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// definicja stałych
const TAXADMINDATA_FILENAME = "taxadmindata.json";

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

  public function __construct() {
    parent::__construct();
    // tworzenie warstwy [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // testy
  public function testTaxAdminData() {

  }
}

Komentarze

Aby stworzyć testy dla danej wersji ćwiczenia aplikacyjnego, wykorzystamy środowisko identyczne z tym, z którego korzysta główny skrypt tej wersji. W przypadku wersji 04 jest to następujący skrypt [main.php]:


<?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");

// dołączanie interfejsów i klas
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// definicja stałych
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // tworzenie warstwy [dao]
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // tworzenie warstwy [métier]
  $métier = new Metier($dao);
  // obliczanie podatku w trybie wsadowym
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // wyświetlenie błędu
  print $ex->getMessage() . "\n";
}
// koniec
print "Terminé\n";
exit;

Aby przetestować warstwę [dao] w klasie testowej:

  • przejmujemy środowisko z wierszy 13–27 skryptu [main.php];
  • w konstruktorze klasy testowej tworzymy warstwę [dao] tak jak w wierszu 31;
  • piszemy metody testowe;

Postępujemy w ten sposób dla wszystkich klas testowych.

Wróćmy do pełnego kodu klasy testowej:


<?php

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

// przestrzeń nazw
namespace Application;

// stałe
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// dołączanie interfejsów i klas
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// definicja stałych
const TAXADMINDATA_FILENAME = "taxadmindata.json";

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

  public function __construct() {
    parent::__construct();
    // tworzenie warstwy [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // testy
  public function testTaxAdminData() {
    // stałe obliczeniowe
    $this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
    $this->assertEquals(21037, $this->taxAdminData->getPlafondRevenusCelibatairePourReduction());
    $this->assertEquals(42074, $this->taxAdminData->getPlafondRevenusCouplePourReduction());
    $this->assertEquals(3797, $this->taxAdminData->getValeurReducDemiPart());
    $this->assertEquals(1196, $this->taxAdminData->getPlafondDecoteCelibataire());
    $this->assertEquals(1970, $this->taxAdminData->getPlafondDecoteCouple());
    $this->assertEquals(1595, $this->taxAdminData->getPlafondImpotCelibatairePourDecote());
    $this->assertEquals(2627, $this->taxAdminData->getPlafondImpotCouplePourDecote());
    $this->assertEquals(12502, $this->taxAdminData->getAbattementDixPourcentMax());
    $this->assertEquals(437, $this->taxAdminData->getAbattementDixPourcentMin());
    // progi podatkowe
    $this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $this->taxAdminData->getLimites());
    $this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $this->taxAdminData->getCoeffR());
    $this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $this->taxAdminData->getCoeffN());
  }

}

Komentarze

  • wiersze 10–25: ładowanie środowiska niezbędnego do przeprowadzenia testów oraz definicje stałych;
  • wiersze 31–36: utworzenie warstwy [dao] (wiersz 34), a następnie zainicjowanie atrybutu [$taxAdminData] z wiersza 29. Atrybut ten zawiera dane urzędu skarbowego;
  • wiersze 39–55: jedyna metoda testowa. Polega ona na sprawdzeniu, czy zawartość atrybutu [$taxAdminData] odpowiada oczekiwanej wartości;
  • wiersze 41–50: weryfikacja stałych stosowanych w obliczeniach podatku;
  • wiersze 52–55: weryfikacja przedziałów podatkowych. Metoda [assertSame] sprawdza, czy dwa obiekty PHP, w tym przypadku tabele, są identyczne;

Aby uruchomić tę klasę testową, należy postępować w następujący sposób:

Image

  • w [1-2] uruchamia się test;
  • [3]: okno wyników testów;
  • [4]: uruchomiona klasa testowa;
  • [5]: wyniki. Tutaj jedyna metoda testowa zakończyła się powodzeniem;
  • [6]: gdy test zakończy się niepowodzeniem lub, co zdarza się częściej, gdy nie przeprowadzono żadnego testu, należy przejść do okna [6]. Najczęściej przyczyną jest niepowodzenie w załadowaniu środowiska testowego, w wyniku czego nie można było przeprowadzić żadnego testu. Błędy wyświetlane w oknie [6] są takie same, jak te, które pojawiłyby się podczas wykonywania klasycznego skryptu PHP;

Przedstawmy przykład błędnego testu:

W klasie testowej wprowadzamy błąd w definicji stałej:


// stałe
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");

a następnie uruchamiamy test. Otrzymujemy następujący wynik:

Image

W oknie [4]:

Image

11.7.4. Testy warstwy [métier]

Klasa testowa [MetierTest] jest zbudowana zgodnie z tymi samymi zasadami co klasa [DaoTest], ale zawiera więcej metod testowych:


<?php

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

// przestrzeń nazw
namespace Application;

// stałe
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// dołączanie interfejsów i klas
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// definicja stałych
const TAXADMINDATA_FILENAME = "taxadmindata.json";

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

  public function __construct() {
    parent::__construct();
    // tworzenie warstwy [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    // tworzenie warstwy [métier]
    $this->métier = new Metier($dao);
  }

  // testy
  public function test1() {
    $result = $this->métier->calculerImpot("oui", 2, 55555);
    $this->assertEqualsWithDelta(2815, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test2() {
    $result = $this->métier->calculerImpot("oui", 2, 50000);
    $this->assertEqualsWithDelta(1385, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(384, $result["décôte"], 1);
    $this->assertEqualsWithDelta(347, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test3() {
    $result = $this->métier->calculerImpot("oui", 3, 50000);
    $this->assertEqualsWithDelta(0, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(720, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test4() {
    $result = $this->métier->calculerImpot("non", 2, 100000);
    $this->assertEqualsWithDelta(19884, $result["impôt"], 1);
    $this->assertEqualsWithDelta(4480, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test5() {
    $result = $this->métier->calculerImpot("non", 3, 100000);
    $this->assertEqualsWithDelta(16782, $result["impôt"], 1);
    $this->assertEqualsWithDelta(7176, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test6() {
    $result = $this->métier->calculerImpot("oui", 3, 100000);
    $this->assertEqualsWithDelta(9200, $result["impôt"], 1);
    $this->assertEqualsWithDelta(2180, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.3, $result["taux"]);
  }

  public function test7() {
    $result = $this->métier->calculerImpot("oui", 5, 100000);
    $this->assertEqualsWithDelta(4230, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test8() {
    $result = $this->métier->calculerImpot("non", 0, 100000);
    $this->assertEqualsWithDelta(22986, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test9() {
    $result = $this->métier->calculerImpot("oui", 2, 30000);
    $this->assertEqualsWithDelta(0, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0, $result["taux"]);
  }

  public function test10() {
    $result = $this->métier->calculerImpot("non", 0, 200000);
    $this->assertEqualsWithDelta(64210, $result["impôt"], 1);
    $this->assertEqualsWithDelta(7498, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.45, $result["taux"]);
  }

  public function test11() {
    $result = $this->métier->calculerImpot("oui", 3, 200000);
    $this->assertEqualsWithDelta(42842, $result["impôt"], 1);
    $this->assertEqualsWithDelta(17283, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }
}

Komentarze

  • wiersze 10–25: wczytywanie plików definiujących środowisko testowe. Jest ono takie samo jak dla warstwy [dao];
  • wiersze 31–37: instancjonowanie warstw [dao] i [métier];
  • wiersze 40–47: test obliczania podatku;
  • wiersz 41: przeprowadzono pewną kalkulację podatku przy użyciu warstwy [métier];
  • wiersze 42–46: sprawdzamy, czy uzyskane wyniki są zgodne z wynikami symulatora urzędu skarbowego [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm];
  • wiersze 23–26: testy zgodności przeprowadzono z dokładnością do 1 euro. Stwierdzono bowiem, że problemy z zaokrągleniem powodowały, iż algorytm z dokumentu dawał oczekiwane wyniki z dokładnością do 1 euro;
  • wiersz 27: stawka podatkowa jest obliczana bez marginesu błędu;
  • wiersze 49–137: tego typu testy powtarza się 10 razy, za każdym razem dla innego profilu podatnika;

Testy dają następujące wyniki:

Image

11.7.5. Testy kolejnych wersji

W dalszej części testy warstw [dao] i [métier] będą identyczne jak w wersji 04. Zmieni się jedynie środowisko testowe. Przedstawimy zatem wyłącznie to środowisko oraz wyniki testów.