13. Практичне завдання — версія 5

Ми вже написали кілька версій цього завдання. В останній версії використовувалася багатошарова архітектура:

Рівень [dao] реалізує інтерфейс [InterfaceDao]. Ми створили клас, що реалізує цей інтерфейс:
- [DaoImpotsWithTaxAdminDataInJsonFile], який отримував податкові дані з файлу jSON;
Ми реалізуємо інтерфейс [InterfaceDao] за допомогою нового класу [DaoImpotsWithTaxAdminDataInDatabase], який буде отримувати дані податкової адміністрації з бази даних MySQL.
13.1. Створення бази даних [dbimpots-2019]
Дотримуючись прикладу з розділу «Посилання», ми створюємо базу даних MySQL під назвою [dbimpots-2019], власником якої буде [admimpots] із паролем [mdpimpots]:

- у [1-4], наведеному вище, ми бачимо базу даних [dbimpots-2019], яка наразі не містить таблиць;

- у [1-5] вище ми бачимо, що користувач [admimpots] має всі права на базу даних [dbimpots-2019]. Чого ми тут не бачимо, так це того, що цей користувач має пароль [admimpots];
Тепер ми створюємо таблицю [tbtranches], яка міститиме податкові шкали:

- у [1-7] ми створюємо таблицю з назвою [tbtranches], що має 4 стовпці;

- у [3-6] ми визначаємо стовпець з назвою [id] (3), типу цілого числа [int] (4), який буде первинним ключем [6] таблиці та автоматично інкрементуватиметься [5] за допомогою SGBD. Це означає, що MySQL самостійно керуватиме значеннями первинного ключа під час вставлення записів. Він присвоїть значення 1 первинному ключу першого вставленого запису, потім 2 — наступному тощо;
- у [7] майстер пропонує нам інші варіанти налаштування первинного ключа. Тут ми просто підтверджуємо [7] значення за замовчуванням;

- у [8-16] визначаються три інші стовпці таблиці:
- [limites] (8) типу десяткового числа (9) з 10 цифрами, з яких 2 — десяткові знаки (10), міститиме елементи стовпця 17 податкових шкал;
- [coeffR] (11) типу десяткового числа (12) з 6 цифрами, з яких 2 — десяткові (13), міститиме елементи стовпця 18 податкових шкал;
- [coeffN] (14) типу десяткового числа (15) з 10 цифрами, з яких 2 — десяткові (16), міститиме елементи стовпця 19 податкових шкал;
Після перевірки цієї структури ми отримуємо такий результат:

- у [5] піктограма ключа вказує, що стовпець [id] є первинним ключем. Також видно, що цей первинний ключ має цілочисельні значення (6) і що він управляється (автоінкрементується) стовпцем MySQL;
Так само, як ми створили таблицю [tbtranches], ми створюємо таблицю [tbconstantes], яка міститиме константи для розрахунку податку:

Можна експортувати структуру бази даних у текстовий файл у вигляді послідовності команд SQL:

Опція [5] експортує лише структуру бази даних, а не її вміст. У нашому випадку база ще не містить даних.



