Skip to content

8. Ćwiczenie praktyczne – wersja 3

Wracamy do ćwiczenia omówionego wcześniej (punkty 4.3 i 4.4), aby rozwiązać je za pomocą kodu PHP wykorzystującego klasę.

8.1. Struktura drzewa skryptów

Image

8.2. Wyjątek [ExceptionImpots]

W wersji 03, gdy konstruktor lub metoda klasy napotka błąd, wygeneruje wyjątek typu [ExceptionImpots] o następującej treści:

<?php

// przestrzeń nazw
namespace Application;

class ExceptionImpots extends \RuntimeException {

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

}

Komentarze

  • wiersz 4: klasa [ExceptionImpots] znajduje się w przestrzeni nazw [Application];
  • wiersz 6: klasa [ExceptionImpots] dziedziczy po klasie predefiniowanej w PHP [RuntimeException];
  • wiersz 8: konstruktor oczekuje dwóch parametrów:
    • $message: to komunikat o błędzie powiązany z wyjątkiem;
    • $code: to kod błędu powiązany z wyjątkiem. Jeśli nie występuje, zostanie użyty kod 0;

8.3. Klasa [TaxAdminData]

W wersji 02 dane administracji podatkowej zostały zebrane:

  • najpierw w pliku jSON;
  • następnie z tego pliku jSON do tablicy asocjacyjnej;

W wersji 03 dane administracji podatkowej nadal znajdują się w pliku [taxadmindata.json], ale mają inne nazwy atrybutów:


{
    "limites": [
        9964,
        27519,
        73779,
        156244,
        0
    ],
    "coeffR": [
        0,
        0.14,
        0.3,
        0.41,
        0.45
    ],
    "coeffN": [
        0,
        1394.96,
        5798,
        13913.69,
        20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
}

W wersji 02 plik ten służył do zainicjowania tablicy asocjacyjnej. W wersji 03 plik ten zainicjuje następującą klasę [TaxAdminData]:


<?php

namespace Application;

class TaxAdminData {
  // przedziały podatkowe
  private $limites;
  private $coeffR;
  private $coeffN;
  // stałe służące 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);
    $erreur = FALSE;
    // błąd?
    if (!$fileContents) {
      // odnotowuje się błąd
      $erreur = TRUE;
      $message = "Le fichier des données [$taxAdminDataFilename] n'existe pas";
    }
    if (!$erreur) {
      // pobieranie kodu jSON z pliku konfiguracyjnego do tablicy asocjacyjnej
      $arrayTaxAdminData = \json_decode($fileContents, true);
      // błąd?
      if ($arrayTaxAdminData === FALSE) {
        // odnotowuje się błąd
        $erreur = TRUE;
        $message = "Le fichier de données jSON [$taxAdminDataFilename] n'a pu être exploité correctement";
      }
    }
    // błąd?
    if ($erreur) {
      // rzucamy wyjątek
      throw new ExceptionImpots($message);
    }
    // inicjalizacja atrybutów klasy
    foreach ($arrayTaxAdminData as $key => $value) {
      $this->$key = $value;
    }
    // sprawdzamy, czy wszystkie klucze zostały zainicjowane
    $arrayOfAttributes = \get_object_vars($this);
    foreach ($arrayOfAttributes as $key => $value) {
      if (!isset($this->$key)) {
        throw new ExceptionImpots("L'attribut [$key] de [TaxAdminData] n'a pas été initialisé");
      }
    }
    // sprawdzanie, czy wszystkie wartości są liczbami rzeczywistymi
    foreach ($this as $key => $value) {
      // $value musi być liczbą rzeczywistą >=0 lub tablicą liczb rzeczywistych >=0
      $result = $this->check($value);
      // błąd?
      if ($result->erreur) {
        // rzucamy wyjątek
        throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
      } else {
        // zapisuje się wartość
        $this->$key = $result->value;
      }
    }
    // 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 getCoeffR() {
    return $this->coeffR;
  }


  }

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

  public function setCoeffR($coeffR) {
    $this->coeffR = $coeffR;
    return $this;
  }



}

Komentarze

  • wiersze 6–20: atrybuty, które będą przechowywać atrybuty o tej samej nazwie z plików jSON i [taxadmindata.json]. Jest to ważna kwestia: atrybuty klasy [TaxAdminData] identyczne z atrybutami plików jSON i [taxadmindata.json]. Ta cecha znacznie ułatwia pisanie kodu;
  • klasa [TaxAdminData] nie posiada konstruktora. W PHP nie ma możliwości posiadania wielu konstruktorów. Zdefiniowanie jednego z nich uniemożliwia zatem zainicjowanie obiektu w inny sposób. W dalszej części nasze klasy nie będą miały konstruktora, ale kilka metod typu [setFromQqChose], które pozwolą na ich inicjalizację na różne sposoby. Tworzenie obiektu typu [TaxAdminData] odbywa się zatem za pomocą wyrażenia:
(new TaxAdminData())→setFromQqChose(…)
  • wiersz 23: metoda [setFromJsonFile] inicjuje atrybuty klasy, przypisując im wartości o tej samej nazwie z pliku [$jsonFilename];
  • wiersze 24–42: plik jSON jest wykorzystywany do utworzenia tablicy asocjacyjnej [$arrayTaxAdminData]. Kod ten pojawił się już wcześniej w skrypcie [main.php] z wersji 02;
  • wiersze 44–47: jeśli podczas przetwarzania pliku jSON wystąpił błąd, generowany jest wyjątek. Zostanie on przekazany do głównego skryptu [main.php];
  • wiersze 48–51: inicjowane są atrybuty klasy. Wykorzystujemy tutaj fakt, że tablica asocjacyjna [$arrayTaxAdminData] oraz klasa [TaxAdminData] mają atrybuty o takich samych nazwach jak wartości pochodzące z pliku jSON;
  • wiersze 53–57: sprawdzamy, czy wszystkie atrybuty klasy [TaxAdminData] zostały zainicjowane;
  • wiersz 53: wyrażenie [get_object_vars($this)] zwraca tablicę asocjacyjną, której atrybuty są atrybutami obiektu [$this], a zatem atrybutami klasy [TaxAdminData]. Należy tu pamiętać, że operacja inicjalizacji w wierszach 48–51 mogła dodać atrybuty do obiektu [$this]. Jeśli więc napiszemy:
    $this->x = "1000";

to atrybut [x] zostanie dodany do obiektu [$this], nawet jeśli atrybut ten nie został zadeklarowany w klasie [TaxAdminData]. Pewne jest, że atrybuty z wierszy 6–20 rzeczywiście należą do obiektu [$this], ale mogły nie zostać zainicjowane. Łatwo popełnić taki błąd – wystarczy pomylić nazwę atrybutu w pliku [taxadmindata.json];

  • wiersze 54–57: przeglądamy wszystkie atrybuty obiektu [$this] i jeśli któryś z nich nie został zainicjowany, generujemy wyjątek;
  • atrybut może zostać zainicjowany nieprawidłową wartością. W pliku PHP nie ma możliwości przypisania typu do atrybutów. Dlatego operacja:
$this→plafondQfDemiPart=’abcd’

