Skip to content

18. تمرين تطبيقي – الإصدار 8

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

18.1. مقدمة

كانت بنية الإصدار 5 كما يلي:

Image

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

سنقوم بترحيل هذه البنية إلى بنية العميل/الخادم التالية:

Image

  • في [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. الخادم

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

Image

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

Image

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

Image

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

18.2.2. الطبقة [dao]

Image

تنفذ الطبقة [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]

Image

Image

تنفذ الطبقة [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. نص البرمجة الخادم

Image

Image

ينفذ البرنامج النصي للخادم الطبقة [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]:

Image

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

Image

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

Image

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

Image

  • في [13]، الرد jSON من الخادم؛

لنقم ببعض اختبارات الأخطاء:

نطلب URL و [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]

ونحصل على النتيجة التالية:

Image

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

Image

18.2.5. اختبارات [Codeception]

في كل مرة نقوم فيها بإنشاء إصدار جديد من الخادم، سنقوم باختبار الطبقات [métier] و [dao] كما تم القيام بذلك منذ الإصدار 04 (انظر الفقرتين الرابط والرابط).

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

Image

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

18.2.5.1. اختبارات الطبقة [dao]

Image

سيكون الاختبار [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] هي تلك الموضحة في الفقرة «الرابط»؛

نتائج الاختبار هي كما يلي:

Image

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

Image

سيكون الاختبار [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] هي تلك الموضحة في الفقرة «الرابط»؛

نتائج الاختبار هي كما يلي:

Image

18.3. العميل

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

Image

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

Image

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

Image

تم وصف جميع الكيانات المذكورة أعلاه واستخدامها بالفعل:

18.3.2. الطبقة [dao]

Image

الطبقة [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]

Image

Image

الطبقة [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. النص البرمجي الرئيسي

Image

Image

يقوم البرنامج النصي للعميل [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:

Image

وستكون النتائج في وحدة التحكم الخاصة بالعميل كما يلي:


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] اسم المستخدم الذي يقوم بتسجيل الدخول:

1
2
3
4
    "user": {
        "login": "x",
        "passwd": "x"
},

وستكون النتائج في وحدة التحكم الخاصة بالعميل كما يلي:


L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé

18.3.5. اختبارات [Codeception]

كما تم في الإصدارات السابقة، سنقوم بكتابة اختبارات [Codeception] للإصدار 08.

Image

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] هي تلك الموضحة في الفقرة [رابط]؛

نتائج الاختبار هي كما يلي:

Image