Опція [11] створює такий файл SQL [dbimpots-2019.sql]:
-- phpMyAdmin SQL Зливання
-- версія 4.8.5
-- https://www.phpmyadmin.net/
--
-- Хост: localhost:3306
-- Час створення: 30 червня 2019 р. о 01:10 PM
-- Версія сервера: 5.7.24
-- PHP Версія: 7.2.11
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- База даних: `dbimpots-2019`
--
CREATE DATABASE IF NOT EXISTS `dbimpots-2019` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `dbimpots-2019`;
-- --------------------------------------------------------
--
-- Структура таблиці `tbconstantes`
--
DROP TABLE IF EXISTS `tbconstantes`;
CREATE TABLE `tbconstantes` (
`id` int(11) NOT NULL,
`plafondQfDemiPart` decimal(10,2) NOT NULL,
`plafondRevenusCelibatairePourReduction` decimal(10,2) NOT NULL,
`plafondRevenusCouplePourReduction` decimal(10,2) NOT NULL,
`valeurReducDemiPart` decimal(10,2) NOT NULL,
`plafondDecoteCelibataire` decimal(10,2) NOT NULL,
`plafondDecoteCouple` decimal(10,2) NOT NULL,
`plafondImpotCelibatairePourDecote` decimal(10,2) NOT NULL,
`plafondImpotCouplePourDecote` decimal(10,2) NOT NULL,
`abattementDixPourcentMax` decimal(10,2) NOT NULL,
`abattementDixPourcentMin` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Структура таблиці `tbtranches`
--
DROP TABLE IF EXISTS `tbtranches`;
CREATE TABLE `tbtranches` (
`id` int(11) NOT NULL,
`limites` decimal(10,2) NOT NULL,
`coeffR` decimal(10,2) NOT NULL,
`coeffN` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Індекси для вивантажених таблиць
--
--
-- Індекси для таблиці `tbconstantes`
--
ALTER TABLE `tbconstantes`
ADD PRIMARY KEY (`id`);
--
-- Індекси для таблиці `tbtranches`
--
ALTER TABLE `tbtranches`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT для вивантажених таблиць
--
--
-- AUTO_INCREMENT для таблиці `tbconstantes`
--
ALTER TABLE `tbconstantes`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT для таблиці `tbtranches`
--
ALTER TABLE `tbtranches`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
Ви можете використати цей файл SQL для відновлення бази даних [dbimpots-2019], якщо вона була знищена або пошкоджена. У цьому випадку немає необхідності видаляти базу даних перед її відновленням, оскільки скрипт SQL робить це самостійно:


13.2. Організація коду
Щоб краще продемонструвати роль різних скриптів PHP, які ми пишемо, ми організуємо наш код у папки:

- у [1] — загальний огляд версії 05;
- у [2] — сутності додатка, що обмінюються між рівнями;
- у [3] — утиліти додатка;
- у [4] — дані, що використовуються або генеруються додатком. Тут ми вирішили використовувати лише файли jSON для текстових файлів. Вони мають кілька переваг:
- їх розпізнають багато інструментів;
- ці інструменти підтримують підсвічування синтаксису. Крім того, формат jSON має певні правила. Якщо їх не дотримуватися, інструменти повідомляють про це. Наприклад, помилкою, яку важко виявити у звичайному текстовому файлі, є використання великих та малих літер «O» замість нулів. Якщо така помилка трапиться, про неї буде повідомлено. Так, у коді jSON:
"plafondRevenusCouplePourReduction": 42O74
де в [42074] випадково вставлено велику літеру «O» замість нуля, NetBeans повідомляє про помилку:

Дійсно, NetBeans розпізнає велику літеру «O», що перетворює [49O74] на рядок символів. Звідси він робить висновок, що синтаксис повинен бути таким: [4-5] — рядок [47O74] має бути взятий у лапки. Таким чином, увага розробника привертається до помилки, і він може її виправити: або поставити лапки, або замінити букву «O» на нуль;
Інші елементи версії 05 такі:

- у [6] — інтерфейси та класи рівня [Dao];
- у [7] — інтерфейси та класи шару [métier];
- у [8] — основні скрипти версії 05;
Версія 05 має дві окремі цілі:
- заповнити базу даних MySQL [dbimpots-2019] вмістом файлу jSON [Data/txadmindata.json];
- реалізувати розрахунок податку з використанням податкових даних, які відтепер надходять із бази MySQL [dbimpots-2019];
Ми розглянемо ці два завдання окремо.
13.3. Заповнення бази даних [dbimpots-2019]
13.3.1. Завдання
Текстовий файл taxadmindata.json містить дані податкової адміністрації:
{
"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
}
Наше завдання полягає в тому, щоб перенести ці дані до раніше створеної бази даних MySQL [dbimpots-2019].
13.3.2. Об’єкти

Об’єкт [Database] слугуватиме для інкапсуляції даних із наступного файлу jSON [database.json]:
{
"dsn": "mysql:host=localhost;dbname=dbimpots-2019",
"id": "admimpots",
"pwd": "mdpimpots",
"tableTranches": "tbtranches",
"colLimites": "limites",
"colCoeffR": "coeffr",
"colCoeffN": "coeffn",
"tableConstantes": "tbconstantes",
"colPlafondQfDemiPart": "plafondQfDemiPart",
"colPlafondRevenusCelibatairePourReduction": "plafondRevenusCelibatairePourReduction",
"colPlafondRevenusCouplePourReduction": "plafondRevenusCouplePourReduction",
"colValeurReducDemiPart": "valeurReducDemiPart",
"colPlafondDecoteCelibataire": "plafondDecoteCelibataire",
"colPlafondDecoteCouple": "plafondDecoteCouple",
"colPlafondImpotCelibatairePourDecote": "plafondImpotCelibatairePourDecote",
"colPlafondImpotCouplePourDecote": "plafondImpotCouplePourDecote",
"colAbattementDixPourcentMax": "abattementDixPourcentMax",
"colAbattementDixPourcentMin": "abattementDixPourcentMin"
}
Елемент [TaxAdminData] призначений для інкапсуляції даних з наступних файлів: jSON та [taxadmindata.json]:
{
"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
}
Об’єкт [TaxPayerData] слугуватиме для інкапсуляції даних з наступних файлів: jSON та [taxpayerdata.json]:
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
13.3.2.1. Базовий клас [BaseEntity]
Щоб спростити код сутностей, ми дотримуватимемося такого правила: атрибути сутності мають ті самі імена, що й атрибути файлу jSON, який ця сутність має інкапсулювати. Згідно з цим правилом, сутності [Database, TaxAdminData, TaxPayerData] мають спільні риси, які можна об’єднати в батьківський клас. Це буде наступний клас [BaseEntity]:
<?php
namespace Application;
class BaseEntity {
// атрибут
protected $arrayOfAttributes;
// ініціалізація з файлу jSON
public function setFromJsonFile(string $jsonFilename) {
// витягується вміст файлу з податковими даними
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// помилка?
if (!$fileContents) {
// фіксується помилка
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// код jSON з файлу конфігурації заноситься в асоціативний масив
$this->arrayOfAttributes = \json_decode($fileContents, true);
// помилка?
if ($this->arrayOfAttributes === FALSE) {
// зафіксовано помилку
$erreur = TRUE;
$message = "Le fichier de données jSON [$jsonFilename] n'a pu être exploité correctement";
}
}
// помилка?
if ($erreur) {
// генерується виняток
throw new ExceptionImpots($message);
}
// ініціалізація атрибутів класу
foreach ($this->arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// повертається об’єкт
return $this;
}
public function checkForAllAttributes() {
// перевіряється, чи всі ключі були ініціалізовані
foreach (\array_keys($this->arrayOfAttributes) as $key) {
if ($key !== "arrayOfAttributes" && !isset($this->$key)) {
throw new ExceptionImpots("L'attribut [$key] de la classe "
. get_class($this) . " n'a pas été initialisé");
}
}
}
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// ініціалізуються деякі атрибути класу
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// повертається об’єкт
return $this;
}
// toString
public function __toString() {
// атрибути об’єкта
$arrayOfAttributes = \get_object_vars($this);
// видаляється атрибут батьківського класу
unset($arrayOfAttributes["arrayOfAttributes"]);
// JSON-рядок об’єкта
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
// геттер
public function getArrayOfAttributes() {
return $this->arrayOfAttributes;
}
}
Коментарі
- рядок 5: клас [BaseEntity] призначений для розширення класами [Database, TaxAdminData, TaxPayerData];
- рядок 7: атрибут [$arrayOfAttributes] — це масив, що містить усі атрибути дочірнього класу, який розширив [BaseEntity], а також їхні значення;
- рядки 9–41: атрибут [$arrayOfAttributes] ініціалізується на основі файлу jSON [$jsonFilename], переданого як параметр. Виникає виняток типу [ExceptionImpot], якщо файл jSON не вдалося прочитати або якщо це не дійсний файл jSON;
- рядки 36–38: тут міститься спеціальний код, який виконується, якщо функцію викликає дочірній клас. У цьому випадку [$this] представляє екземпляр дочірнього класу [Database, TaxAdminData, TaxPayerData], і в такому випадку рядки 36–38 ініціалізують атрибути цього дочірнього класу, за умови, що ці атрибути мають рівень видимості protected (або public) (див. параграф «посилання»). Адже було зазначено, що атрибути сутностей [Database, TaxAdminData, TaxPayerData] є такими самими, як і атрибути файлу jSON, який вони інкапсулюють. Нарешті, метод [setFromJsonFile] дозволяє дочірньому класу ініціалізуватися на основі файлу jSON;
- рядок 40: об’єкт [$this], тобто екземпляр дочірнього класу, повертається, якщо метод [setFromJsonFile] був викликаний дочірнім класом;
- рядки 43–51: метод [checkForAllAttributes] дозволяє дочірньому класу перевірити, чи всі його атрибути були ініціалізовані. Якщо це не так, генерується виняток [ExceptionImpots]. Цей метод дозволяє дочірньому класу перевірити, чи не пропущено в його файлі jSON певні атрибути;
- рядки 53–60: метод [setFromArrayOfAttributes] дозволяє дочірньому класу ініціалізувати всі або частину своїх атрибутів на основі асоціативного масиву, ключі якого мають ті самі імена, що й атрибути дочірнього класу, який потрібно ініціалізувати;
- рядки 63–70: метод [__toString] дозволяє отримати представлення jSON дочірнього класу;
13.3.2.2. Суть [Database]
Суть [Database] має такий вигляд:
<?php
namespace Application;
class Database extends BaseEntity {
// атрибути
protected $dsn;
protected $id;
protected $pwd;
protected $tableTranches;
protected $colLimites;
protected $colCoeffR;
protected $colCoeffN;
protected $tableConstantes;
protected $colPlafondQfDemiPart;
protected $colPlafondRevenusCelibatairePourReduction;
protected $colPlafondRevenusCouplePourReduction;
protected $colValeurReducDemiPart;
protected $colPlafondDecoteCelibataire;
protected $colPlafondDecoteCouple;
protected $colPlafondImpotCelibatairePourDecote;
protected $colPlafondImpotCouplePourDecote;
protected $colAbattementDixPourcentMax;
protected $colAbattementDixPourcentMin;
…
}
Клас [Database] використовується для інкапсуляції даних наступного файлу jSON [database.json]:
{
"dsn": "mysql:host=localhost;dbname=dbimpots-2019",
"id": "admimpots",
"pwd": "mdpimpots",
"tableTranches": "tbtranches",
"colLimites": "limites",
"colCoeffR": "coeffr",
"colCoeffN": "coeffn",
"tableConstantes": "tbconstantes",
"colPlafondQfDemiPart": "plafondQfDemiPart",
"colPlafondRevenusCelibatairePourReduction": "plafondRevenusCelibatairePourReduction",
"colPlafondRevenusCouplePourReduction": "plafondRevenusCouplePourReduction",
"colValeurReducDemiPart": "valeurReducDemiPart",
"colPlafondDecoteCelibataire": "plafondDecoteCelibataire",
"colPlafondDecoteCouple": "plafondDecoteCouple",
"colPlafondImpotCelibatairePourDecote": "plafondImpotCelibatairePourDecote",
"colPlafondImpotCouplePourDecote": "plafondImpotCouplePourDecote",
"colAbattementDixPourcentMax": "abattementDixPourcentMax",
"colAbattementDixPourcentMin": "abattementDixPourcentMin"
}
Клас і файл jSON мають однакові атрибути. Вони описують характеристики бази даних MySQL [dbimpots-2019]:
dsn | Назва бази даних DSN |
id | Власник бази даних |
pwd | Його пароль |
tableTranches | Назва таблиці, що містить податкові діапазони |
colLimites colCoeffR colCoeffN | Назви стовпців таблиці [tableTranches] |
tableConstantes | Назва таблиці, що містить константи для розрахунку податку |
colPlafondQfDemiPart colPlafondRevenusCelibatairePourReduction colPlafondRevenusCouplePourReduction colValeurReducDemiPart colPlafondDecoteCelibataire colPlafondDecoteCouple colPlafondImpotCelibatairePourDecote colPlafondImpotCouplePourDecote colAbattementDixPourcentMax colAbattementDixPourcentMin | Назви стовпців таблиці [tableConstantes], що містять константи для розрахунку податку |
Навіщо називати таблиці та стовпці, якщо їхні назви вже відомі і вони не підлягають зміні? Після SGBD MySQL ми будемо використовувати SGBD PostgreSQL для зберігання даних податкової адміністрації. Однак імена стовпців і таблиць Postgres не відповідають тим самим правилам, що й MySQL. Нам доведеться використовувати інші імена. Це також стосується інших таблиць SGBD. Якщо ми хочемо, щоб код був сумісним між таблицями SGBD, то краще використовувати параметри, а не жорстко задані імена таблиць і стовпців.
Повернемося до коду класу [Database]:
<?php
namespace Application;
class Database extends BaseEntity {
// атрибути
protected $dsn;
protected $id;
protected $pwd;
protected $tableTranches;
protected $colLimites;
protected $colCoeffR;
protected $colCoeffN;
protected $tableConstantes;
protected $colPlafondQfDemiPart;
protected $colPlafondRevenusCelibatairePourReduction;
protected $colPlafondRevenusCouplePourReduction;
protected $colValeurReducDemiPart;
protected $colPlafondDecoteCelibataire;
protected $colPlafondDecoteCouple;
protected $colPlafondImpotCelibatairePourDecote;
protected $colPlafondImpotCouplePourDecote;
protected $colAbattementDixPourcentMax;
protected $colAbattementDixPourcentMin;
// сеттер
// ініціалізація
public function setFromJsonFile(string $jsonFilename) {
// батьківський елемент
parent::setFromJsonFile($jsonFilename);
// перевіряється, чи всі атрибути були ініціалізовані
parent::checkForAllAttributes();
// повертається об’єкт
return $this;
}
// гетери та сеттери
public function getDsn() {
return $this->dsn;
}
…
public function setDsn($dsn) {
$this->dsn = $dsn;
return $this;
}
…
}
Коментарі
- рядки 7–24: усі атрибути класу мають видимість [protected]. Це є умовою того, щоб їх можна було змінювати з батьківського класу [BaseEntity] (див. параграф «посилання»);
- рядки 28–35: метод [setFromJsonFile] дозволяє ініціалізувати атрибути класу [Database] на основі вмісту файлу jSON, переданого як параметр. Атрибути файлу jSON та класу [Database] мають бути ідентичними. Якщо файл jSON не підходить для обробки, генерується виняток;
- рядок 30: ініціалізацію виконує батьківський клас;
- рядок 32: батьківський клас отримує запит перевірити, чи всі атрибути класу [Database] були ініціалізовані. Якщо це не так, генерується виняток;
- рядок 34: повертається екземпляр [Database], який щойно було ініціалізовано;
- рядки 37 і далі: методи getter та setter для атрибутів класу;
13.3.2.3. Об’єкт [TaxAdminData]
Об’єкт [TaxAdminData] має такий вигляд:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// податкові шкали
protected $limites;
protected $coeffR;
protected $coeffN;
// константи для розрахунку податку
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
…
}
Клас [TaxAdminData] використовується для інкапсуляції даних з наступного файлу jSON [taxadmindata.json]:
{
"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
}
Клас і файл jSON мають однакові атрибути. Вони представляють дані податкової адміністрації. Решта коду класу [TaxAdminData] виглядає так:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// податкові шкали
protected $limites;
protected $coeffR;
protected $coeffN;
// константи для розрахунку податку
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// ініціалізація
public function setFromJsonFile(string $taxAdminDataFilename) {
// батьківський елемент
parent::setFromJsonFile($taxAdminDataFilename);
// перевіряється, чи всі атрибути були ініціалізовані
parent::checkForAllAttributes();
// перевіряється, чи значення атрибутів є дійсними числами >=0
foreach ($this as $key => $value) {
if ($key !== "arrayOfAttributes") {
// $value має бути дійсним числом >=0 або масивом дійсних чисел >=0
$result = $this->check($value);
// помилка?
if ($result->erreur) {
// генерується виняток
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// записується значення
$this->$key = $result->value;
}
}
}
// повертається об’єкт
return $this;
}
protected function check($value): \stdClass {
// $value — це масив елементів типу string або один елемент
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// масив рядків перетворюється на масив дійсних чисел
$newTableau = [];
$result = new \stdClass();
// елементи масиву мають бути додатними або нульовими десятковими числами
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// поміщаємо число типу float у newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// фіксуємо помилку
$result->erreur = TRUE;
// програма завершується
return $result;
}
}
// повертаємо результат
$result->erreur = FALSE;
if (!\is_array($value)) {
// одне значення
$result->value = $newTableau[0];
} else {
// список значень
$result->value = $newTableau;
}
return $result;
}
// гетери та сеттери
…
}
Коментарі
- рядок 23: метод [setFromJsonFile] слугує для ініціалізації атрибутів класу [TaxAdminData] на основі файлу jSON, переданого як параметр. Атрибути файлу jSON повинні існувати під тими самими іменами у класі;
- рядок 25: цю роботу виконує батьківський клас;
- рядок 27: батьківський клас перевіряє, чи всі атрибути дочірнього класу були ініціалізовані;
- рядки 29–42: локально перевіряється, чи всі атрибути мають дійсне додатне або нульове значення. Ця перевірка вже обговорювалася в розділі «Посилання» версії 03;
13.3.3. Рівень [dao]
Тепер ми можемо написати код, який перенесе дані з текстового файлу [taxadmindata.json] у таблиці [tbtranches, tbconstantes] бази даних MySQL [dbimpots-2019]. Ми будемо дотримуватися такої архітектури:


Рівень [dao] реалізує наступний інтерфейс [InterfaceDao4TransferAdminDataFromFile2Database]:
<?php
// простір імен
namespace Application;
interface InterfaceDao4TransferAdminData2Database {
public function transferAdminData2Database(): void;
}
Коментарі
- рядок 8: метод [transferAdminData2Database] призначений для зберігання даних податкової адміністрації в базі даних;
Інтерфейс [InterfaceDao4TransferAdminData2Database] буде реалізовано наступним класом [DaoTransferAdminDataFromJsonFile2Database]:
<?php
// простір імен
namespace Application;
// визначення класу TransferAdminDataFromFile2DatabaseDao
class DaoTransferAdminDataFromJsonFile2Database implements InterfaceDao4TransferAdminData2Database {
// атрибути цільової бази даних
private $database;
// дані податкової адміністрації
private $taxAdminData;
// виробник
public function __construct(string $databaseFilename, string $taxAdminDataFilename) {
// зберігається конфігурація бази даних
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// зберігаються податкові дані
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// передає дані про податкові ставки з текстового файлу
// до бази даних
public function transferAdminData2Database(): void {
// робота з базою даних
$database = $this->database;
try {
// відкривається з'єднання з базою даних
$connexion = new \PDO($database->getDsn(), $database->getId(), $database->getPwd());
// потрібно, щоб при кожній помилці SGBD генерувалося виключення
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// запускаємо транзакцію
$connexion->beginTransaction();
// заповнюємо таблицю податкових шкал
$this->fillTableTranches($connexion);
// заповнюється таблиця констант
$this->fillTableConstantes($connexion);
// транзакція успішно завершується
$connexion->commit();
} catch (\PDOException $ex) {
// чи є транзакція, що виконується?
if (isset($connexion) && $connexion->inTransaction()) {
// завершення транзакції з помилкою
$connexion->rollBack();
}
// видається виняток у код, що викликав
throw new ExceptionImpots($ex->getMessage());
} finally {
// закрито з'єднання
$connexion = NULL;
}
}
// заповнення таблиці податкових шкал
private function fillTableTranches($connexion): void {
…
}
// заповнення таблиці констант
private function fillTableConstantes($connexion): void {
…
}
}
Коментарі
Тут ми використовуємо те, що вивчили в розділі про MySQL.
- рядок 7: клас [DaoTransferAdminDataFromJsonFile2Database] реалізує інтерфейс [InterfaceDao4TransferAdminData2Database];
- рядок 9: атрибут [$database] є об’єктом типу [Database], що інкапсулює дані файлу [database.json];
- рядок 11: атрибут [$taxAdminData] є об’єктом типу [TaxAdminData], що інкапсулює дані файлу [taxadmindata.json];
- рядки 14–19: конструктор отримує як параметри імена файлів [database.json, taxadmindata.json];
- рядок 16: ініціалізація атрибута [$database];
- рядок 18: ініціалізація атрибута [$taxAdminData];
- рядок 23: реалізується єдиний метод інтерфейсу [InterfaceDao4TransferAdminData2Database];
- рядки 26–38: заповнюємо таблиці [tbtranches, tbconstantes] у два етапи:
- рядок 34: спочатку заповнюється таблиця [tbtranches]. Це відбувається в рамках транзакції (рядки 32, 38). Метод [fillTableTranches] (рядок 55) генерує виняток, щойно щось йде не так. У цьому випадку виконання продовжується з блоком catch / finally у рядках 39–50;
- рядок 36: таблицю [tbconstantes] заповнюють аналогічним чином за допомогою методу [fillTableConstantes] (рядок 60);
- рядки 39–47: випадок, коли кодом було згенеровано виняток;
- рядки 41–44: якщо транзакція існує, її скасовують;
- рядок 46: генерується виняток типу [ExceptionImpots] із повідомленням про вихідний виняток, який, у свою чергу, може бути будь-якого типу;
- рядки 47–50: у блоці [finally] з’єднання закривається;
Код методу [fillTableTranches] такий:
private function fillTableTranches($connexion): void {
// скорочення для БД
$database = $this->database;
// дані для введення в базу даних
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
// очищення таблиці на випадок, якщо в ній щось є
$statement = $connexion->prepare("delete from " . $database->getTableTranches());
$statement->execute();
// підготовка записів для вставки
$sqlInsert = "insert into {$database->getTableTranches()} "
. "({$database->getColLimites()}, {$database->getColCoeffR()},"
. " {$database->getColCoeffN()}) values (:limites, :coeffR, :coeffN)";
$statement = $connexion->prepare($sqlInsert);
// виконуємо підготовлену команду із значеннями податкових шкал
for ($i = 0; $i < count($limites); $i++) {
$statement->execute([
"limites" => $limites[$i],
"coeffR" => $coeffR[$i],
"coeffN" => $coeffN[$i]]);
}
}
Коментарі
- рядок 1: метод [fillTableTranches] отримує як параметр відкрите з’єднання. Крім того, відомо, що в межах цього з’єднання розпочалася транзакція;
- рядки 5–7: значення, які потрібно вставити в таблицю, надаються атрибутом [$taxAdminData];
- рядки 9–10: видаляється поточний вміст таблиці [tbtranches];
- рядки 12–15: готується вставлення рядків у таблицю. Тут використовуються імена стовпців, надані атрибутом [$database];
- рядки 17–22: виконується стільки разів, скільки потрібно, інструкція вставки, підготовлена в рядках 12–15;
Код методу [fillTableConstantes] такий:
private function fillTableConstantes($connexion): void {
// скорочений варіант
$database = $this->database;
// очищаємо таблицю на випадок, якщо в ній щось є
$statement = $connexion->prepare("delete from {$database->getTableConstantes()}");
$statement->execute();
// підготовка вставки
$taxAdminData = $this->taxAdminData;
$sqlInsert = "insert into {$database->getTableConstantes()}"
. " ({$database->getColPlafondQfDemiPart()},"
. " {$database->getColPlafondRevenusCelibatairePourReduction()},"
. " {$database->getColPlafondRevenusCouplePourReduction()},"
. " {$database->getColValeurReducDemiPart()},"
. " {$database->getColPlafondDecoteCelibataire()},"
. " {$database->getColPlafondDecoteCouple()},"
. " {$database->getColPlafondImpotCelibatairePourDecote()},"
. " {$database->getColPlafondImpotCouplePourDecote()},"
. " {$database->getColAbattementDixPourcentMax()},"
. " {$database->getColAbattementDixPourcentMin()})"
. " values ("
. ":plafondQfDemiPart,"
. ":plafondRevenusCelibatairePourReduction,"
. ":plafondRevenusCouplePourReduction,"
. ":valeurReducDemiPart,"
. ":plafondDecoteCelibataire,"
. ":plafondDecoteCouple,"
. ":plafondImpotCelibatairePourDecote,"
. ":plafondImpotCouplePourDecote,"
. ":abattementDixPourcentMax,"
. ":abattementDixPourcentMin)";
$statement = $connexion->prepare($sqlInsert);
// виконується підготовлений запит
$statement->execute([
"plafondQfDemiPart" => $taxAdminData->getPlafondQfDemiPart(),
"plafondRevenusCelibatairePourReduction" => $taxAdminData->getPlafondRevenusCelibatairePourReduction(),
"plafondRevenusCouplePourReduction" => $taxAdminData->getPlafondRevenusCouplePourReduction(),
"valeurReducDemiPart" => $taxAdminData->getValeurReducDemiPart(),
"plafondDecoteCelibataire" => $taxAdminData->getPlafondDecoteCelibataire(),
"plafondDecoteCouple" => $taxAdminData->getPlafondDecoteCouple(),
"plafondImpotCelibatairePourDecote" => $taxAdminData->getPlafondImpotCelibatairePourDecote(),
"plafondImpotCouplePourDecote" => $taxAdminData->getPlafondImpotCouplePourDecote(),
"abattementDixPourcentMax" => $taxAdminData->getAbattementDixPourcentMax(),
"abattementDixPourcentMin" => $taxAdminData->getAbattementDixPourcentMin()
]);
}
Коментарі
- рядок 1: метод [fillTableConstantes] отримує як параметр відкрите з’єднання. Крім того, відомо, що в рамках цього з’єднання розпочалася транзакція;
- рядки 5–6: таблиця [tbconstantes] очищується;
- рядки 9–31: підготовка команди вставки SQL. Вона є складною, оскільки в цій операції вставки потрібно ініціалізувати 10 стовпців, а імена стовпців необхідно отримати з атрибута [$database];
- рядки 33–44: виконання команди вставки. Вставити потрібно лише один рядок. І тут знову код ускладнюється тим, що значення, які потрібно вставити, доводиться шукати в атрибуті [$taxAdminData];
13.3.4. Головний скрипт


Головний скрипт використовує рівень [dao] для передачі даних:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// обробка помилок за допомогою PHP
// ini_set("display_errors", "0");
// включення інтерфейсів та класів
require_once __DIR__ . "/../Entities/BaseEntity.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao.php";
require_once __DIR__ . "/../Dao/TraitDao.php";
require_once __DIR__ . "/../Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../Dao/DaoTransferAdminDataFromJsonFile2Database.php";
//
// визначення констант
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
//
try {
// створення шару [dao]
$dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
// передача даних у базу
$dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
// виведення повідомлення про помилку
print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// кінець
print "Terminé\n";
exit;
Коментарі
- рядки 12–21: завантаження класів та інтерфейсів додатка;
- рядки 24–24: два файли jSON;
- рядок 30: створюється екземпляр шару [dao], передаючи конструктору обидва файли jSON;
- рядок 32: виконується передача даних;
Коли ми виконуємо цей код, у базі даних отримуємо такий результат:

У стовпці [3] видно значення, присвоєні MySQL первинному ключу [id]. Нумерація починається з 1. Наведений вище знімок екрана було отримано після декількох запусків скрипта.


13.4. Розрахунок податку

13.4.1. Архітектура
У версії 04 програми для розрахунку податку використовувалася багаторівнева архітектура:

Рівень [dao] реалізує інтерфейс [InterfaceDao]. Ми створили клас, що реалізує цей інтерфейс:
- [DaoImpotsWithTaxAdminDataInJsonFile], який отримував податкові дані з файлу jSON. Це була версія 04;
Ми реалізуємо інтерфейс [InterfaceDao] за допомогою нового класу [DaoImpotsWithTaxAdminDataInDatabase], який буде отримувати дані з податкової адміністрації з бази даних MySQL. Рівень [dao], як і раніше, записуватиме результати та помилки у текстові файли, а також отримуватиме дані про платників податків із текстового файлу. Тільки цього разу цими текстовими файлами будуть файли jSON. Крім того, ми знаємо, що якщо ми й надалі дотримуватимемося інтерфейсу [InterfaceDao], шар [métier] не потребуватиме змін.

13.4.2. Елемент [TaxPayerData]

Клас [TaxPayerData] слугує для інкапсуляції в клас даних з наступного файлу jSON [taxpayersdata.json]:
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
Клас [TaxPayerData] має такий вигляд:
<?php
// простір імен
namespace Application;
// клас даних
class TaxPayerData extends BaseEntity {
// дані, необхідні для розрахунку податку платника податків
protected $marié;
protected $enfants;
protected $salaire;
// результати розрахунку податку
protected $impôt;
protected $surcôte;
protected $décôte;
protected $réduction;
protected $taux;
// методи getter та setter
…
}
Коментарі
- рядок 7: клас [TaxPayerData] успадковує клас [BaseEntity]. Оскільки методів його батьківського класу достатньо, клас [TaxPayerData] сам не визначає жодних методів. Нагадаємо, що атрибути класу [TaxPayerData] ідентичні атрибутам файлу jSON [taxpayersdata.json];
13.4.3. Рівень [dao]
13.4.3.1. Особливість [TraitDao]
Лінія [TraitDao] реалізує частину інтерфейсу [InterfaceDao]. Нагадаємо його:
<?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;
}
Трайт [TraitDao] реалізує методи [getTaxPayersData, saveResults] інтерфейсу [InterfaceDao]. Оскільки між версіями 04 та 05 було змінено визначення сутності [TaxPayerData], нам потрібно переглянути код [TraitDao]:
<?php
// простір імен
namespace Application;
trait TraitDao {
// читання даних платників податків
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// отримання даних про платників податків у таблицю
$baseEntity = new BaseEntity();
$baseEntity->setFromJsonFile($taxPayersFilename);
$arrayOfAttributes = $baseEntity->getArrayOfAttributes();
// масив даних про платників податків
$taxPayersData = [];
// масив помилок
$errors = [];
// цикл по масиву атрибутів елементів типу [TaxPayerData]
$i = 0;
foreach ($arrayOfAttributes as $attributesOfTaxPayerData) {
// перевірка
$error = $this->check($attributesOfTaxPayerData);
if (!$error) {
// платник з +
$taxPayersData[] = (new TaxPayerData())->setFrOmArrayOfAttributes($attributesOfTaxPayerData);
} else {
// помилка + - фіксується номер недійсних даних
$error = ["numéro" => $i] + $error;
$errors[] = $error;
}
// далі
$i++;
}
// зберігаємо помилки у файлі json
$string = "";
foreach ($errors as $error) {
$string .= \json_encode($error, JSON_UNESCAPED_UNICODE) . "\n";
}
$this->saveString($errorsFilename, $string);
// результат функції
return $taxPayersData;
}
private function check(array $attributesOfTaxPayerData): array {
// перевіряються дані з [$taxPayerData]
// список атрибутів із помилками
$attributes = [];
// сімейний стан має бути «так» або «ні»
$marié = trim(strtolower($attributesOfTaxPayerData["marié"]));
$erreur = ($marié !== "oui" and $marié !== "non");
if ($erreur) {
// фіксується помилка
$attributes[] = ["marié" => $marié];
}
// кількість дітей має бути додатним цілим числом або нулем
$enfants = trim($attributesOfTaxPayerData["enfants"]);
if (!preg_match("/^\d+$/", $enfants)) {
// позначено помилку
$erreur = TRUE;
$attributes[] = ["enfants" => $enfants];
} else {
$enfants = (int) $enfants;
}
// зарплата має бути додатним цілим числом або нулем (без центів)
$salaire = trim($attributesOfTaxPayerData["salaire"]);
if (!preg_match("/^\d+$/", $salaire)) {
// зафіксовано помилку
$erreur = TRUE;
$attributes[] = ["salaire" => $salaire];
} else {
$salaire = (int) $salaire;
}
// помилка?
if ($erreur) {
// повернення з помилкою
return ["erreurs" => $attributes];
} else {
// повернення без помилки
return [];
}
}
// запис результатів
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// запис таблиці [$taxPayersData] у текстовий файл [$resultsFileName]
// якщо текстовий файл [$resultsFileName] не існує, його створюють
// формування рядка jSON із результатів
$string = "[" . implode(",
", $taxPayersData) . "]";
// запис цього рядка
$this->saveString($resultsFilename, $string);
}
// запис результатів з масиву у текстовий файл
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]");
}
}
}
Коментарі
- [TraitDao] реалізує методи [getTaxPayersData] (рядок 9) та [saveResults] (рядок 86) інтерфейсу [InterfaceDao];
- рядок 9: метод [getTaxPayersData] отримує як параметри:
- [$taxPayersFilename]: ім’я файлу jSON із даними платників податків [taxpayersdata.json];
- [$errorsFilename]: ім’я файлу jSON з помилками [errors.json];
- рядки 11–13: вміст файлу jSON з даними платників податків переноситься в асоціативний масив [$arrayOfAttributes]. Якщо файл jSON виявився непридатним для обробки, було згенеровано виняток [ExceptionImpots];
- рядок 15: масив [$taxPayersData] міститиме дані платників податків, інкапсульовані в об’єкти типу [TaxPayerData];
- рядок 17: помилки будуть накопичуватися в масиві [$errors];
- рядки 99–33: формування таблиці [$taxPayersData];
- рядок 22: перед інкапсуляцією в тип [TaxPayerData] дані перевіряються. Метод [check] повертає:
- масив [‘erreurs’=>[…]] з атрибутами, що містять помилки, якщо дані є некоректними;
- порожній масив, якщо дані правильні;
- рядок 25: випадок, коли дані є правильними. Створюється новий об’єкт [TaxPayerData], який додається до масиву [$taxPayersData];
- рядки 26–30: випадок, коли дані є недійсними. У повідомленні про помилку зазначається номер помилкового об’єкта [TaxPayerData] у файлі jSON, щоб користувач міг його знайти, після чого помилка додається до масиву [$errors];
- рядки 35–39: записуються помилки, виявлені у файлі jSON, переданому як параметр у файлі [$errorsFilename], рядок 9;
- рядок 41: повертається масив створених об’єктів [TaxPayerData]: це було метою методу;
- рядки 44–83: приватний метод [check] перевіряє правильність параметрів [marié, enfants, salaire] масиву [$attributesOfTaxPayerData], переданого як параметр у рядку 44. Якщо є помилкові атрибути, вона накопичує їх у масиві [$attributes] (рядки 47, 53, 60, 70) у вигляді масиву [‘attribut erroné’=> valeur de l’attribut erroné];
- рядок 78: якщо є помилки, повертається масив [‘erreurs’=>$attributes];
- рядок 81: якщо помилок немає, повертається порожній масив помилок;
- рядки 86–93: реалізація методу [saveResults] інтерфейсу [InterfaceDao];
- рядок 90: формується рядок jSON для запису у файл jSON [$resultsFilename], переданий як параметр у рядку 86. Необхідно сформувати рядок jSON з масиву:
- кожен елемент масиву відокремлений від наступного комою та перенесенням рядка;
- весь масив укладено в квадратні дужки [];
- рядок 92: рядок jSON записується у файл jSON [$resultsFilename];
13.4.3.2. Клас [DaoImpotsWithTaxAdminDataInDatabase]
Клас [DaoImpotsWithTaxAdminDataInDatabase] реалізує інтерфейс [InterfaceDao] наступним чином:
<?php
// простір імен
namespace Application;
// визначення класу ImpotsWithDataInDatabase
class DaoImpotsWithTaxAdminDataInDatabase implements InterfaceDao {
// використання риси
use TraitDao;
// об’єкт типу TaxAdminData, що містить дані про податкові групи
private $taxAdminData;
// об’єкт типу [Database], що містить характеристики BD
private $database;
// конструктор
public function __construct(string $databaseFilename) {
// зберігається конфігурація jSON бази даних
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// готуємо атрибут
$this->taxAdminData = new TaxAdminData();
try {
// відкриваємо з'єднання з базою даних
$connexion = new \PDO(
$this->database->getDsn(),
$this->database->getId(),
$this->database->getPwd());
// потрібно, щоб при кожній помилці SGBD генерувалося виключення
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// запускаємо транзакцію
$connexion->beginTransaction();
// заповнюємо таблицю податкових шкал
$this->getTranches($connexion);
// заповнюється таблиця констант
$this->getConstantes($connexion);
// транзакція успішно завершується
$connexion->commit();
} catch (\PDOException $ex) {
// чи є транзакція, що виконується?
if (isset($connexion) && $connexion->inTransaction()) {
// завершення транзакції з помилкою
$connexion->rollBack();
}
// видається виняток у код, що викликав
throw new ExceptionImpots($ex->getMessage());
} finally {
// закрито з'єднання
$connexion = NULL;
}
}
// читання даних із бази
private function getTranches($connexion): void {
…
}
// читання таблиці констант
private function getConstantes($connexion): void {
…
}
// повертає дані, необхідні для розрахунку податку
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Коментарі
- рядок 4: зберігається простір імен, який вже використовується для інших реалізацій шару [dao];
- рядок 7: клас [DaoImpotsWithTaxAdminDataInDatabase] реалізує інтерфейс [InterfaceDao];
- рядок 9: імпортується риса [TraitDao]. Відомо, що ця риса реалізує частину інтерфейсу. Єдиний метод, який залишається реалізувати, — це метод [getTaxAdminData] у рядках 62–64. Цей метод лише повертає приватний атрибут [taxAdminData] із рядка 11. Звідси випливає, що конструктор повинен ініціалізувати цей атрибут. Це його єдина роль;
- рядок 16: конструктор отримує як єдиний параметр [$databaseFilename], що є іменем файлу jSON [database.json], який визначає базу даних MySQL [dbimpots-2019] ;
- рядок 18: файл jSON [$databaseFilename] використовується для створення об’єкта типу [Database], який побудовано та збережено в атрибуті [$database] у рядку 13. Якщо файл jSON не вдалося правильно обробити, було згенеровано виняток [ExceptionImpots];
- рядок 20: створюється об’єкт [$this→taxAdminData], який конструктор повинен ініціалізувати;
- рядки 22–26: відкривається з’єднання з базою даних. Зверніть увагу на позначення [\PDO], яке позначає клас [PDO] з PHP. Адже, оскільки ми перебуваємо в просторі імен [Application], якби ми просто написали [PDO], до цього відносного імені додався б префікс поточного простору імен, і, отже, отримався б клас [Application\PDO], якого не існує;
- рядок 28: у разі помилки SGBD запустить \PDOException (рядок 37);
- рядок 30: запускається транзакція. Вона не є особливо корисною, оскільки будуть виконані лише два команди SQL, які не змінюють базу даних. Проте це робиться для ізоляції від інших користувачів бази даних;
- рядок 32: читання таблиці податкових шкал [tbtranches] здійснюється за допомогою приватного методу [getTranches] із рядка 52;
- рядок 34: читання таблиці розрахункових констант [tbconstantes] здійснюється за допомогою приватного методу [getConstantes] з рядка 57;
- рядок 36: якщо виконано цей рядок, це означає, що все пройшло успішно. Отже, транзакцію підтверджують;
- рядки 37–42: якщо ми дійшли до цього місця, значить, стався виняток. Тому ми скасовуємо транзакцію, якщо така була в процесі виконання (рядки 39–42). Рядок 44: щоб забезпечити однорідність винятків, ми повторно генеруємо повідомлення про отриманий виняток, цього разу у вигляді винятку типу [ExceptionImpots];
- рядки 45–48: у будь-якому випадку (незалежно від того, чи стався виняток) закриваємо з’єднання;
Метод [getTranches] має такий вигляд:
private function getTranches($connexion): void {
// скорочення
$database = $this->database;
$taxAdminData = $this->taxAdminData;
// підготовка запиту SELECT
$statement = $connexion->prepare(
"select {$database->getColLimites()}," .
" {$database->getColCoeffR()}," .
" {$database->getColCoeffN()}" .
" from {$database->getTableTranches()}");
// виконання підготовленого запиту із значеннями податкових шкал
$statement->execute();
// аналізуємо результат
$limites = [];
$coeffR = [];
$coeffN = [];
// заповнення трьох таблиць
while ($tranche = $statement->fetch(\PDO::FETCH_OBJ)) {
$limites[] = (float) $tranche->{$database->getColLimites()};
$coeffR[] = (float) $tranche->{$database->getColCoeffR()};
$coeffN[] = (float) $tranche->{$database->getColCoeffN()};
}
// зберігаємо дані в атрибуті [$taxAdminData] класу
$taxAdminData->setFromArrayOfAttributes([
"limites" => $limites,
"coeffR" => $coeffR,
"coeffN" => $coeffN
]);
}
Коментарі
- рядок 1: метод отримує як параметр [$connexion] — це відкрите з’єднання, у якому триває транзакція;
- рядки 2–4: створюються два скорочення, щоб уникнути необхідності писати [$this->database] та [$taxAdminData = $this->taxAdminData] у всьому коді. Тут маємо копії посилань на об’єкти, а не копії самих об’єктів;
- рядки 6–10: готується команда SELECT, яка потім виконується в рядку 12;
- рядки 13–22: використовується результат SELECT. Отримана інформація накопичується у трьох масивах [limites, coeffR, coeffN];
- рядки 24–28: ці три масиви використовуються для ініціалізації атрибута [$this->taxAdminData] класу;
Приватний метод [getConstantes] має такий вигляд:
private function getConstantes($connexion): void {
// скорочення
$database = $this->database;
$taxAdminData = $this->taxAdminData;
// підготовка запиту SELECT
$select = "select {$database->getColPlafondQfDemiPart()}," .
"{$database->getColPlafondRevenusCelibatairePourReduction()}," .
"{$database->getColPlafondRevenusCouplePourReduction()}," . "{$database->getColValeurReducDemiPart()}," .
"{$database->getColPlafondDecoteCelibataire()}," . "{$database->getColPlafondDecoteCouple()}," .
"{$database->getColPlafondImpotCelibatairePourDecote()}," . "{$database->getColPlafondImpotCouplePourDecote()}," .
"{$database->getColAbattementDixPourcentMax()}," . "{$database->getColAbattementDixPourcentMin()}" .
" from {$database->getTableConstantes()}";
$statement = $connexion->prepare($select);
// виконується підготовлений запит
$statement->execute();
// використовуємо результат — тут лише один рядок
$row = $statement->fetch(\PDO::FETCH_OBJ);
// ініціалізуємо атрибут [$taxAdminData]
$taxAdminData->setPlafondQfDemiPart($row->{$database->getColPlafondQfDemiPart()});
$taxAdminData->setPlafondRevenusCelibatairePourReduction(
$row->{$database->getColPlafondRevenusCelibatairePourReduction()});
$taxAdminData->setPlafondRevenusCouplePourReduction($row->{$database->getColPlafondRevenusCouplePourReduction()});
$taxAdminData->setValeurReducDemiPart($row->{$database->getColValeurReducDemiPart()});
$taxAdminData->setPlafondDecoteCelibataire($row->{$database->getColPlafondDecoteCelibataire()});
$taxAdminData->setPlafondDecoteCouple($row->{$database->getColPlafondDecoteCouple()});
$taxAdminData->setPlafondImpotCelibatairePourDecote($row->{$database->getColPlafondImpotCelibatairePourDecote()});
$taxAdminData->setPlafondImpotCouplePourDecote($row->{$database->getColPlafondImpotCouplePourDecote()});
$taxAdminData->setAbattementDixPourcentMax($row->{$database->getColAbattementDixPourcentMax()});
$taxAdminData->setAbattementDixPourcentMin($row->{$database->getColAbattementDixPourcentMin()});
}
Коментарі
- рядок 1: метод отримує як параметр [$connexion], який є відкритим з’єднанням, у якому триває транзакція;
- рядки 2–4: створюються два скорочення, щоб уникнути необхідності писати [$this->database] та [$taxAdminData = $this->taxAdminData] у всьому коді. Тут маємо копії посилань на об’єкти, а не копії самих об’єктів;
- рядки 6–15: готується команда SELECT, яка потім виконується в рядку 15;
- рядки 17–29: використовується результат SELECT. Отримана інформація використовується для ініціалізації атрибута [$this->taxAdminData] класу;
Примітка: слід зауважити, що клас не залежить від SGBD та MySQL. Саме код, що викликає, визначає SGBD, який використовується через DSN з бази даних.
13.4.4. Рівень [métier]

- ми щойно реалізували рівень [dao] (3);
- оскільки ми дотрималися інтерфейсу [InterfaceDao], шар [métier] (2) теоретично може залишитися без змін. Однак ми змінили не лише шар [dao]. Ми також змінили об’єкти, які є спільними для всіх шарів;
Рівень [métier] реалізує наступний інтерфейс [InterfaceMetier]:
<?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;
}
- рядок 12: метод [executeBatchImpots] тепер використовує файл jSON [$taxPayersFileName], тоді як у версії 04 це був звичайний текстовий файл;
У версії 04 метод [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);
}
- рядок 15 тепер є помилковим. У новому визначенні класу [TaxPayerData] метод [setMontant] більше не існує;
У версії 05 метод [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->setFromArrayOfAttributes($result);
// заносимо результат у таблицю результатів
$results [] = $taxPayerData;
}
// запис результатів
$this->dao->saveResults($resultsFileName, $results);
}
Коментарі
- рядок 15: замість використання окремих сеттерів класу [TaxPayerData] використовується його глобальний сеттер [setFromArrayOfAttributes];
- решту коду змінювати не потрібно;
13.4.5. Головний скрипт

- ми щойно реалізували шари [dao] (3) та [métier] (2);
- залишилося написати головний скрипт (1);
Головний скрипт аналогічний скрипту версії 04:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// обробка помилок за допомогою PHP
//ini_set("display_errors", "0");
// включення інтерфейсів та класів
require_once __DIR__ . "/../Entities/BaseEntity.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao.php";
require_once __DIR__ . "/../Dao/TraitDao.php";
require_once __DIR__ . "/../Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once __DIR__ . "/../Métier/InterfaceMetier.php";
require_once __DIR__ . "/../Métier/Metier.php";
//
// визначення констант
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
const RESULTS_FILENAME = "../Data/resultats.json";
const ERRORS_FILENAME = "../Data/errors.json";
const TAXPAYERSDATA_FILENAME = "../Data/taxpayersdata.json";
try {
// створення шару [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// створення шару [métier]
$métier = new Metier($dao);
// розрахунок податків у пакетному режимі
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// виведення повідомлення про помилку
print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// кінець
print "Terminé\n";
exit;
Коментарі
- рядки 12–22: завантаження всіх файлів версії 05;
- рядки 25–29: імена різних файлів jSON програми;
- рядок 33: побудова шару [dao];
- рядок 35: побудова шару [métier];
- рядок 37: виклик методу [executeBatchImpots] шару [métier];
Результати
Додаток створює два файли jSON:
- [resultats.json]: результати різних розрахунків податків;
- [errors.json]: у якому вказано помилки, виявлені у файлах jSON та [taxpayersdata.json];
Файл [errors.json] має такий вигляд:
{
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
Це означає, що у файлі [taxpayersdata.json] елемент № 1 таблиці платників податків містить помилку. Файл [taxpayersdata.json] мав такий вигляд:
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
{
"marié": "ouix",
"enfants": "2x",
"salaire": "55555x"
},
{
"marié": "oui",
"enfants": "2",
"salaire": 50000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 50000
},
{
"marié": "non",
"enfants": 2,
"salaire": 100000
},
{
"marié": "non",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 5,
"salaire": 100000
},
{
"marié": "non",
"enfants": 0,
"salaire": 100000
},
{
"marié": "oui",
"enfants": 2,
"salaire": 30000
},
{
"marié": "non",
"enfants": 0,
"salaire": 200000
},
{
"marié": "oui",
"enfants": 3,
"salaire": 20000
}
]
Файл результатів [resultats.json] має такий вигляд:
[
{
"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é": "non",
"enfants": 3,
"salaire": 100000,
"impôt": 16782,
"surcôte": 7176,
"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é": "oui",
"enfants": 5,
"salaire": 100000,
"impôt": 4230,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
},
{
"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": 20000,
"impôt": 0,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0
}
]
Ці результати відповідають результатам версії 04.
13.5. Тести [Codeception]
Як це було зроблено в розділі, присвяченому версії 04, ми напишемо тести [Codeception] для версії 05.

13.5.1. Тест рівня [dao]
Тест [DaoTest.php] виглядає наступним чином:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// кореневі каталоги
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// включення інтерфейсів та класів
require_once ROOT . "/Entities/BaseEntity.php";
require_once ROOT . "/Entities/TaxAdminData.php";
require_once ROOT . "/Entities/TaxPayerData.php";
require_once ROOT . "/Entities/Database.php";
require_once ROOT . "/Entities/ExceptionImpots.php";
require_once ROOT . "/Utilities/Utilitaires.php";
require_once ROOT . "/Dao/InterfaceDao.php";
require_once ROOT . "/Dao/TraitDao.php";
require_once ROOT . "/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/Métier/InterfaceMetier.php";
require_once ROOT . "/Métier/Metier.php";
// сторонні бібліотеки
require_once VENDOR . "/autoload.php";
// визначення констант
const DATABASE_CONFIG_FILENAME = ROOT ."/Data/database.json";
const TAXADMINDATA_FILENAME = ROOT ."/Data/taxadmindata.json";
const RESULTS_FILENAME = ROOT ."/Data/resultats.json";
const ERRORS_FILENAME = ROOT ."/Data/errors.json";
const TAXPAYERSDATA_FILENAME = ROOT ."/Data/taxpayersdata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// створення шару [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// тестування
public function testTaxAdminData() {
// константи обчислення
$this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
…
}
}
Коментарі
- рядки 9–33: визначення середовища тесту. Ми використовуємо те саме середовище, що й у головному скрипті [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase], описаному в розділі «Посилання»;
- рядки 39–44: побудова шару [dao];
- рядок 43: атрибут [$this→taxAdminData] містить дані, що підлягають тестуванню;
- рядки 47–51: метод [testTaxAdminData] — це той, що описаний у параграфі «посилання»;
Результати тесту такі:

13.5.2. Тестування шару [métier]
Тест [MetierTest.php] виглядає так:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// простір імен
namespace Application;
// кореневі каталоги
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// підключення інтерфейсів та класів
require_once ROOT . "/Entities/BaseEntity.php";
require_once ROOT . "/Entities/TaxAdminData.php";
require_once ROOT . "/Entities/TaxPayerData.php";
require_once ROOT . "/Entities/Database.php";
require_once ROOT . "/Entities/ExceptionImpots.php";
require_once ROOT . "/Utilities/Utilitaires.php";
require_once ROOT . "/Dao/InterfaceDao.php";
require_once ROOT . "/Dao/TraitDao.php";
require_once ROOT . "/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/Métier/InterfaceMetier.php";
require_once ROOT . "/Métier/Metier.php";
// сторонні бібліотеки
require_once VENDOR . "/autoload.php";
// визначення констант
const DATABASE_CONFIG_FILENAME = ROOT ."/Data/database.json";
const TAXADMINDATA_FILENAME = ROOT ."/Data/taxadmindata.json";
const RESULTS_FILENAME = ROOT ."/Data/resultats.json";
const ERRORS_FILENAME = ROOT ."/Data/errors.json";
const TAXPAYERSDATA_FILENAME = ROOT ."/Data/taxpayersdata.json";
class MetierTest extends \Codeception\Test\Unit {
// бізнес-шар
private $métier;
public function __construct() {
parent::__construct();
// створення шару [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_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 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"]);
}
}
Коментарі
- рядки 9–33: визначення середовища тестування. Ми використовуємо те саме середовище, що й у головному скрипті [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase], описаному в розділі «Посилання»;
- рядки 39–45: побудова шарів [dao] та [métier];
- рядок 44: атрибут [$this→métier] посилається на шар [métier];
- рядки 47–64: методи [test1, test2…, test11] — це ті, що описані в розділі «посилання»;
Результати тесту такі:
