18. تمرين تطبيقي – الإصدار 8
سنستأنف تطبيق المثال – الإصدار 5 (الفقرة «الرابط») وسنحوله إلى تطبيق عميل/خادم.
18.1. مقدمة
كانت بنية الإصدار 5 كما يلي:

- تتولى الطبقة المسماة [dao] (كائنات الوصول إلى البيانات) التبادل مع قاعدة البيانات MySQL ونظام الملفات المحلي؛
- الطبقة المسماة [métier] تقوم بحساب الضريبة؛
- النص البرمجي الرئيسي هو قائد الأوركسترا: فهو يقوم بإنشاء مثيلات الطبقات [dao] و [métier] ثم يتواصل مع الطبقة [métier] للقيام بما يجب القيام به؛
سنقوم بترحيل هذه البنية إلى بنية العميل/الخادم التالية:

- في [2]، سنستعيد الطبقة [dao] من الإصدار 5 مع إزالة طرق الوصول إلى نظام الملفات المحلي منها. وستنتقل هذه الطرق إلى الطبقة [dao] التابعة للعميل [6, 7]؛
- في [3]، وستبقى الطبقة [métier] هي نفسها الموجودة في الإصدار 5 بدون طرقها [executeBatchImpôts, saveResults] التي ستنتقل إلى الطبقة [dao] و[7] الخاصة بالعميل؛
- في [4]، يجب كتابة البرنامج النصي للخادم: وسيكون عليه:
- إنشاء الطبقات [métier] و [dao] و [3, 2]؛
- التفاعل مع البرنامج النصي للعميل [5, 7]؛
- في [7]، يجب كتابة طبقة [dao] الخاصة بالعميل:
- وستكون عميلاً HTTP لبرنامج الخادم النصي [4, 5]؛
- وستستخدم طرق الوصول إلى نظام الملفات المحلي الخاصة بطبقة [dao] من الإصدار 5؛
- في [8]، ستلتزم طبقة العميل [métier] بواجهة [InterfaceMetier] الخاصة بالإصدار 5. إلا أن طريقة تنفيذها ستكون مختلفة. في الإصدار 5، كانت الطبقة [métier] هي التي تقوم بحساب الضريبة. أما هنا، فإن الطبقة [métier] الخاصة بالخادم هي التي تقوم بهذا الحساب. وبالتالي، ستستدعي الطبقة [métier] الطبقتين [dao] و[7] للتواصل مع الخادم وطلب حساب الضريبة منه؛
- في [9]، سيتعين على البرنامج النصي الخاص بوحدة التحكم إنشاء مثيل للطبقات [dao, métier] الخاصة بالعميل وبدء تشغيلها؛
18.2. الخادم
نحن مهتمون بجزء الخادم من التطبيق.

سيتم تنفيذ هذه البنية من خلال البرامج النصية التالية:

18.2.1. الكيانات المتبادلة بين الطبقات

الكيانات المتبادلة بين الطبقات هي تلك الخاصة بالإصدار 5 الموصوفة في الفقرة المرتبطة.
18.2.2. الطبقة [dao]

تنفذ الطبقة [dao] الواجهة [InterfaceServerDao] التالية:
<?php
// مساحة الأسماء
namespace Application;
interface InterfaceServerDao {
// قراءة بيانات مصلحة الضرائب
public function getTaxAdminData(): TaxAdminData;
}
- السطر 9: تقوم الطريقة [getTaxAdminData] باسترداد بيانات مصلحة الضرائب من قاعدة البيانات؛
يتم تنفيذ الواجهة [InterfaceServerDao] بواسطة الفئة [ServerDao] التالية:
<?php
// مساحة الأسماء
namespace Application;
// تعريف فئة ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
// الكائن من النوع 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;
}
}
وقد تم عرض هذا الرمز في الفقرة «الرابط».
18.2.3. الطبقة [métier]