jest możliwa, podczas gdy atrybut [$plafondQfDemiPart] powinien być typu rzeczywistego;

  • wiersze 59–71: sprawdza się, czy każdy z atrybutów klasy ma wartość liczbową typu realnego, dodatnią lub zerową. Zadanie to realizuje funkcja [check] w wierszu 76. Jej parametr [$value] jest albo pojedynczą wartością, albo tablicą wartości;
  • wiersz 62: funkcja [check] zwraca obiekt typu [\stdClass] z dwoma atrybutami:
    • [erreur]: przyjmuje wartość TRUE, jeśli wystąpił błąd, w przeciwnym razie przyjmuje wartość FALSE;
    • [value]: rzeczywista wartość liczbowa odpowiadająca parametrowi [$value] przekazanemu jako parametr, wiersz 62;
  • wiersz 64: sprawdzamy, czy weryfikacja zakończyła się powodzeniem, czy nie;
  • wiersz 66: jeśli atrybut nie jest liczbą rzeczywistą dodatnią lub zerem, zgłaszamy wyjątek;
  • wiersz 69: w przeciwnym razie zapisujemy jego wartość liczbową;
  • wiersz 73: zwracamy obiekt [$this] jako wynik;

Funkcja [check] wygląda następująco:


private function check($value): \stdClass {
    // $value jest albo tablicą elementów, albo pojedynczym elementem
    // tworzy się tablicę
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // przekształca się tablicę elementów o nieznanym typie w tablicę liczb rzeczywistych
    $newTableau = [];
    $result = new \stdClass();
    // elementy tablicy muszą być dodatnimi liczbami dziesiętnymi lub zerami
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // umieszczamy liczbę zmiennoprzecinkową w newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // odnotowuje się błąd
        $result->erreur = TRUE;
        // kończymy
        return $result;
      }
    }
    // zwracamy wynik
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // pojedyncza wartość
      $result->value = $newTableau[0];
    } else {
      // lista wartości
      $result->value = $newTableau;
    }
    return $result;
  }

Komentarze

  • wiersz 1: parametr [$value] jest albo tablicą, albo pojedynczym elementem. Ponadto nie znamy jego typu. Wartość pochodzi z pliku [taxadmindata.json]. W zależności od wartości zapisanych w tym pliku odczytane wartości mogą być liczbami całkowitymi, liczbami rzeczywistymi, ciągami znaków lub wartościami logicznymi. Na przykład:

"plafondQfDemiPart": 1551,
"plafondQfDemiPart": 1551.78,
"plafondQfDemiPart": "1551",
"plafondQfDemiPart": "xx",

W przypadku 1 wartość jest typu [entier], w przypadku 2 typu [réel], w przypadku 3 ma typ [string] i może zostać przekonwertowana na liczbę, w przypadku 4 ma typ [string] i nie może zostać przekonwertowana na liczbę;

  • wiersze 4–8: tworzymy tablicę na podstawie parametru [$value] otrzymanego w wierszu 1;
  • wiersz 10: tablica, którą wypełnimy liczbami rzeczywistymi;
  • wiersz 11: wynikiem będzie obiekt typu [\stdClass];
  • wiersz 13: wyrażenie relacyjne określające liczbę rzeczywistą dodatnią lub zerową;
  • wiersze 14–24: sprawdzamy, czy wszystkie elementy tablicy [$tableau] są liczbami rzeczywistymi dodatnimi lub zerowymi, a następnie wypełniamy tablicę [$newTableau] tymi elementami przekształconymi na typ [float] (wiersz 17);
  • wiersze 18–23: gdy tylko wykryty zostanie element, który nie jest liczbą rzeczywistą dodatnią lub zerem, odnotowuje się błąd w wyniku i zwraca się ten wynik;
  • wiersze 25–34: przypadek, w którym wszystkie elementy tablicy [$tableau] zostały uznane za poprawne;
  • wiersz 32: zwracana wartość [$result→value] jest tablicą liczb rzeczywistych [float] lub pojedynczą liczbą rzeczywistą;

Funkcja [__toString] w wierszach 82–85 zwraca ciąg znaków jSON zawierający atrybuty i wartości obiektu [$this].

Wiersze 87–110: metody getter i setter klasy;

Uwaga: czasami pisanie wszystkich metod get/set dla klasy może być nieco uciążliwe, zwłaszcza gdy jest wiele atrybutów. NetBeans może automatycznie wygenerować te metody, a także konstruktor. W tym celu wystarczy podać atrybuty [1]:

Image

  • w [2], kliknij prawym przyciskiem myszy w miejscu, w którym chcesz wstawić kod, a następnie wybierz opcję [Insert Code];

Image

  • na [4], zaznacz, że chcesz wygenerować konstruktor;
  • w [5] zaznacz wszystkie atrybuty: oznacza to, że chcesz, aby konstruktor miał parametr dla każdego z atrybutów;
  • w [6] należy zastosować styl konstruktorów języka Java;
  • w [7] należy zaznaczyć, że wyraźnie chcesz, aby przed konstruktorem znajdowało się słowo kluczowe [public];
  • w [8] zatwierdź;

Image

  • w [9] program NetBeans wygenerował konstruktor. Nie był jednak w stanie określić typów parametrów, ponieważ ich nie zna. Dodaj je samodzielnie: [10];

Aby wygenerować metody getter i setter, powtórz kroki 2–4, a w kroku 4 wybierz [Getter and Setter]:

Image

  • w [5] należy zaznaczyć, że potrzebne są metody getter i setter dla każdego z atrybutów;
  • w [6] należy wskazać, że chcesz, aby metody getter i setter były zgodne ze stylem stosowanym w Javie: setAttribut, getAttribut;
  • w pliku [7] należy wskazać, że te metody getter i setter mają być publiczne;
  • w pliku [8] zatwierdź zmiany;

Image

  • w pliku [9], metody getter i setter wygenerowane przez NetBeans;

Usuń te metody getter i setter i powtórz kroki 2–7.

  • w pliku [8] zaznacz opcję [Fluent Setter], której wcześniej nie zaznaczyliśmy;

Otrzymany wynik jest następujący:

Image

Każda metoda setter kończy się operacją [return $this]. Pozwala to zainicjować atrybuty w następujący sposób:

$data→setLimites($limites)→setCoeffR($coeffR)→setCoeffN($coeffN) ;

W rzeczywistości wartość [$data→setLimites($limites)] (wiersz 32 kodu) wynosi [$this], a zatem w tym przypadku [$data]. Można zatem wywołać metodę [setCoeffR($coeffR)] tego obiektu i tak dalej, ponieważ ta metoda z kolei zwraca [$this] (wiersz 37 kodu). Taki sposób definiowania metod klasy, w wyniku którego metody, które nie powinny zwracać żadnej wartości, zwracają obiekt [$this], nazywa się pisaniem płynnym. Ułatwia to korzystanie z tych metod.

8.4. Interfejs [InterfaceImpots]

Teraz definiujemy następujący interfejs [InterfaceImpots]: [InterfaceImpots.php]:


<?php

// przestrzeń nazw
namespace Application;

interface InterfaceImpots {

  // pobieranie danych dotyczących przedziałów podatkowych umożliwiających obliczenie podatku
  // może wywołać wyjątek ExceptionImpots
  public function getTaxAdminData(): TaxAdminData;

  // interfejs potrafi obliczyć podatek
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // interfejs potrafi przetwarzać dane z plików tekstowych
  // $usersFilename: plik danych użytkownika zawierający stan cywilny, liczbę dzieci oraz roczne wynagrodzenie
  // $resultsFilename: plik wyników zawierający stan cywilny, liczbę dzieci, roczne wynagrodzenie oraz kwotę podatku
  // $errorsFilename: plik zawierający wykaz napotkanych błędów
  // może wywołać wyjątek ExceptionImpots
  public function executeBatchImpots(string $usersFileName, string $resultsFileName, string $errorsFileName): void;
}

Komentarze

  • wiersz 4: interfejs znajduje się w przestrzeni nazw [Application];
  • wiersz 6: interfejs umożliwiający obliczanie podatków;
  • wiersz 10: metoda [getTaxAdminData] umożliwi pobranie danych z urzędu skarbowego do obiektu typu [TaxAdminData], który właśnie przedstawiliśmy. Ponieważ dane te mogą znajdować się w pliku, bazie danych, a nawet w sieci, metoda [getTaxAdminData] może nie uzyskać danych. W takim przypadku wygeneruje wyjątek typu [ExceptionImpots]. Jest to standardowa metoda w programowaniu obiektowym służąca do zgłaszania błędu napotkanego w metodzie lub konstruktorze;
  • wiersz 13: metoda [calculerImpot] pozwoli obliczyć podatek dla jednego użytkownika;
  • wiersz 20: metoda [executeBatchImpots] służy do obliczania podatku dla wielu podatników:
    • [$usersFileName] to nazwa pliku tekstowego zawierającego dane podatników;
    • [$resultsFileName] to nazwa pliku tekstowego zawierającego kwotę podatku dla tych podatników;
    • [$errorsFileName] to nazwa pliku tekstowego zawierającego błędy napotkane podczas przetwarzania tych plików;

Zawartość pliku tekstowego [$usersFileName] może wyglądać następująco:


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

Należy zauważyć, że wiersze 5 i 7 zawierają błędne dane.

Zawartość pliku tekstowego [$resultsFileName] będzie zatem następująca:

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}

a zawartość pliku tekstowego [$errorsFileName] będzie następująca:

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

8.5. Klasa [Utilitaires]

Ponadto definiujemy klasę [Utilitaires] w pliku [Utilitaires.php]:


<?php

// przestrzeń nazw
namespace Application;

// klasa funkcji pomocniczych
abstract class Utilitaires {

  public static function cutNewLinechar(string $ligne): string {
    // usuwamy 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);
  }
}

Komentarze

  • wiersz 4: klasa [Utilitaires] jest również umieszczona w przestrzeni nazw [Exemples];
  • wiersz 9: metoda [cutNewLinechar] usuwa ewentualny znak końca linii z tekstu przekazanego jej jako parametr. Zwraca w ten sposób utworzony nowy wiersz. Należy zauważyć, że jest to metoda statyczna, co oznacza, że będzie wywoływana w postaci [Utilitaires::cutNewLineChar];

8.6. Klasa abstrakcyjna [AbstractBaseImpots]

Interfejs [InterfaceImpots] zostanie zaimplementowany przez następującą klasę abstrakcyjną [AbstractBaseImpots]: [AbstractBaseImpots.php]:


<?php

// przestrzeń nazw
namespace Application;

// definicja klasy abstrakcyjnej AbstractBaseImpots
abstract class AbstractBaseImpots implements InterfaceImpots {
  // dane urzędu skarbowego
  private $taxAdminData = NULL;

  // dane niezbędne do obliczenia podatku
  abstract function getTaxAdminData(): TaxAdminData;

// obliczenie podatku
// --------------------------------------------------------------------------
  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, dwukrotnie więcej na kolejne
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // bierzemy wyższy podatek
    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];
  }

// --------------------------------------------------------------------------
  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
  // ulga ma dolną i górną granicę
  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ą redukcję
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {

    // wynik
    return ceil($réduction);
  }

  public function executeBatchImpots(string $usersFileName, string $resultsFileName, string $errorsFileName): void {

  }

}

Komentarze

  • wiersz 4: klasa [AbstractBaseImpots] będzie znajdować się w przestrzeni nazw [Application], podobnie jak pozostałe elementy tworzonej aplikacji;
  • wiersz 7: klasa [AbstractBaseImpots] implementuje interfejs [InterfaceImpots];
  • wiersz 9: dane urzędu skarbowego zostaną umieszczone w atrybucie [$taxAdminData];
  • wiersz 12: implementacja metody [getTaxAdminData] z interfejsu. Nie wiemy jeszcze, jak zdefiniować tę metodę: w poprzednim akapicie widzieliśmy przykład, w którym dane z urzędu skarbowego zostały pobrane z pliku jSON. Zobaczymy jeszcze jeden przypadek, w którym dane będą pobierane z bazy danych. Zadaniem klas pochodnych będzie zdefiniowanie treści metody [getTaxAdminData]. Oba poprzednie przypadki doprowadzą do powstania dwóch klas pochodnych. Metoda [getTaxAdminData] jest zatem zadeklarowana jako abstrakcyjna, co automatycznie sprawia, że sama klasa staje się abstrakcyjna (wiersz 7);
  • wiersze 15–64: funkcja obliczania podatku, którą znamy już z akapitów link i link;
  • wersja 02 umieszczała dane urzędu skarbowego w tablicy asocjacyjnej [$taxAdminData]. Wersja 03 umieszcza je w atrybucie [$this→taxAdminData]. Pierwszą różnicą między tymi dwoma rozwiązaniami jest różnica w widoczności danych podatkowych:
    • w wersji 02 tablica asocjacyjna [$taxAdminData] nie miała widoczności globalnej. Była zatem przekazywana jako parametr do wszystkich funkcji obliczających podatek;
    • w wersji 03 atrybut [$this→taxAdminData] ma widoczność globalną dla wszystkich metod klasy. Nie jest więc przekazywany jako parametr do wszystkich funkcji obliczających podatek;
  • druga różnica wynika z faktu, że w wersji 03 funkcje zostały zastąpione metodami klasy. Każde wywołanie metody odbywa się teraz za pomocą wyrażenia [$this→getMéthode(…)] (wiersze 27, 31, 57, 60);
  • trzecią różnicą jest to, że gdy metoda [calculerImpot] rozpoczyna działanie, nie wie, czy potrzebny jej atrybut [private $taxAdminData] został zainicjowany. Konstruktor klasy bowiem go nie inicjuje. Zadaniem metody [calculerImpot] jest zatem zainicjowanie go za pomocą metody [getTaxAdminData] z wiersza 12. Odbywa się to w wierszach 23–25;
  • poza tymi różnicami metody obliczania podatku pozostają takie same jak w poprzednich wersjach;

Funkcja [executeBatchImpots] wygląda następująco:


public function executeBatchImpots(string $usersFileName, string $resultsFileName, string $errorsFileName): void {
    // podczas pracy z plikami może wystąpić sporo błędów
    try {
      // błędy podczas otwierania pliku
      $errors = fopen($errorsFileName, "w");
      if (!$errors) {
        throw new ExceptionImpots("Impossible de créer le fichier des erreurs [$errorsFileName]", 10);
      }
      // otwieranie pliku wyników
      $results = fopen($resultsFileName, "w");
      if (!$results) {
        throw new ExceptionImpots("Impossible de créer le fichier des résultats [$resultsFileName]", 11);
      }
      // odczyt danych użytkownika
      // każdy wiersz ma następujący format: stan cywilny, liczba dzieci, roczne wynagrodzenie
      $data = fopen($usersFileName, "r");
      if (!$data) {
        throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$usersFileName]", 12);
      }
      // przetwarzamy bieżący wiersz z 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($data, 100)) {
        // debug
        //  print „wiersz nr ” . ($i + 1) . „: ” . $ligne;
        // usuwamy ewentualny znak końca linii
        $ligne = Utilitaires::cutNewLineChar($ligne);
        // pobieramy 3 pola: żona: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("/^\s*\d+\s*$/", $enfants)) {
            $erreur = TRUE;
          } else {
            $enfants = (int) $enfants;
          }
        }
        if (!$erreur) {
          // wynagrodzenie jest liczbą całkowitą bez centów euro
          $salaire = trim($salaire);
          if (!preg_match("/^\s*\d+\s*$/", $salaire)) {
            $erreur = TRUE;
          } else {
            $salaire = (int) $salaire;
          }
        }
        // błąd?
        if ($erreur) {
          fputs($errors, "la ligne [$num] du fichier [$usersFileName] est erronée\n");
          $nbErreurs++;
        } else {
          // obliczamy podatek
          $result = $this->calculerImpot($marié, (int) $enfants, (int) $salaire);
          // wynik zapisuje się w pliku wyników
          $result = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $result;
          fputs($results, \json_encode($result, JSON_UNESCAPED_UNICODE) . "\n");
        }
        // następny wiersz
        $num++;
      }
      // czy są błędy?
      if ($nbErreurs > 0) {
        throw new ExceptionImpots("Il y a eu des erreurs", 15);
      }
    } catch (ExceptionImpots $ex) {
      // ponownie generuje się wyjątek
      throw $ex;
    } finally {
      // zamyka się wszystkie pliki
      fclose($data);
      fclose($results);
      fclose($errors);
    }
  }

Komentarze do kodu

  • wiersz 1: funkcja przyjmuje trzy parametry:
    • [$usersFileName]: nazwa pliku tekstowego zawierającego dane podatników. Każdy wiersz tekstu zawiera dane jednego podatnika w następującej formie: stan cywilny (tak / nie), liczba dzieci, roczne wynagrodzenie:
oui,2,55555
oui,2,50000
  • (ciąg dalszy)
    • [$resultsFileName]: nazwa pliku tekstowego, który będzie zawierał wyniki. Każdy wiersz tekstu będzie miał następującą postać:
{"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}
  • (ciąg dalszy)
    • [$errorsFileName]: nazwa pliku tekstowego zawierającego błędy:

la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
  • wiersz 3: ponieważ niektóre operacje mogą wywołać wyjątek, cały kod metody jest otoczony blokiem try / catch / finally;
  • wiersze 3–19: otwierane są trzy pliki. Wyjątek jest zgłaszany, gdy tylko otwarcie pliku nie powiedzie się;
  • wiersz 24: wiersze pliku [$data] są odczytywane pojedynczo, po maksymalnie 100 znaków (wszystkie wiersze mają mniej niż 100 znaków);
  • wiersz 28: używana jest metoda statyczna [Utilitaires::cutNewLineChar] w celu usunięcia ewentualnego znaku końca wiersza;
  • wiersz 30: pobierane są trzy elementy z odczytanego wiersza;
  • wiersze 33–52: sprawdzana jest poprawność trzech elementów. W tym przypadku nie zgłaszamy wyjątku w razie wystąpienia błędu, lecz zapisujemy komunikat o nim w pliku tekstowym [$errors] (wiersz 55);
  • wiersz 59: jeśli odczytany wiersz jest prawidłowy, przeprowadzane jest obliczenie podatku. Wynik otrzymujemy w postaci tablicy asocjacyjnej ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  • wiersz 61: do uzyskanego wyniku dodaje się klucze [marié, enfants, salaire];
  • wiersz 61: wynik jest zapisywany w pliku tekstowym [$results] w postaci ciągu znaków jSON odpowiadającego uzyskanym wynikom;
  • wiersze 68–70: po zakończeniu przetwarzania pliku [$data] sprawdzana jest liczba napotkanych błędnych wierszy. Jeśli jest co najmniej jeden, generowany jest wyjątek;
  • wiersze 71–74: przechwytuje się wyjątek, który mógł zostać wygenerowany przez kod, i natychmiast go ponownie generuje (wiersz 73). Celem tego zabiegu jest umożliwienie umieszczenia klauzuli [finally] w wierszach 74–79: niezależnie od tego, w jaki sposób zakończy się wykonywanie kodu metody, wszystkie trzy pliki, które mogły zostać otwarte przez ten kod, zostaną zamknięte. Zamknięcie pliku, który nie został otwarty, nie powoduje błędu;

8.7. Klasa [ImpotsWithTaxAdminDataInJsonFile]

Klasa abstrakcyjna [AbstractBaseImpots] nie implementuje metody [getTaxAdminData] z interfejsu [InterfaceImpots]. Musimy zatem zdefiniować ją w klasie pochodnej. Robimy to w następującej klasie pochodnej [ImpotsWithTaxAdminDataInJsonFile]:


<?php

// przestrzeń nazw
namespace Application;

// definicja klasy ImpotsWithDataInArrays
class ImpotsWithTaxAdminDataInJsonFile extends AbstractBaseImpots {
  // atrybut typu Data
  private $taxAdminData;

  // konstruktor
  public function __construct(string $jsonFileName) {
    // inicjuje się $this->taxAdminData na podstawie pliku jSON
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($jsonFileName);
  }

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

}

Komentarze

  • wiersz 7: klasa [ImpotsWithTaxAdminDataInJsonFile] rozszerza klasę abstrakcyjną [AbstractBaseImpots]. Będzie musiała zdefiniować metodę [getTaxAdminData], której jej klasa nadrzędna nie zdefiniowała;
  • wiersz 9: atrybut [$taxAdminData] będzie zawierał dane urzędu skarbowego;
  • wiersze 12–15: konstruktor otrzymuje jako jedyny parametr nazwę pliku jSON zawierającego dane podatkowe;
  • wiersz 14: tworzony jest obiekt typu [TaxAdminData], a następnie jest on inicjowany. Operacja ta może wywołać wyjątek typu [ExceptionImpots]. Wyjątek ten zostanie przekazany do głównego skryptu [main.php];
  • wiersze 18–20: nadajemy treść metodzie [getTaxAdminData], której klasa nadrzędna nie zdefiniowała. W tym przypadku wystarczy, aby atrybut [$this->taxAdminData] został zainicjowany przez konstruktor;

8.8. Skrypt [main.php]

Klasy i interfejs te są wykorzystywane przez następujący skrypt [main.php]:


<?php

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

// przestrzeń nazw
namespace Application;

// dołączanie interfejsów i klas
require_once __DIR__ . '/InterfaceImpots.php';
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . '/ExceptionImpots.php';
require_once __DIR__ . '/Utilitaires.php';
require_once __DIR__ . '/AbstractBaseImpots.php';
require_once __DIR__ . "/ImpotsWithTaxAdminDataInJsonFile.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 {
  // tworzymy obiekt ImpotsWithTaxAdminDataInJsonFile
  $impots = new ImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // uruchamianie procesu przetwarzania podatków
  $impots->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // wyświetlanie błędu
  print $ex->getMessage() . "\n";
}
// koniec
print "Terminé\n";
exit();


Komentarze

  • wiersz 4: wymusza się ścisłe przestrzeganie typów parametrów funkcji;
  • wiersz 7: skrypt [main.php] jest również umieszczony w przestrzeni nazw [Application];
  • wiersze 10–15: informujemy interpreter PHP, gdzie znajdują się klasy i interfejsy wykorzystywane przez skrypt. Należy zauważyć, że w tym miejscu nie użyliśmy instrukcji use do zadeklarowania pełnych nazw klas wykorzystywanych przez skrypt. Jest to bowiem zbędne, ponieważ skrypt i klasy znajdują się w tej samej przestrzeni nazw [Application];
  • wiersze 18–22: nazwy plików tekstowych wykorzystywanych w skrypcie;
  • wiersze 24–29: tworzony jest obiekt [ImpotsWithTaxAdminDataInJsonFile] i obsługiwane są ewentualne wyjątki;
  • wiersz 28: wywoływana jest metoda [executeBatchImpots], która obliczy podatki dla wszystkich podatników z pliku [TAXPAYERSDATA_FILENAME]. Wyniki zostaną zapisane w pliku [RESULTS_FILENAME], a ewentualne błędy w pliku [ERRORS_FILENAME];
  • wiersze 29–32: w przypadku błędu nieusuwalnego wyświetlany jest komunikat o błędzie;

Wyniki

Wraz z plikiem podatników [taxpayersdata.txt] następuje:


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}