11. Практичне завдання — версія 4
Додаток для розрахунку податків реалізує таку багаторівневу структуру:

Ми візьмемо елементи з версії 3, описані в розділі «Посилання», та модифікуємо їх, щоб адаптувати до нової архітектури додатка. Іноді це називають «рефакторингом». Тут ми припускаємо, що дані, необхідні для додатка, містяться у текстових файлах. Саме шар [Dao] відповідатиме за обмін даними з цими файлами.
11.1. Структура скриптів

11.2. Об’єкти, що обмінюються між рівнями
Ми збережемо деякі об’єкти з версії 3. Наводимо їх тут для нагадування.
Виняток [ExceptionImpots] — це виняток, який запускатиме шар [Dao], коли він зіткнеться з проблемою або з доступом до даних, або з характером даних (неправильні дані).
<?php
// простір імен
namespace Application;
class ExceptionImpots extends \RuntimeException {
public function __construct(string $message, int $code=0) {
parent::__construct($message, $code);
}
}
Клас [Utilitaires] об’єднує методи, корисні для роботи з текстовими файлами (у даному випадку — один метод):
<?php
// простір імен
namespace Application;
// клас допоміжних функцій
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
// видаляється маркер кінця рядка з $ligne, якщо він існує
$longueur = strlen($ligne); // довжина рядка
while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
$ligne = substr($ligne, 0, $longueur - 1);
$longueur--;
}
// кінець — повертається рядок
return($ligne);
}
}
Клас [TaxAdminData] — це клас, що інкапсулює дані податкової адміністрації:
<?php
namespace Application;
class TaxAdminData {
// податкові шкали
private $limites;
private $coeffR;
private $coeffN;
// константи для розрахунку податку
private $plafondQfDemiPart;
private $plafondRevenusCelibatairePourReduction;
private $plafondRevenusCouplePourReduction;
private $valeurReducDemiPart;
private $plafondDecoteCelibataire;
private $plafondDecoteCouple;
private $plafondImpotCouplePourDecote;
private $plafondImpotCelibatairePourDecote;
private $abattementDixPourcentMax;
private $abattementDixPourcentMin;
// ініціалізація
public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
// отримання вмісту файлу податкових даних
$fileContents = \file_get_contents($taxAdminDataFilename);
…
// повертається об’єкт
return $this;
}
private function check($value): \stdClass {
…
return $result;
}
// toString
public function __toString() {
// JSON-рядок об’єкта
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
// методи getter та setter
public function getLimites() {
return $this->limites;
}
…
public function setLimites($limites) {
$this->limites = $limites;
return $this;
}
…
}
Ми додаємо новий клас [TaxPayerData], який інкапсулює дані, записані у файл результатів:
<?php
// простір імен
namespace Application;
// клас даних
class TaxPayerData {
// дані, необхідні для розрахунку податку платника податків
private $marié;
private $enfants;
private $salaire;
// результати розрахунку податку
private $montant;
private $surcôte;
private $décôte;
private $réduction;
private $taux;
// сетер
public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel) : TaxPayerData{
// дані платника податків, необхідні для розрахунку податку
$this->marié = $marié;
$this->enfants = $nbEnfants;
$this->salaire = $salaireAnnuel;
// повертається ініціалізований об’єкт
return $this;
}
// гетери та сеттери
public function getMarié() {
return $this->marié;
}
…
public function setMarié($marié) {
$this->marié = $marié;
return $this;
}
…
// toString
public function __toString() {
// JSON-рядок об’єкта
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
}
Примітка: скористайтеся автоматичним генеруванням коду для створення конструктора, методів getter та setter (див. параграф за посиланням). Зверніть увагу, що методи setter є «флюентними».
11.3. Рівень [dao]
Тут нас цікавить рівень [1] нашого додатка:

11.3.1. Інтерфейс [InterfaceDao]
Інтерфейс шару [dao] буде таким: [InterfaceDao.php]:
<?php
// простір імен
namespace Application;
interface InterfaceDao {
// читання даних про платників податків
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// читання даних податкової адміністрації (податкові ставки)
public function getTaxAdminData(): TaxAdminData;
// запис результатів
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Коментарі
- Технічне завдання виглядає так:
- дані платників податків містяться у текстовому файлі;
- результати розрахунку податків записуються у текстовий файл;
- можливі помилки записуються у текстовий файл;
- невідомо, у якому форматі доступні дані податкової адміністрації. Для кожного нового формату інтерфейс [InterfaceDao] повинен бути реалізований новим класом;
- методи інтерфейсу, які під час доступу до даних стикаються з непереборною помилкою, повинні генерувати виняток типу [ExceptionImpots];
- рядок 9: метод, що дозволяє отримати дані платника податків [statut marital, nombre d’enfants, salaire annuel];
- перший параметр — це ім’я текстового файлу, у якому містяться ці дані;
- другий параметр — це ім’я текстового файлу, у якому слід записати можливі помилки;
- рядок 12: метод, що дозволяє отримати дані від податкової служби. Тут йому не передається жодного параметра, оскільки невідомо, як саме вони зберігаються;
- рядок 15: метод, що дозволяє зберегти результати розрахунку податку у текстовому файлі, ім’я якого передається як параметр;
Під час написання інтерфейсу [InterfaceDao] відомо, що існуватимуть різні варіанти реалізації методу [getTaxAdminData] залежно від способу зберігання даних податкової служби. Отже, інтерфейс [InterfaceDao] буде реалізовано різними класами, кожен з яких відповідатиме за конкретний спосіб зберігання цих даних (таблиці, текстові файли, база даних, веб-сервіс). Проте ці похідні класи матимуть спільний код — код реалізації методів [getTaxPayersData, saveResults]. Відомо, що цей варіант використання можна реалізувати двома способами (див. параграф за посиланням):
- створюється абстрактний клас C, який об’єднує код, спільний для похідних класів. Клас C реалізує інтерфейс I, але деякі методи, які мають бути оголошені в похідних класах, у класі C оголошені як абстрактні, а отже, сам клас C є абстрактним. Потім створюються класи C1 та C2, похідні від C, які кожен по-своєму реалізують невизначені (абстрактні) методи свого батьківського класу C;
- створюється риса T, майже ідентична абстрактному класу C з попереднього рішення. Ця риса не реалізує інтерфейс I, оскільки синтаксично вона не може цього зробити. Далі створюються класи C1 та C2, які реалізують інтерфейс I та використовують рис T. Цим класам залишається лише реалізувати ті методи інтерфейсу I, які не реалізовані рисою T, яку вони імпортують;
Для прикладу ми використаємо тут рис [TraitDao].
11.3.2. Трайт [TraitDao]
Код риси [TraitDao] має такий вигляд: [TraitDao.php]:
<?php
// простір імен
namespace Application;
trait TraitDao {
// читання даних платників податків
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// таблиця даних платників податків
$taxPayersData = [];
// таблиця помилок
$errors = [];
// під час роботи з файлами може виникнути чимало помилок
try {
// зчитування даних користувача
// кожен рядок має такий формат: сімейний стан, кількість дітей, річний оклад
$taxPayersFile = fopen($taxPayersFilename, "r");
if (!$taxPayersFile) {
throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
}
// обробляється поточний рядок файлу даних користувача
// яка має такий формат: сімейний стан, кількість дітей, річний оклад
$num = 1; // номер поточного рядка
$nbErreurs = 0; // кількість виявлених помилок
while ($ligne = fgets($taxPayersFile, 100)) {
// порожні рядки ігноруються
$ligne = trim($ligne);
if (strlen($ligne) == 0) {
// наступний рядок
$num++;
// повертаємося до початку циклу
continue;
}
// видаляється можливий символ кінця рядка
$ligne = Utilitaires::cutNewLineChar($ligne);
// отримуємо 3 поля «одружений:діти:зарплата», які утворюють $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// перевіряємо їх
// сімейний стан має бути «так» або «ні»
$marié = trim(strtolower($marié));
$erreur = ($marié !== "oui" and $marié !== "non");
if (!$erreur) {
// кількість дітей має бути цілим числом
$enfants = trim($enfants);
if (!preg_match("/^\d+$/", $enfants)) {
$erreur = TRUE;
} else {
$enfants = (int) $enfants;
}
}
if (!$erreur) {
// зарплата — ціле число без центів
$salaire = trim($salaire);
if (!preg_match("/^\d+$/", $salaire)) {
$erreur = TRUE;
} else {
$salaire = (int) $salaire;
}
}
// помилка?
if ($erreur) {
$errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
$nbErreurs++;
} else {
// інформація зберігається
$taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
}
// наступний рядок
$num++;
}
// чи досягнуто кінця файлу?
if (!feof($taxPayersFile)) {
// цикл завершено через помилку читання
throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
} else {
// цикл завершено через позначку кінця файлу
// зберігаємо помилки у текстовому файлі
$this->saveString($errorsFilename, implode("\n", $errors));
// результат функції
return $taxPayersData;
}
} finally {
// файл закривається, якщо він відкритий
if ($taxPayersFile) {
fclose($taxPayersFile);
}
}
}
// запис результатів
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// запис масиву [$taxPayersData] у текстовий файл [$resultsFileName]
// якщо текстовий файл [$resultsFileName] не існує, його створюють
$this->saveString($resultsFilename, implode("\n", $taxPayersData));
}
// запис результатів таблиці у текстовий файл
private function saveString(string $fileName, string $data): void {
// запис таблиці [$data] у текстовий файл [$fileName]
// якщо текстовий файл [$fileName] не існує, його створюють
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
}
}
}
Коментарі
- рядок 6: тут ми визначаємо рис, а не клас;
- рядки 9–89: метод [getTaxPayersData] реалізує однойменний метод інтерфейсу [InterfaceDao]. Він отримує з текстового файлу з назвою [$taxPayersFilename] дані про платників податків [statut marital, nombre d’enfants, salaire annuel]. Вона повертає ці дані у вигляді масиву [$taxPayersData] з елементами типу [TaxPayerData] (рядки 67, 81);
- метод [getTaxPayersData] дуже схожий на метод [AbstractBaseImpots::executeBatchImpots], описаний у розділі «Посилання», з такими відмінностями:
- метод [getTaxPayersData] лише отримує дані про платників податків. Він не здійснює розрахунку податку. Цю роль тут виконує рівень [métier];
- як і метод [executeBatchImpots], він повідомляє про помилки. Тут помилки спочатку зберігаються в масиві [$errors] (рядок 13), який після завершення обробки записується у текстовий файл (рядок 79). Залежно від обставин, він може бути порожнім або заповненим;
- у разі непереборної помилки генерується виняток типу [ExceptionImpots] (рядки 20, 75);
- рядок 73: зверніть увагу на обробку, що виконується після виходу з циклу рядків 26–71. Справа в тому, що функція [fgets] має недолік: вона повертає логічне значення FALSE як у випадку, коли під час зчитування рядків було досягнуто позначки кінця файлу, так і в разі, якщо зчитування не вдалося завершити через помилку. Щоб розрізнити ці два випадки, за допомогою функції [feof] перевіряється, чи досягнуто кінця файлу. Якщо кінець файлу не досягнуто, це означає, що сталася помилка, і тоді генерується виняток;
- рядки 83–88: функція [finally] виконується незалежно від того, чи сталося виключення під час обробки файлу;
- рядок 85: якщо файл було відкрито, то «дескриптор» [$taxPayersFile] цього файлу має булеве значення TRUE, в іншому випадку — FALSE;
- рядки 99–105: приватний метод [saveString], який використовується в рядку 79 для запису масиву помилок у текстовий файл;
- рядок 99: метод [saveString] приймає два параметри:
- [string $filename] — це ім’я текстового файлу, що використовується для запису даних;
- [string $data] — це рядок символів, який потрібно записати у текстовий файл. Цей рядок буде набором рядків, що закінчуються символом кінця рядка \n;
- рядок 102: функція PHP [file_puts_contents] записує рядок символів у текстовий файл. Вона відкриває файл, записує в нього рядок і закриває файл. У разі виникнення помилки вона повертає логічне значення FALSE;
- рядок 103: якщо сталася помилка, генерується виняток;
- рядки 92–96: реалізація методу [saveResults] інтерфейсу [InterfaceDao]. Знову використовується приватний метод [saveString]. Тут другим параметром методу [saveString] є рядок, побудований на основі масиву [$taxPayersData], елементи якого мають тип [TaxPayerData]. Можна задатися питанням, яким буде результат цієї операції:
implode("\n", $taxPayersData)
У класі [TaxPayerData] (параграф «посилання») ми визначили такий метод [__toString]:
public function __toString() {
// рядок JSON об’єкта
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
Операція
implode("\n", $taxPayersData)
об'єднає кожен елемент масиву [$taxPayersData], перетворений у рядок за допомогою його методу [__toString], із символом кінця рядка \n. У результаті отримаємо рядок такого вигляду:
json1\njson2\n…
Висновок
Клас [TraitDao] реалізував два методи інтерфейсу [InterfaceDao], а саме [getTaxPayersData] та [saveResults]:
<?php
// простір імен
namespace Application;
interface InterfaceDao {
// читання даних про платників податків
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// читання даних податкової адміністрації (податкові ставки)
public function getTaxAdminData(): TaxAdminData;
// запис результатів
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Нам залишається реалізувати метод [getTaxAdminData], який отримує дані з податкової адміністрації.
11.3.3. Клас [ImpotsWithTaxAdminDataInJsonFile]
Клас [ImpotsWithTaxAdminDataInJsonFile] реалізує інтерфейс [InterfaceDao] наступним чином:
<?php
// простір імен
namespace Application;
// визначення класу ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
// використання риски
use TraitDao;
// об’єкт типу TaxAdminData, що містить дані про податкові групи
private $taxAdminData;
// конструктор
public function __construct(string $taxAdminDataFilename) {
// потрібно ініціалізувати атрибут [$this->taxAdminData]
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// повертає дані, необхідні для розрахунку податку
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Коментарі
- рядок 7: клас [ImpotsWithTaxAdminDataInJsonFile] реалізує інтерфейс [InterfaceDao];
- рядок 9: клас [ImpotsWithTaxAdminDataInJsonFile] використовує рис [traitDao], який, як відомо, реалізує методи [getTaxPayersData] та [saveResults]інтерфейсу [InterfaceDao]. Отже, класу [ImpotsWithTaxAdminDataInJsonFile] залишається лише реалізувати метод [getTaxAdminData], який отримує дані від податкової адміністрації;
- рядок 11: атрибут типу [TaxAdminData], який повертає метод [getTaxAdminData] з рядків 20–22. Цей атрибут ініціалізується конструктором з рядків 14–17;
Ми завершили роботу з рівнем [dao] нашого додатка: у нас є клас, який повністю реалізує інтерфейс [InterfaceDao], який ми для себе визначили. Тепер ми можемо перейти до рівня [métier].
11.4. Рівень [métier]
Тепер ми реалізуємо рівень [2] нашої архітектури:

11.4.1. Інтерфейс [InterfaceMétier]
Інтерфейс шару [métier] матиме такий вигляд:
<?php
// простір імен
namespace Application;
interface InterfaceMetier {
// розрахунок податків платника податків
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// розрахунок податків у пакетному режимі
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
Коментарі
- рядок 9: інтерфейс [InterfaceMétier] вміє обчислювати суму податку для фізичної особи-платника податків за умови надання йому таких даних: сімейний стан, кількість дітей, річний дохід. Метод [calculerImpot] не використовує шар [dao], тому він не генерує винятків;
- рядок 9: інтерфейс [InterfaceMétier] також може обчислити суму податку для групи платників податків, дані про яких зібрані у текстовому файлі з назвою [$taxPayersFileName]. Він записує результати у текстовий файл з назвою [$resultsFileName]. Метод [executeBatchImpots] повинен звертатися до рівня [dao], який відповідає за доступ до файлової системи. У цьому випадку з рівня [dao] можуть надходити винятки, які метод [executeBatchImpots] не перехоплюватиме: він дозволить їм передаватися до головного скрипта. Некритичні помилки записуються у текстовий файл із назвою [$errorsFileName];
- рядок 9: метод [calculerImpot] є суто методом [métier]. Він не зважає на те, звідки походять дані, які він використовує;
- рядок 12: метод [executeBatchImpots] звертається до рівня [dao] для читання та запису даних у текстові файли. Він неодноразово викликає бізнес-метод [calculerImpot];
11.4.2. Клас [Metier]
Клас [Metier] реалізує інтерфейс [InterfaceMetier] наступним чином:
<?php
// простір імен
namespace Application;
class Metier implements InterfaceMetier {
// рівень Dao
private $dao;
// дані податкової адміністрації
private $taxAdminData;
//---------------------------------------------
// сетер шару [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// зберігається посилання на рівень [dao]
$this->dao = $dao;
// отримуються дані, необхідні для розрахунку податку
// метод [getTaxAdminData] може викликати виняток ExceptionImpots
// тоді виняток передається до коду, що викликає
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// розрахунок податку
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// результат
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 {
…
// результат
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=річназарплата-пільга
// податкова пільга має мінімальне та максимальне значення
private function getRevenuImposable(float $salaire): float {
…
// результат
return floor($revenuImposable);
}
// розраховує можливе зниження
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// результат
return ceil($décôte);
}
// розраховує можливе зниження
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
…
// результат
return ceil($réduction);
}
// розрахунок податків у пакетному режимі
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
…
// запис результатів
$this->dao->saveResults($resultsFileName, $results);
}
}
Коментарі
- рядок 6: клас [Metier] реалізує інтерфейс [InterfaceMetier], тобто методи [calculerImpot] (рядки 30–34) та [executeBatchImpots] (рядки 66–70);
- рядок 8: посилання на шар [dao]. Він обов’язково потрібен, щоб шар [métier] знав, до кого звертатися, коли йому потрібні зовнішні дані. Цей атрибут буде ініціалізовано за допомогою сеттера у рядках 14–17 або за допомогою конструктора у рядках 19–26;
- рядок 10: об’єкт типу [TaxAdminData], який інкапсулює дані податкової адміністрації. Ці дані необхідні для бізнес-методу [calculerImpot]. Цей атрибут ініціалізується за допомогою конструктора в рядках 19–26;
- рядки 19–26: конструктор ініціалізує обидва атрибути класу:
- атрибут [$dao] ініціалізується за допомогою посилання, переданого як параметр конструктору. Слід зазначити, що тип цього параметра відповідає типу інтерфейсу [InterfaceDao], що дозволяє ініціалізувати клас [Metier] будь-яким класом, який реалізує цей інтерфейс;
- атрибут [$taxAdminData] ініціалізується за допомогою виклику методу [getTaxAdminData] з рівня [dao];
З цього випливає, що під час виконання методів [calculerImpots] та [executeBatchImpots] ініціалізуються обидва атрибути [$dao] та [$taxAdminData].
Метод [calculerImpots] має такий вигляд:
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié: так, ні
// $enfants: кількість дітей
// $salaire: річний оклад
// $this->taxAdminData: дані податкової служби
//
// перевіряється наявність даних податкової служби
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// розрахунок податку з урахуванням дітей
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// розрахунок податку без урахування дітей
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// застосування обмеження сімейного коефіцієнта
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART євро за перших двох дітей
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART євро за перших двох дітей, удвічі більше за наступних
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// береться найвищий податок
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"];
}
// розрахунок можливої знижки
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// розрахунок можливого зменшення податку
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// результат
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
Коментарі
- цей код належить методу [AbstractBaseImpots::calculerImpot] версії 3, описаному в розділі «Посилання». Те саме стосується приватних методів [calculerImpot2, getDecôte, getRéduction, getRevenuImposable];
Метод [Metier::executeBatchImpots] має такий вигляд:
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// дозволяє передавати винятки, що походять з рівня [dao]
// отримуємо дані про платників податків
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// таблиця результатів
$results = [];
// здійснюється їх аналіз
foreach ($taxPayersData as $taxPayerData) {
// розраховується податок
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// заповнюємо [$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"]);
// заносимо результат у таблицю результатів
$results [] = $taxPayerData;
}
// запис результатів
$this->dao->saveResults($resultsFileName, $results);
}
Коментарі
- рядок 1: метод повинен повторно викликати метод [calculerImpot] для кожного платника податків, знайденого у текстовому файлі з назвою [$taxPayersFileName]. Він повинен записати результати у текстовий файл з назвою [$resultsFileName]. Виявлені некритичні помилки записуються у текстовий файл із назвою [$errorsFileName]. Метод сам не генерує винятків, але пропускає ті, що генерує шар [dao];
- рядок 4: дані платників податків запитуються у шарі [dao]. Цей шар повертає масив елементів типу [TaxPayerData], який є класом атрибутів [marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux] (див. параграф «посилання»). Якщо тут виникає виняток, оскільки він не перехоплюється catch, він автоматично передається до коду, що викликає. Це означає, що в разі винятку рядок 6 не виконується;
- рядок 6: масив результатів типу [TaxPayerData];
- рядки 8–22: обчислюється податок для кожного елемента масиву платників податків [$taxPayersData]. Для цього викликається внутрішній метод [calculerImpot] (рядок 10);
- рядки 15–19: отриманий результат використовується для ініціалізації атрибутів таблиці [TaxPayerData], які ще не були ініціалізовані;
- рядок 21: отриманий результат накопичується в таблиці результатів [$results];
- рядок 24: після розрахунку податку для всіх платників податків результати зберігаються у текстовому файлі. Цю роботу виконує шар [dao];
Висновок
Загалом шар [métier] досить простий у написанні, оскільки він звертається до шару [dao], який, у свою чергу, керує доступом до даних та відповідним обробленням помилок.
11.5. Головний скрипт
Тепер напишемо скрипт шару [3] нашої архітектури:

Головний скрипт має такий вигляд: [main.php]:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// обробка помилок за допомогою PHP
//ini_set("display_errors", "0");
// включення інтерфейсів та класів
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";
// тест -----------------------------------------------------
// визначення констант
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// створення шару [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// створення шару [métier]
$métier = new Metier($dao);
// розрахунок податків у пакетному режимі
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// виведення повідомлення про помилку
print $ex->getMessage() . "\n";
}
// кінець
print "Terminé\n";
exit;
Коментарі
- рядок 24: ім’я файлу даних платників податків;
- рядок 25: ім’я файлу результатів;
- рядок 26: ім’я файлу з помилками;
- рядок 27: ім’я файлу jSON, що містить дані податкової адміністрації;
- рядок 31: створення шару [dao];
- рядок 33: створення шару [métier] на основі цього шару [dao];
- рядок 35: виконання методу [executeBatchImpots] шару [métier];
- рядки 36–39: як ми вже бачили, шар [métier] може генерувати винятки. Вони перехоплюються саме тут;
11.6. Візуальні тести
11.6.1. Тест № 1
З файлом платників податків [taxpayersdata.txt], що містить такі дані:
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
отримуємо такий файл помилок [errors.txt]:
la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
та файл результатів [resultats.txt]:
11.6.2. Тест № 2
У головному скрипті для файлу платників податків вказуємо ім’я файлу, якого не існує:
Результати, отримані в консолі, такі:
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.
- рядок 1: попередження (warning) інтерпретатора PHP;
- рядок 2: повідомлення про помилку винятку, згенерованого рівнем [dao];
Можна приглушити повідомлення про помилки інтерпретатора PHP:

У рядку 21 наведеного вище коду вказано, що помилки PHP не повинні відображатися. На етапі розробки їх необхідно відображати. У виробничому режимі їх слід приховати.
Результати виконання будуть такими:
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
11.7. Тести [Codeception]
Візуальні тести є вкрай недостатніми:
- зазвичай обмежуються лише кількома тестами;
- під час такої візуальної перевірки ми не завжди достатньо уважні, і деякі деталі можуть вислизнути з-під нашої уваги;
У реальності професійної розробки тести пишуть спеціалісти, для яких це є основною функцією. Вони прагнуть зробити тести якомога повнішими. Для цього вони використовують фреймворки для тестування.
Тут ми будемо використовувати фреймворк Codeception [https://codeception.com/], оскільки його можна інтегрувати в NetBeans. Це фреймворк із широким спектром можливостей. Ми скористаємося лише деякими з них. Ідея полягає в тому, щоб мати швидкий спосіб перевіряти працездатність кожної нової версії програми після її створення. Наявність успішно виконаних тестів надає розробнику впевненості в написаному ним коді. Це важливий фактор.
11.7.1. Встановлення фреймворку [Codeception]
Як і багато бібліотек PHP, фреймворк [Codeception] встановлюється разом із [Composer]. Тому ми відкриваємо термінал Laragon (див. параграф із посиланням).
Спочатку нам потрібно встановити фреймворк для тестування PHPUnit [https://phpunit.de/]. Адже Codeception використовує у фоновому режимі фреймворк PHPUnit:

Далі встановлюємо фреймворк Codeception:

Ось і все. Тепер розглянемо інтеграцію [Codeception] у NetBeans.
11.7.2. Інтеграція [CodeCeption] у NetBeans

- у [1-2] відкриваємо властивості проєкту;
- у [3-4] ми робимо [Codeception] одним із тестових фреймворків проєкту;


- у [5-8] ініціалізується фреймворк [Codeception] для проекту;

- у [9] було створено папку [tests], а також файл конфігурації [codeception.yml] у [10-11]. Файл [11] є таким самим, як і файл [10]. Codeception просто створив папку [Important Files], щоб надати особливого значення файлу [10];
- у файлі [12-13] ми повертаємося до властивостей проєкту;

- у [14-16] папка [tests] [16] позначається як папка тестів проєкту;
- у [16] папка [tests] з’являється під новою назвою [Test Files]. Наявність цієї папки в проєкті PHP свідчить про те, що цей проєкт містить фреймворк для програмованих тестів;
- ми створимо наші тести в папці [unit] [17];
11.7.3. Тести рівня [dao]

- ми створимо всі наші тести у папці [unit] [1];
- імена тестових класів [Codeception] повинні закінчуватися ключовим словом [Test], інакше класи не будуть розпізнані як тестові;
Наші тестові класи [Codeception] матимуть такий вигляд: [https://codeception.com/docs/05-UnitTests]:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// завантаження тестового середовища
…
class DaoTest extends \Codeception\Test\Unit {
// атрибути тесту
private $attribut1;
public function __construct() {
parent::__construct();
// ініціалізація тестового середовища
…
}
// тести
public function testTaxAdminData() {
// тести
$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);
…
}
}
Коментарі
- рядок 7: тестові класи будуть у тому самому просторі імен, що й тестована програма;
- рядки 9–10: тут містяться операції [require] для завантаження тестованих класів та інтерфейсів;
- рядок 12: назва тестового класу обов’язково має закінчуватися ключовим словом [Test]. Цей клас має успадковувати клас [\Codeception\Test\Unit];
- рядки 16–20: конструктор дозволить нам ініціалізувати тестове середовище;
- рядок 23: імена тестових методів обов’язково повинні починатися з ключового слова [test];
- рядки 25–31: можна використовувати різні методи тестування;
Клас тесту [DaoTest] матиме такий вигляд:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// константи
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// включення інтерфейсів та класів
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";;
// тест -----------------------------------------------------
// визначення констант
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// створення шару [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// тестування
public function testTaxAdminData() {
…
}
}
Коментарі
Для створення тестів для певної версії практичного завдання ми будемо використовувати середовище, ідентичне тому, що використовується головним скриптом цієї версії. Для версії 04 це наступний скрипт [main.php]:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// обробка помилок за допомогою PHP
ini_set("display_errors", "0");
// підключення інтерфейсів та класів
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";
// тест -----------------------------------------------------
// визначення констант
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// створення шару [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// створення шару [métier]
$métier = new Metier($dao);
// розрахунок податків у пакетному режимі
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// виведення повідомлення про помилку
print $ex->getMessage() . "\n";
}
// кінець
print "Terminé\n";
exit;
Щоб протестувати шар [dao], у класі тестування:
- використовуємо середовище з рядків 13–27 скрипта [main.php];
- у конструкторі тестового класу створюємо шар [dao], як у рядку 31;
- пишемо методи тестування;
Ми будемо діяти таким чином для всіх тестових класів.
Повернемося до повного коду тестового класу:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// константи
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// імпорт інтерфейсів та класів
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";;
// тест -----------------------------------------------------
// визначення констант
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// створення шару [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// тестування
public function testTaxAdminData() {
// константи розрахунку
$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());
// податкові ставки
$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());
}
}
Коментарі
- рядки 10–25: завантаження середовища, необхідного для тестування, та визначення констант;
- рядки 31–36: побудова шару [dao] (рядок 34), а потім ініціалізація атрибута [$taxAdminData] (рядок 29). Цей атрибут містить дані податкової адміністрації;
- рядки 39–55: єдиний метод тестування. Він полягає у перевірці того, чи вміст атрибута [$taxAdminData] відповідає очікуваним значенням;
- рядки 41–50: перевірка констант розрахунку податку;
- рядки 52–55: перевірка податкових шкал. Метод [assertSame] перевіряє, чи два об’єкти PHP, у даному випадку таблиці, є ідентичними;
Щоб виконати цей клас тесту, слід діяти наступним чином:

- у [1-2] виконується тест;
- [3]: вікно результатів тестування;
- [4]: виконаний тестовий клас;
- [5]: результати. Тут єдиний метод тестування пройшов успішно;
- [6]: коли тест завершується з помилкою або, що трапляється частіше, коли жоден тест не було виконано, слід перейти до вікна [6]. Найчастіше саме завантаження тестового середовища завершилося з помилкою, і тому жоден тест не зміг бути виконаний. Помилки, що відображаються у вікні [6], — це ті, які виникають під час виконання звичайного скрипта PHP;
Розглянемо приклад тесту, що завершився помилкою:
У класі тесту ми вносимо помилку в визначення константи:
// константи
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");
потім виконуємо тест. Отриманий результат такий:

У вікні [4]:

11.7.4. Тестування шару [métier]
Клас тесту [MetierTest] побудовано за тими самими правилами, що й клас [DaoTest], але він містить більше методів тестування:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// константи
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// включення інтерфейсів та класів
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";;
// тест -----------------------------------------------------
// визначення констант
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class MetierTest extends \Codeception\Test\Unit {
// бізнес-шар
private $métier;
public function __construct() {
parent::__construct();
// створення шару [dao]
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
// створення шару [métier]
$this->métier = new Metier($dao);
}
// тестування
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"]);
}
}
Коментарі
- рядки 10–25: завантаження файлів, що визначають тестове середовище. Воно таке саме, як і для шару [dao];
- рядки 31–37: створення екземплярів шарів [dao] та [métier];
- рядки 40–47: тест на розрахунок податку;
- рядок 41: певний розрахунок податку виконується за допомогою шару [métier];
- рядки 42–46: перевіряється, чи отримані результати збігаються з результатами симулятора податкової адміністрації [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm];
- рядки 23–26: тести на рівність проводяться з точністю до 1 євро. Адже було виявлено, що через проблеми з округленням алгоритм, описаний у документі, давав очікувані результати з точністю до 1 євро;
- рядок 27: ставка оподаткування розраховується без похибки;
- рядки 49–137: такі тести повторюються 10 разів, щоразу з різними параметрами платника податків;
Тести дають такі результати:

11.7.5. Тестування наступних версій
Надалі тестування шарів [dao] та [métier] буде ідентичним тестуванню версії 04. Зміниться лише тестове середовище. Тому ми наведемо лише його опис та результати тестування.