تنفذ الطبقة [métier] الواجهة [InterfaceServerMetier] التالية:
<?php
// مساحة الأسماء
namespace Application;
interface InterfaceServerMetier {
// حساب الضرائب المستحقة على المكلف
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
يتم تنفيذ الواجهة [InterfaceServerMetier] بواسطة الفئة [ServerMetier] التالية:
<?php
// مساحة الأسماء
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// طبقة Dao
private $dao;
// بيانات الإدارة الضريبية
private $taxAdminData;
//---------------------------------------------
// تعيين الطبقة [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $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);
}
}
سبق عرض هذا الكود وشرحه في الإصدار 1 في الفقرة [رابط]. وقد تم عرض نسخته الكائنية مع قاعدة البيانات في الفقرة [رابط].
18.2.4. نص البرمجة الخادم


ينفذ البرنامج النصي للخادم الطبقة [web] [4]. يتم تكوين البرنامج النصي [impots-server] بواسطة الملف jSON [config-server.json] التالي:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
"databaseFilename": "Data/database.json",
"taxAdminDataFileName": "Data/taxadmindata.json",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/ExceptionImpots.php",
"/Entities/TaxAdminData.php",
"/Entities/Database.php",
"/Dao/InterfaceServerDao.php",
"/Dao/ServerDao.php",
"/Métier/InterfaceServerMetier.php",
"/Métier/ServerMetier.php"
],
"absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
"users": [
{
"login": "admin",
"passwd": "admin"
}
]
}
- السطر 1: المجلد الجذر الذي سيتم قياس مسارات الملفات انطلاقًا منه؛
- السطر 2: ملف jSON الخاص بتكوين قاعدة البيانات MySQL؛
- السطر 3: ملف jSON الخاص ببيانات إدارة الضرائب؛
- الأسطر 5-14: ملفات التطبيق؛
- السطر 15: التبعية اللازمة لمكتبات الجهات الخارجية، وهي هنا Symfony؛
- الأسطر 16-20: جدول المستخدمين المصرح لهم باستخدام التطبيق؛
الملفان jSON و [database.json, taxadmindata.json] هما ملفا الإصدار 5 الموصوفان في الفقرة «الرابط».
يقوم البرنامج النصي [impots-server] بتنفيذ الطبقة [web] بالطريقة التالية:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// إدارة الأخطاء بواسطة PHP
//ini_set("display_errors", "0");
//
// مسار ملف التكوين
define("CONFIG_FILENAME", "Data/config-server.json");
// يتم استرداد التكوين
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// تضمين التبعيات اللازمة للنص البرمجي
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// التبعيات المطلقة (مكتبات الجهات الخارجية)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// تعريف الثوابت
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// التبعيات الخاصة بـ Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// تحضير استجابة الخادم JSON
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// استرداد الطلب الحالي
$request = Request::createFromGlobals();
// المصادقة
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// هل المستخدم موجود؟
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// تحديد رمز حالة الرد
if (!$trouvé) {
// غير موجود - الرمز 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// رسالة خطأ
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// النهاية
exit;
}
// المستخدم صالح - يتم التحقق من المعلمات المستلمة
$erreurs = [];
// يجب أن يكون لدينا ثلاثة معلمات GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// خطأ؟
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// يتم استرداد الحالة الاجتماعية
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// خطأ؟
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// يتم استرداد عدد الأطفال
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// يجب أن يكون عدد الأطفال عددًا صحيحًا >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// خطأ؟
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// يتم استرداد الراتب السنوي
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// يجب أن يكون الراتب عددًا صحيحًا >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// خطأ؟
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// معلمات أخرى في الاستعلام؟
foreach (\array_keys($request->query->all()) as $key) {
// هل المعلمة صالحة؟
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// أخطاء؟
if ($erreurs) {
// يتم إرسال رمز الخطأ 400 إلى العميل
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// لدينا كل ما نحتاجه للعمل
// إنشاء بنية الخادم
$msgErreur = "";
try {
// إنشاء الطبقة [dao]
$dao = new ServerDao($config["databaseFilename"]);
// إنشاء الطبقة [métier]
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// تم تسجيل الخطأ
$msgErreur = utf8_encode($ex->getMessage());
}
// خطأ؟
if ($msgErreur) {
// يتم إرسال رمز الخطأ 500 إلى العميل
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// حساب الضريبة
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// إرجاع الرد
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
تعليقات
- السطر 16: يتم تحميل ملف التكوين؛
- الأسطر 18-26: يتم تحميل جميع التبعيات؛
- السطر 29: اسم الملف [database.json]؛
- الأسطر 32-33: يتم إعلان فئات المكتبات الخارجية التي سيتم استخدامها؛
- الأسطر 36-38: يتم إعداد استجابة jSON؛
- الأسطر 40-52: يتم التحقق من أن المستخدم الذي يقوم بالطلب هو بالفعل أحد المستخدمين المصرح لهم؛
- الأسطر 54-63: إذا لم يكن الأمر كذلك، يتم إرسال الرمز HTTP 401 الذي يشير إلى رفض الوصول. عند استلام هذا الرمز ورأس الرسالة HTTP [WWW-Authenticate => Basic realm=]، تعرض معظم المتصفحات نافذة مصادقة تطلب من المستخدم المصادقة؛
- السطر 59: توضح استجابة الخادم jSON سبب الخطأ. وستكون جميع استجابات الخادم عبارة عن السلسلة jSON من جدول [‘réponse’=>’qq chose’]؛
- الأسطر 64-117: يتم التحقق من صحة الطلب:
- طلب GET مع ثلاثة معلمات بالضبط؛
- معلمة [marié] يجب أن تكون قيمتها «نعم» أو «لا»؛
- معلمة [enfants] يجب أن تكون قيمتها عددًا صحيحًا >=0؛
- معلمة [salaire] يجب أن تكون قيمتها عددًا صحيحًا >=0؛
- السطر 65: في كل مرة يتم فيها اكتشاف خطأ، تُضاف رسالة خطأ إلى المصفوفة [$erreurs]؛
- الأسطر 120-126: في حالة وجود خطأ، يتم إرسال الرمز HTTP [400 Bad Request] إلى العميل (السطر 122)؛
- السطر 123: توضح استجابة الخادم jSON سبب الخطأ؛
- ابتداءً من السطر 132، تم التحقق من كل شيء. يمكن إنشاء مثيل للطبقات [dao, métier]. هذا الإنشاء له تكلفة ولا يجب القيام به إلا إذا كنا متأكدين من أن الطلب صالح؛
- الأسطر 130-138: يتم إنشاء بنية الخادم. قد يؤدي إنشاء الطبقة [dao] إلى إثارة استثناء من النوع [ExceptionImpots]. في حالة حدوث هذا الاستثناء، يتم تسجيل الخطأ؛
- الأسطر 135-138: في حالة حدوث استثناء، يتم إرسال الرمز HTTP 500 إلى العميل. يشير هذا الرمز إلى حدوث خطأ في الخادم؛
- السطر 143: توضح الاستجابة سبب الخطأ؛
- السطر 148 : يتم تفويض حساب الضريبة إلى الطبقة [métier]؛
- السطران 150-151: إرسال الرد؛
لنختبر هذا البرنامج النصي باستخدام متصفح. لنطلب URL الآمن [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

- في [1]، الـ URL الآمنة المطلوبة؛
- إلى [2]، المعلمات الثلاثة [marié, enfants, salaire]؛
- في [3]، أرسل خادم Apache الخاص بـ Laragon شهادة SSL موقعة ذاتيًا. لاحظ المتصفح ذلك وعرض تحذيرًا أمنيًا: فهو يعتبر أن موقع الخادم غير موثوق به؛
- في [4]، نواصل؛

- في [6]، نواصل؛

- في [7]، يعرض المتصفح نافذة ليتمكن المستخدم من المصادقة؛
- في [9,10]، نكتب [admin] و [admin]؛

- في [13]، الرد jSON من الخادم؛
لنقم ببعض اختبارات الأخطاء:
نطلب URL و [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]
ونحصل على النتيجة التالية:

نقطع SGBD MySQL ونطلب URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

18.2.5. اختبارات [Codeception]
في كل مرة نقوم فيها بإنشاء إصدار جديد من الخادم، سنقوم باختبار الطبقات [métier] و [dao] كما تم القيام بذلك منذ الإصدار 04 (انظر الفقرتين الرابط والرابط).
أولاً، نربط المشروع [scripts-web] باختبارات [Codeception]. وللقيام بذلك، اتبع نفس الإجراء المتبع للمشروع [scripts-console] في الفقرة [رابط]. نحصل على مشروع [scripts-web] مع مجلد [Test Files]:

سنقوم بإنشاء اختبار للطبقة [dao] واختبار آخر للطبقة [métier].
18.2.5.1. اختبارات الطبقة [dao]

سيكون الاختبار [ServerDaoTest] كما يلي:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// تعريف الثوابت
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// مسار ملف التكوين
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// استرداد التكوين
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// تضمين التبعيات اللازمة للنص البرمجي
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// التبعيات المطلقة (مكتبات الجهات الخارجية)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// اختبار -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// الأصل
parent::__construct();
// استرداد التكوين
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// إنشاء الطبقة [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// الاختبارات
public function testTaxAdminData() {
…
}
}
تعليقات
- الأسطر 9-24: يتم إنشاء نفس بيئة العمل الموجودة في الخادم [impots-server.php]. ويتم ذلك في الأسطر 9-12 من خلال تعريف الثابتين اللتين تعتمد عليهما البيئة؛
- الأسطر 32-40: يتم إنشاء مثيل لطبقة [dao] المراد اختبارها كما تم في البرنامج النصي للخادم [impots-server.php]؛
- من الآن فصاعدًا، نحن في نفس الظروف التي كان عليها البرنامج النصي للخادم [impots-server.php]: يمكننا بدء الاختبارات؛
- الأسطر 43-45: الطريقة [testTaxAdminData] هي تلك الموضحة في الفقرة «الرابط»؛
نتائج الاختبار هي كما يلي:

18.2.5.2. اختبارات الطبقة [métier]

سيكون الاختبار [ServerMetierTest] كما يلي:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// تعريف الثوابت
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// مسار ملف التكوين
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// استرداد التكوين
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// تضمين التبعيات اللازمة للنص البرمجي
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// التبعيات المطلقة (مكتبات الجهات الخارجية)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// فئة الاختبار
class ServerMetierTest extends \Codeception\Test\Unit {
// طبقة الأعمال
private $métier;
public function __construct() {
parent::__construct();
// استرداد التكوين
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// إنشاء الطبقة [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// إنشاء الطبقة [métier]
$this->métier = new ServerMetier($dao);
}
// اختبارات
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
تعليقات
- الأسطر 9-24: يتم إنشاء نفس بيئة العمل الموجودة في الخادم [impots-server.php]. ويتم ذلك في الأسطر 9-12 من خلال تعريف الثابتين اللتين تعتمد عليهما البيئة؛
- الأسطر 30-38: يتم إنشاء مثيل لطبقة [métier] لاختباره كما تم في البرنامج النصي للخادم [impots-server.php]؛
- من الآن فصاعدًا، نحن في نفس الظروف التي كان عليها البرنامج النصي للخادم [impots-server.php]: يمكننا بدء الاختبارات؛
- الأسطر 40-53: طرق [test1, test2…, test11] هي تلك الموضحة في الفقرة «الرابط»؛
نتائج الاختبار هي كما يلي:

18.3. العميل
نحن مهتمون بالجزء الخاص بالعميل من التطبيق.

سيتم تنفيذ هذه البنية من خلال البرامج النصية التالية:

18.3.1. الكيانات المتبادلة بين الطبقات

تم وصف جميع الكيانات المذكورة أعلاه واستخدامها بالفعل:
18.3.2. الطبقة [dao]

الطبقة [dao] تنفذ الواجهة [InterfaceClientDao] التالية:
<?php
// مساحة الأسماء
namespace Application;
interface InterfaceClientDao {
// قراءة بيانات دافعي الضرائب
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// حساب ضرائب أحد المكلفين
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// تسجيل النتائج
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- السطر 9: تقوم الدالة [getTaxPayersData] بتحميل بيانات دافعي الضرائب من الملف [$taxPayersFilename] إلى الذاكرة. في حالة وجود أخطاء، يتم تسجيلها في الملف [$errorsFilename]؛
- السطر 12: تحسب الدالة [calculerImpots] ضريبة أحد دافعي الضرائب؛
- السطر 15: تقوم الدالة [saveResults] بحفظ بيانات الجدول [$taxPayersData]، التي تمثل نتائج عدة حسابات ضريبية، في الملف [$resultsFilename]؛
يتم تنفيذ واجهة [InterfaceClientDao] بواسطة الفئة [ClientDao] التالية:
<?php
namespace Application;
// التبعيات
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// استخدام معالجة
use TraitDao;
// السمات
private $urlServer;
private $user;
// المنشئ
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// حساب الضريبة
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// إنشاء عميل HTTP
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// إرسال الطلب إلى الخادم
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// استلام الرد
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// السجلات
// print "$json=json\n";
// يتم استرداد حالة الرد
$statusCode = $response->getStatusCode();
// خطأ؟
if ($statusCode !== 200) {
// حدثت خطأ - يتم إثارة استثناء
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// نقوم بإرجاع الرد
return $réponse;
}
}
تعليقات
- السطر 10: يتم إدراج [TraitDao] (انظر الفقرة ذات الرابط) التي تنفذ الطريقتين [getTaxPayersData] و [saveResults]. وبالتالي، لم يتبق سوى الطريقة [calculerImpots] لتنفيذها. وقد تم تنفيذها في الأسطر 22-49؛
- الأسطر 16-19: يتلقى منشئ الفئة [ClientDao] معلمتين:
- URL و [$urlServer] من خادم حساب الضرائب؛
- المصفوفة [$user] التي تحتوي على مفاتيح «login» و«passwd» والتي تحدد المستخدم الذي يقوم بالطلب؛
- السطر 22: تستقبل الطريقة [calculerImpots] المعلمات الثلاثة لإرسالها إلى خادم حساب الضرائب؛
- الأسطر 24-27: يتم إنشاء عميل HTTP باستخدام:
- السطر 25: بيانات تعريف المستخدم الذي يقوم بالطلب؛
- السطر 26: الخيار الذي يجعل العميل HTTP لا يتحقق من صحة الشهادة SSL المرسلة من الخادم؛
- الأسطر 29-34: يتم استعلام الخادم باستخدام المعلمات الثلاثة التي يتوقعها؛
- السطر 36: يتم استرداد الرد jSON من الخادم. إذا لم يتم تمرير المعلمة [false] إلى الدالة [Response::getContent]، فعندئذٍ إذا كان حالة استجابة الخادم تقع ضمن النطاق [3xx-5xx] (حالة خطأ)، فإن الكائن [Response] يُطلق استثناءً بمجرد محاولة الحصول على محتوى الاستجابة [Response::getContent] أو رؤوسها HTTP و[Response::getHeaders]. هنا، بغض النظر عن حالة الرد HTTP، نريد أن نتمكن من الوصول إلى محتواه، ولو لمجرد تسجيله (السطر 40)؛
- السطران 37-38: رد الخادم هو السلسلة jSON من جدول [‘réponse’=>qqChose]. نسترد [qqChose]؛
- السطر 40: يتم تسجيل الرد jSON في وضع التطوير؛
- السطر 42: يتم استرداد رمز حالة الاستجابة؛
- الأسطر 44-49: إذا لم يكن رمز الحالة HTTP هو 200، فهذا يعني أن خادمنا قد واجه مشكلة. عندها يتم إثارة استثناء من النوع [ExceptionImpots] مع رسالة تتضمن الرد jSON الوارد من الخادم مضافًا إليه رمز الرد HTTP؛
- السطر 51: يتم إرجاع النتيجة وهي مصفوفة مرتبطة بمفاتيح [impôt, surcôte, décôte, réduction, taux]؛
18.3.3. الطبقة [métier]


الطبقة [métier] [8] تُنفذ الواجهة [InterfaceClientMetier] التالية:
<?php
// مساحة الأسماء
namespace Application;
interface InterfaceClientMetier {
// حساب ضرائب أحد دافعي الضرائب
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// حساب الضرائب في الوضع الدفعي
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- السطر 9: تحسب الدالة [calculerImpots] الضريبة؛
- السطر 12: تقوم الدالة [executeBatchImpots] بحساب الضريبة للمكلفين الذين توجد بياناتهم في الملف [$taxPayersFileName]، وتضع النتائج التي تم الحصول عليها في الملف [$resultsFileName] والأخطاء التي تمت مواجهتها في الملف [$errorsFileName]؛
يتم تنفيذ واجهة [InterfaceClientMetier] بواسطة الفئة [ClientMetier] التالية:
<?php
// مساحة الأسماء
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// السمة
private $clientDao;
// منشئ
public function __construct(InterfaceClientDao $clientDao) {
// يتم تخزين المرجع في الطبقة [dao]
$this->clientDao = $clientDao;
}
// حساب الضريبة
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// حساب الضرائب في الوضع الدفعي
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// يتم السماح بترحيل الاستثناءات الواردة من الطبقة [dao]
// استرداد بيانات دافعي الضرائب
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// جدول النتائج
$results = [];
// يتم استغلالها
foreach ($taxPayersData as $taxPayerData) {
// يتم حساب الضريبة
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// يتم استكمال [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// يتم إدراج النتيجة في جدول النتائج
$results [] = $taxPayerData;
}
// تسجيل النتائج
$this->clientDao->saveResults($resultsFileName, $results);
}
}
تعليقات
- الأسطر 11-14: يتلقى منشئ الفئة [ClientMetier] كمعلمة مرجعًا إلى الطبقة [dao]؛
- الأسطر 17-19: يتم تفويض حساب الضريبة إلى الطبقة [dao]؛
- الأسطر 20-38: تم وصف الدالة [executeBatchImpots] في الفقرة «الرابط»؛
18.3.4. النص البرمجي الرئيسي


يقوم البرنامج النصي للعميل [MainImpotsClient.php] بتنفيذ الطبقة [console] [9]. ويتم تكوينه بواسطة الملف jSON [conf-client.json] التالي:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": [
"Entities/BaseEntity.php",
"Entities/TaxPayerData.php",
"Entities/ExceptionImpots.php",
"Utilities/Utilitaires.php",
"Dao/InterfaceClientDao.php",
"Dao/TraitDao.php",
"Dao/ClientDao.php",
"Métier/InterfaceClientMetier.php",
"Métier/ClientMetier.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php"
],
"user": {
"login": "admin",
"passwd": "admin"
},
"urlServer": "https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php"
}
- السطر 1: المجلد الجذر للعميل؛
- السطر 2: ملف jSON لبيانات دافعي الضرائب؛
- السطر 3: ملف jSON للنتائج؛
- السطر 4: الملف jSON الخاص بالأخطاء؛
- الأسطر 6-19: التبعيات المختلفة لمشروع العميل؛
- الأسطر 20-23: المستخدم الذي يوجه الاستعلامات إلى خادم حساب الضرائب؛
- السطر 24: ملف URL الآمن لخادم حساب الضرائب؛
رمز البرنامج النصي [MainImpotsClient.php] هو كما يلي:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// إدارة الأخطاء بواسطة PHP
//ini_set("display_errors", "0");
//
// مسار ملف التكوين
define("CONFIG_FILENAME", "../Data/config-client.json");
// يتم استرداد التكوين
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// تضمين التبعيات اللازمة للنص البرمجي
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// التبعيات المطلقة (مكتبات الجهات الخارجية)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// تعريف الثوابت
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// التبعيات الخاصة بـ Symfony
use Symfony\Component\HttpClient\HttpClient;
// إنشاء الطبقة [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// إنشاء الطبقة [métier]
$clientMetier = new ClientMetier($clientDao);
// حساب الضرائب في الوضع الدفعي
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// عرض الخطأ
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// النهاية
print "Terminé\n";
exit;
تعليقات
- السطر 13: مسار ملف التكوين؛
- السطر 16: تشغيل ملف التكوين؛
- الأسطر 18-26: تحميل التبعيات؛
- السطر 37: إنشاء الطبقة [dao]. يتم تمرير المعلومات التي يطلبها منشئ الطبقة، وهما:
- URL الخاص بخادم حساب الضرائب؛
- معرفات المستخدم الذي سيقوم بإجراء الاستعلامات؛
- السطر 39: إنشاء الطبقة [métier]. نمرر إلى منشئ الطبقة مرجعًا إلى الطبقة [dao] التي تم إنشاؤها للتو؛
- السطر 43: يُطلب من الطبقة [métier] ما يلي:
- حساب الضرائب لجميع دافعي الضرائب في الملف $config["taxPayerDataFileName"]؛
- وضع النتائج في الملف $config["resultsFileName"]؛
- تسجيل الأخطاء في الملف $config["errorsFileName"]؛
- قد يؤدي السطر 43 إلى حدوث استثناءات؛
- السطر 46: عرض رسالة خطأ الاستثناء؛
يؤدي تشغيل العميل إلى نفس النتائج التي أسفرت عنها الإصدارات السابقة. تحقق من الملفات التالية:
- [Data/taxpayersdata.json]: بيانات دافعي الضرائب الذين يتم حساب مبلغ الضريبة لهم؛
- [Data/results.json]: النتائج الخاصة بمختلف دافعي الضرائب في الملف [Data/taxpayersdata.json]؛
- [Data/errors.json]: الأخطاء التي قد تكون حدثت أثناء معالجة الملف [Data/taxpayersdata.json]؛
لنلقِ نظرة على حالات الأخطاء المحتملة. أولاً، دعونا نوقف خادم Laragon. عندئذٍ تكون النتائج في وحدة التحكم الخاصة بالعميل كما يلي:
Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé
الآن، لنقم بتشغيل خادم Apache فقط دون تشغيل SGBD MySQL:

وستكون النتائج في وحدة التحكم الخاصة بالعميل كما يلي:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}
Terminé
الآن، لنقم بتشغيل MySQL ثم نعدل في [config-client] اسم المستخدم الذي يقوم بتسجيل الدخول:
وستكون النتائج في وحدة التحكم الخاصة بالعميل كما يلي:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
18.3.5. اختبارات [Codeception]
كما تم في الإصدارات السابقة، سنقوم بكتابة اختبارات [Codeception] للإصدار 08.

