11. Вправа IMPOTS із сервісом WEB та трирівневою архітектурою
Ми повернемося до вправи IMPOTS (див. параграфи 4.2, 4.3, 6) і перетворимо її на клієнт-серверний додаток. Серверний скрипт буде розбитий на три елементи:
- шар під назвою [dao] (Data Access Objects), який відповідатиме за взаємодію з базою даних MySQL
- шар під назвою [métier], який здійснюватиме розрахунок податку
- шар [web], який відповідатиме за взаємодію з веб-клієнтами.
![]() |
Клієнтський скрипт [1]:
- передає серверному скрипту три параметри ($marié, $enfants, $salaire), необхідні для розрахунку податку
- виводить відповідь сервера на консоль
Серверний скрипт [2] складається з серверного рівня [web].
- під час початку нової сесії клієнта він розмістить у масивах дані з бази даних MySQL та [dbimpots]. Для цього він звернеться до шару [dao]. Таким чином сформовані масиви будуть розміщені в сесії клієнта, щоб їх можна було використовувати в подальших запитах клієнта.
- Під час запиту клієнта він передасть три набори даних ($marié, $enfants, $salaire) на рівень [métier], який обчислить податок $impot.
- Скрипт сервера поверне розрахований податок $impôt.
11.1. Клієнтський скрипт (clients_impots_05_web)
Клієнтський скрипт буде клієнтом веб-сервісу з розрахунку податку. Він надішле (POST) на сервер параметри у вигляді:
params=$marié,$enfants,$salaire, де
- $marié — це рядок oui або non,
- $enfants — кількість дітей,
- $salaire — зарплату платника податків
Він знаходить три вищезазначені параметри у текстовому файлі [data.txt] у форматі (одружений, діти, заробітна плата):
Клієнтський скрипт
- прочитає текстовий файл [data.txt] по рядках
- надішле рядок params=$marié,$enfants,$salaire до веб-сервісу розрахунку податку
- отримає відповідь від сервісу. Вона може мати два варіанти:
- запише відповідь сервера у текстовий файл [resultats.txt] в одному з двох таких форматів:
Код клієнтського скрипта такий:
<?php
// клієнт податків
// обробка помилок
ini_set("display_errors", "off");
// ---------------------------------------------------------------------------------
// клас допоміжних функцій
class Utilitaires {
function cutNewLinechar($ligne) {
...
}
}
// main -----------------------------------------------------
// визначення констант
$DATA = "data.txt";
$RESULTATS = "resultats.txt";
// дані сервера
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/impots_05_web.php";
// параметри платників податків (сімейний стан, кількість дітей, річний заробіток)
// були розміщені у текстовому файлі $DATA по одному рядку на кожного платника податків
// результати (сімейний стан, кількість дітей, річний дохід, сума податку до сплати)
// або (сімейний стан, кількість дітей, річний дохід, повідомлення про помилку) розміщені у
// текстовий файл $RESULTATS по одному результату на рядок
// клас «Утиліти»
$u = new Utilitaires();
// відкриття файлу з даними платників податків
$data = fopen($DATA, "r");
if (!$data) {
print "Impossible d'ouvrir en lecture le fichier des données [$DATA]\n";
exit;
}
// відкриття файлу результатів
$résultats = fopen($RESULTATS, "w");
if (!$résultats) {
print "Impossible de créer le fichier des résultats [$RESULTATS]\n";
exit;
}
// обробка поточного рядка файлу даних платників податків
while ($ligne = fgets($data, 100)) {
// видаляємо можливий маркер кінця рядка
$ligne = $u->cutNewLineChar($ligne);
// витягуються 3 поля «одружений:діти:зарплата», що утворюють $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// розраховується податок
list($erreur, $impôt) = calculerImpot($HOTE, $PORT, $urlServeur, $cookie, array($marié, $enfants, $salaire));
// записуємо результат
$résultat = $erreur ? "$marié:$enfants:$salaire:$erreur" : "$marié:$enfants:$salaire:$impôt";
fputs($résultats, "$résultat\n");
// наступні дані
}
// закриваємо файли
fclose($data);
fclose($résultats);
// кінець
print "Terminé...\n";
exit;
function calculerImpot($HOTE, $PORT, $urlServeur, &$cookie, $params) {
// підключає клієнта до ($HOTE,$PORT,$urlServeur)
// надсилає файл cookie $cookie, якщо він не порожній. $cookie передається за посиланням
// надсилає $params на сервер
// обробляє єдиний рядок, повернутий сервером
// відкриття з’єднання на порту 80 $HOTE
$connexion = fsockopen($HOTE, $PORT);
// помилка?
if (!$connexion)
return array("erreur lors de la connexion au serveur ($HOTE, $PORT)");
// заголовки (headers) протоколу HTTP повинні закінчуватися порожнім рядком
// POST
fputs($connexion, "POST $urlServeur HTTP/1.1\n");
// Хост
fputs($connexion, "Host: localhost\n");
// З'єднання
fputs($connexion, "Connection: close\n");
// файл cookie надсилається, якщо він не порожній
if ($cookie) {
fputs($connexion, "Cookie: $cookie\n");
}//if
// тепер надсилаємо інструкцію клієнту після її кодування
$infos = "params=" . urlencode(implode(",", $params));
// вказується, який тип інформації буде надіслано
fputs($connexion, "Content-type: application/x-www-form-urlencoded\n");
// надсилається розмір (кількість символів) інформації, що буде надіслана
fputs($connexion, "Content-length: " . strlen($infos) . "\n");
// надсилається порожній рядок
fputs($connexion, "\n");
// надсилається інформація
fputs($connexion, $infos);
// виводиться відповідь веб-сервера
// і дбаємо про те, щоб отримати можливий файл cookie
while ($ligne = fgets($connexion, 1000)) {
// cookie — лише під час першої відповіді
if (!$cookie) {
if (preg_match("/^Set-Cookie: (.*?)\s*$/", $ligne, $champs)) {
$cookie = $champs[1];
}//if
}
// як тільки з’являється порожній рядок, відповідь HTTP завершена
if (trim($ligne) == "") {
break;
}
}//while
// читання рядка результату
$ligne = fgets($connexion, 1000);
// закриваємо з'єднання
fclose($connexion);
// обчислення результату
$erreur="";
$impôt="";
if (preg_match("/^<erreur>(.*?)<\/erreur>\s*$/", $ligne, $champs)) {
$erreur = $champs[1];
} else {
if (preg_match("/^<impot>(.*?)<\/impot>\s*$/", $ligne, $champs)) {
$impôt = $champs[1];
}else{
$erreur="résultat du serveur non exploitable";
}
}
// повернення
return array($erreur, $impôt);
}
Коментарі
Код клієнтського скрипта містить елементи, з якими ми вже знайомі:
- рядки 9–15: клас [Utilitaires] було представлено у розділі 6 версії 3
- рядки 17–68: головна програма аналогічна програмі з версії 1, параграф 4.2. Від неї вона відрізняється лише розрахунком податку, рядок 56.
- рядок 56: функція розрахунку податку приймає такі параметри:
- $HOTE, $PORT, $urlServeur: дозволяють підключитися до веб-сервісу
- $cookie: це сесійний файл cookie. Цей параметр передається за посиланням. Його значення встановлюється функцією розрахунку податку. Під час першого виклику він не має значення. Потім він отримує значення.
- array($marié, $enfants, $salaire): представляє рядок файлу [data.txt]
Функція обчислення податку повертає масив із двох результатів ($erreur, $impôt), де $erreur — це можливе повідомлення про помилку, а $impôt — сума податку.
- рядки 70–134: тут ми маємо класичний HTTP-клієнт, подібний до тих, з якими ми часто стикалися. Зверніть увагу на наступні моменти:
- рядок 83: параметри ($marié, $enfants, $salaire) передаються на сервер за допомогою POST
- рядки 89–91: якщо клієнт має ідентифікатор сеансу, він надсилає його на сервер
- рядок 93: створення параметра params
- рядок 101: надсилання параметра params
- рядки 104–115: клієнт зчитує всі заголовки HTTP, надіслані сервером, доти, доки не зустріне порожній рядок, що позначає кінець заголовків. При цьому він отримує ідентифікатор сеансу у відповіді на свій перший запит.
- рядки 123–125: обробляється можливий рядок у вигляді <erreur>message</erreur>
- рядки 126–128: те саме робимо з можливим рядком у форматі <impot>montant</impot>
- рядок 133: видається результат
11.2. Веб-сервіс для розрахунку податку
Тут нас цікавлять три скрипти, з яких складається сервер:
![]() |
Відповідний проєкт NetBeans виглядає так:
![]() |
У [1] сервер складається з таких скриптів PHP:
- [impots_05_entites] містить класи, що використовуються сервером
- [impots_05_dao] містить класи та інтерфейси шару [dao]
- [impots_05_metier] містить класи та інтерфейси рівня [metier]
- [impots_05_web] містить класи та інтерфейси рівня [dao]
Спочатку ми розглянемо два класи, які використовуються різними рівнями веб-сервісу.
11.2.1. Сутності веб-сервісу (impots_05_entites)
База даних MySQL [dbimpots] містить таблицю [impots], яка містить дані, необхідні для розрахунку податку [1]:
![]() |
Ми збережемо дані з таблиць MySQL та [impots] у масиві об’єктів Tranche, де Tranche є наступним класом:
<?php
// податкова ставка
class Tranche {
// приватні поля
private $limite;
private $coeffR;
private $coeffN;
// методи getter та setter
public function getLimite() {
return $this->limite;
}
public function setLimite($limite) {
$this->limite = $limite;
}
public function getCoeffR() {
return $this->coeffR;
}
public function setCoeffR($coeffR) {
$this->coeffR = $coeffR;
}
public function getCoeffN() {
return $this->coeffN;
}
public function setCoeffN($coeffN) {
$this->coeffN = $coeffN;
}
// конструктор
public function __construct($limite, $coeffR, $coeffN) {
$this->setLimite($limite);
$this->setCoeffR($coeffR);
$this->setCoeffN($coeffN);
}
// toString
public function __toString(){
return "[$this->limite,$this->coeffR,$this->coeffN]";
}
}
Приватні поля [$limite, $coeffR, $coeffN] слугуватимуть для зберігання стовпців [limites, coeffR, coeffN] одного рядка таблиці MySQL [impots].
Крім того, серверний код використовуватиме власний виняток — клас ImpotsException:
- рядок 1: клас [ImpotsException] походить від класу [Exception], попередньо визначеного в PHP 5
- рядок 3: конструктор класу [ImpotsException] приймає два параметри:
- $message: повідомлення про помилку
- $code: код помилки
11.2.2. Рівень [dao] (impots_05_dao)
Рівень [dao] забезпечує доступ до даних бази даних:
![]() |
Рівень [dao] має такий інтерфейс:
Інтерфейс IImpotsDao надає доступ лише до функції getData. Ця функція розміщує в масиві об’єктів Tranche різні рядки таблиці MySQL [dbimpots.impots].
Клас реалізації має такий вигляд:
<?php
// шар DAO
// залежності
require_once "impots_05_entites.php";
// константи
define("TABLE", "impots");
// -----------------------------------------------------------------
// абстрактна реалізація
abstract class ImpotsDaoWithPdo implements IImpotsDao {
// приватні поля
private $dsn;
private $user;
private $passwd;
private $tranches;
// методи getter та setter
public function getDsn() {
return $this->dsn;
}
public function setDsn($dsn) {
$this->dsn = $dsn;
}
public function getUser() {
return $this->user;
}
public function setUser($user) {
$this->user = $user;
}
public function getPasswd() {
return $this->passwd;
}
public function setPasswd($passwd) {
$this->passwd = $passwd;
}
// конструктор
public function __construct($dsn, $user, $passwd) {
// зберігання параметрів
$this->setDsn($dsn);
$this->setUser($user);
$this->setPasswd($passwd);
// отримання даних з SGBD
// підключення ($user, $pwd) до бази даних $dsn
try {
// підключення
$connexion = new PDO($dsn, $user, $passwd, array(PDO::ATTR_PERSISTENT => true));
// зчитування таблиці $TABLE
$requête = "select limites,coeffR,coeffN from " . TABLE;
// виконує запит $requête через з'єднання $connexion
$statement = $connexion->prepare($requête);
$statement->execute();
// обробка результату запиту
while ($colonnes = $statement->fetch()) {
$this->tranches[] = new Tranche($colonnes[0], $colonnes[1], $colonnes[2]);
}
// роз'єднання
$connexion=NULL;
} catch (PDOException $e) {
// повернення з помилкою
throw new ImpotsException($e->getMessage(), 1);
}
}
public function getData(){
return $this->tranches;
}
}
- рядок 5: для реалізації інтерфейсу [IImpotsDao] потрібні класи, визначені у скрипті [impots_05_entites].
- рядок 11: визначення абстрактного класу. Абстрактний клас — це клас, який не можна інстанціювати. Щоб інстанціювати абстрактний клас, його обов’язково потрібно успадкувати. Клас може бути оголошений абстрактним тому, що його неможливо інстанціювати (деякі його методи не визначені) або тому, що його не хочуть інстанціювати. У даному випадку клас [ImpotsDaoWithPdo] не інстанціюватимуть. Будуть інстанціюватися похідні класи.
- рядок 11: клас [ImpotsDaoWithPdo] реалізує інтерфейс [IImpotsDao]. Отже, він повинен визначити метод getData. Цей метод міститься в рядках 72–74.
- рядок 14: $dsn (Data Source Name) — це рядок символів, який однозначно ідентифікує SGBD та використовувану базу даних.
- рядок 15: $user ідентифікує користувача, який підключається до бази даних
- рядок 16: $passwd — це пароль попереднього користувача
- рядок 17: $tranches — це масив об’єктів Tranche, у якому буде збережено таблиці MySQL та [dbimpots.impots].
- рядки 45–70: конструктор класу. Цей код уже зустрічався у версії 4, у розділі 8.2. Слід зауважити, що створення об’єкта [ImpotsDaoWithPdo] може завершитися невдачею. У такому разі генерується виняток типу [ImpotsException].
- рядки 72–74: метод [getData] інтерфейсу [IImpotsDao].
Клас [ImpotsDaoWithPdo] підходить для будь-якого SGBD. Конструктор класу (рядок 45) вимагає знання імені джерела даних (Data Source Name) бази даних. Цей рядок символів залежить від використовуваного SGBD. Ми вирішили не вимагати від користувача класу знання цього імені джерела даних. Для кожного SGBD існуватиме окремий клас, похідний від [ImpotsDaoWithPdo]. Для SGBD та MySQL це буде такий клас:
class ImpotsDaoWithMySQL extends ImpotsDaoWithPdo {
public function __construct($host, $port, $base, $user, $passwd) {
parent::__construct("mysql:host=$host;dbname=$base;port=$port", $user, $passwd);
}
}
- у рядку 3 розробник не вимагає вказати ім'я джерела даних (Data Source Name), а лише ім'я хост-машини SGBD ($host), її порт прослуховування ($port) та ім'я бази даних ($base).
- У рядку 4 формується Data Source Name бази даних MySQL, яке використовується для виклику конструктора батьківського класу.
Слід зауважити, що для адаптації до іншого SGBD достатньо написати відповідний клас, похідний від [ImpotsDaoWithPdo]. У кожному випадку потрібно сформувати ім’я джерела даних (Data Source Name), властиве для використовуваного SGBD.
11.2.3. Рівень [métier] (impots_05_metier)
Рівень [metier] містить логіку розрахунку податку:
![]() |
Рівень [métier] має такий інтерфейс:
<?php
// бізнес-інтерфейс
interface IImpotsMetier {
public function calculerImpot($marié, $enfants, $salaire);
}
Інтерфейс [IImpotsMetier] надає лише один метод — метод [calculerImpot], який дозволяє розрахувати податок платника податків на основі таких параметрів:
- $marié: рядок «так»/«ні» залежно від того, чи перебуває платник податків у шлюбі
- $enfants: кількість дітей платника податків
- $salaire: його заробітна плата
Ці параметри надаватиме йому шар [web].
Реалізація інтерфейсу [IImpotsMetier] виглядає наступним чином:
// залежності
require_once "impots_05_dao.php";
// ------------------------------------------------------------------
// клас реалізації
class ImpotsMetier implements IImpotsMetier {
// DAO-шар
private $dao;
// масив об’єктів [Tranche]
private $data;
// геттери та сеттери
public function getDao() {
return $this->dao;
}
public function setDao($dao) {
$this->dao = $dao;
}
public function setData($data){
$this->data=$data;
}
public function __construct($dao) {
// отримання даних, необхідних для розрахунку податку
$this->setDao($dao);
$this->setData($this->dao->getData());
}
public function calculerImpot($marié, $enfants, $salaire) {
// $marié: так, ні
// $enfants: кількість дітей
// $salaire: річний оклад
// кількість часток
$marié = strtolower($marié);
if ($marié == "oui")
$nbParts = $enfants / 2 + 2;
else
$nbParts=$enfants / 2 + 1;
// на 1/2 частки більше, якщо є щонайменше 3 дітей
if ($enfants >= 3)
$nbParts+=0.5;
// оподатковуваний дохід
$revenuImposable = 0.72 * $salaire;
// сімейний коефіцієнт
$quotient = $revenuImposable / $nbParts;
// вказується в кінці таблиці обмежень для припинення наступного циклу
$N = count($this->data);
$this->data[$N - 1]->setLimite($quotient);
// розрахунок податку
$i = 0;
while ($i < $N and $quotient > $this->data[$i]->getLimite()) {
$i++;
}
// оскільки $quotient розміщено в кінці таблиці $limites, попередній цикл
// не може виходити за межі масиву $limites
// тепер можна обчислити податок
return floor($revenuImposable * $this->data[$i]->getCoeffR() - $nbParts * $this->data[$i]->getCoeffN());
}
}
- рядок 2: шар [métier] потребує класів шару [dao] та сутностей (Tranche, ImpotsException).
- рядок 6: клас [ImpotsMetier] реалізує інтерфейс [IimpotsMetier].
- рядки 9–11: приватні поля класу:
- $dao: посилання на рівень [dao]
- $data: масив об’єктів типу [Tranche], наданий рівнем [dao]
- рядки 26–30: конструктор класу ініціалізує два попередні поля. Він отримує як параметр посилання на шар [dao].
- рядки 32–61: реалізація методу [calculerImpot] інтерфейсу [IimpotsMetier]. Цей метод зустрічається вже починаючи з версії 1 (параграф 4.2).
11.2.4. Шар [web] (impots_05_web)
Рівень [metier] містить логіку розрахунку податку:
![]() |
Рівень [web] складається з веб-сервісу, який відповідає веб-клієнтам. Нагадаємо, що вони надсилають запит до веб-сервісу, передаючи такий параметр: params=marié,enfants,salaire. Ми маємо справу з веб-сервісом, подібним до тих, що ми створювали в попередніх параграфах. Його код такий:
<?php
// бізнес-шар
require_once "impots_05_metier.php";
// обробка помилок
ini_set("display_errors", "off");
// заголовок UTF-8
header("Content-Type: text/plain; charset=utf-8");
// ------------------------------------------------------------------------------
// веб-сервіс податкової служби
// визначення констант
$HOTE = "localhost";
$PORT = 3306;
$BASE = "dbimpots";
$USER = "root";
$PWD = "";
// дані, необхідні для розрахунку податку, були розміщені в таблиці MySQL IMPOTS
//, що належить до бази даних $BASE. Таблиця має таку структуру
// limites decimal(10,2), coeffR decimal(6,2), coeffN decimal(10,2)
// параметри платників податків (сімейний стан, кількість дітей, річний заробіток)
// надсилаються клієнтом у форматі params=сімейний стан, кількість дітей, річний заробіток
// результати (сімейний стан, кількість дітей, річний заробіток, сума податку до сплати) надсилаються клієнту
// у форматі <impot>impot</impot>
// або у вигляді <помилка>помилка</помилка>, якщо параметри є недійсними
// у сеансі отримується шар [métier]
session_start();
if (!isset($_SESSION['metier'])) {
// інстанціювання шару [dao] та шару [métier]
try {
$_SESSION['metier'] = new ImpotsMetier(new ImpotsDaoWithMySQL($HOTE, $PORT, $BASE, $USER, $PWD));
} catch (ImpotsException $ie) {
print "<erreur>Erreur : " . utf8_encode($ie->getMessage() . "</erreur>");
exit;
}
}
$metier = $_SESSION['metier'];
// отримується рядок, надісланий клієнтом
$params = utf8_encode(htmlspecialchars(strtolower(trim($_POST['params']))));
$items = explode(",", $params);
// має бути лише 3 параметри
if (count($items) != 3) {
print "<erreur>[$params] : nombre de paramètres invalides</erreur>\n";
exit;
}//if
// перший параметр (сімейний стан) повинен мати значення «так» або «ні»
$marié = trim($items[0]);
if ($marié != "oui" and $marié != "non") {
print "<erreur>[$params] : 1er paramètre invalide</erreur>\n";
exit;
}//if
// другий параметр (кількість дітей) має бути цілим числом
if (!preg_match("/^\s*(\d+)\s*$/", $items[1], $champs)) {
print "<erreur>[$params] : 2ième paramètre invalide</erreur>\n";
exit;
}//if
$enfants = $champs[1];
// третій параметр (зарплата) повинен бути цілим числом
if (!preg_match("/^\s*(\d+)\s*$/", $items[2], $champs)) {
print "<erreur>[$params] : 3ième paramètre invalide</erreur>\n";
exit;
}//if
$salaire = $champs[1];
// розраховується податок
$impôt = $metier->calculerImpot($marié, $enfants, $salaire);
// повертаємо результат
print "<impot>$impôt</impot>\n";
// кінець
exit;
- рядок 4: шар [web] потребує класів шару [métier]
- рядки 30–40: посилання на шар [métier] додається до сесії. Якщо згадати, що цей шар [métier] має посилання на шар [dao], а останній зберігає дані з SGBD, то стає зрозуміло:
- що перший запит клієнта спричинить звернення до шару SGBD
- що наступні запити цього ж клієнта використовуватимуть дані, збережені шаром [dao]. Отже, доступу до SGBD не відбувається.
- рядок 34: побудова шару [métier], що працює з шаром [dao], реалізованим для SGBD MySQL
- рядки 35–37: обробка можливої помилки в попередній операції. У цьому випадку клієнту надсилається рядок <erreur>message</erreur>.
- рядок 43: отримується параметр «params», який був надісланий клієнтом.
- рядки 46–49: перевіряється кількість елементів, знайдених у «params»
- рядки 51–55: перевіряється правильність першого елемента
- рядки 56–60: те саме для другого запису
- рядки 62–66: те саме для третьої інформації
- рядок 69: саме шар [métier] розраховує податок.
- рядок 71: надсилання результату клієнту
Результати
Нагадаємо, що клієнт [client_impots_web_05] використовує такий файл [data.txt]:
На основі цих рядків (сімейний стан, діти, заробітна плата) клієнт надсилає запит до сервера розрахунку податків і записує результати у текстовий файл [resultats.txt]. Після виконання клієнта вміст цього файлу виглядає наступним чином:
oui:2:200000:22504
non:2:200000:33388
oui:3:200000:16400
non:3:200000:22504
oui:5:50000:0
non:0:3000000:1354938
де кожен рядок має вигляд (одружений, діти, зарплата, розрахований податок).