18.3.5.1. اختبار الطبقة [métier]
اختبار [ClientMetierTest.php] هو كما يلي:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// تعريف الثوابت
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
// مسار ملف التكوين
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// استرداد التكوين
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// تضمين التبعيات اللازمة للنص البرمجي
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// التبعيات المطلقة (مكتبات الجهات الخارجية)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// فئة الاختبار
class ClientMetierTest extends \Codeception\Test\Unit {
// طبقة الأعمال
private $métier;
public function __construct() {
parent::__construct();
// يتم استرداد التكوين
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// إنشاء الطبقة [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// إنشاء الطبقة [métier]
$this->métier = new ClientMetier($clientDao);
}
// اختبارات
public function test1() {
…
}
-------------
public function test11() {
…
}
}
تعليقات
- الأسطر 10-26: تعريف بيئة الاختبار. نستخدم نفس البيئة المستخدمة في البرنامج النصي الرئيسي [MainImpotsClient] الموصوف في الفقرة المرتبطة؛
- الأسطر 33-41: إنشاء الطبقات [dao] و [métier]؛
- السطر 40: يشير السمة [$this→métier] إلى الطبقة [métier]؛
- الأسطر 44-51: الطرق [test1, test2…, test11] هي تلك الموضحة في الفقرة [رابط]؛
نتائج الاختبار هي كما يلي:
