Skip to content

18. تمرين عملي – الإصدار 8

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

18.1. مقدمة

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

Image

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

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

Image

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

18.2. الخادم

نحن نركز على جانب الخادم من التطبيق.

Image

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

Image

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

Image

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

18.2.2. طبقة [dao]

Image

تنفذ طبقة [dao] واجهة [InterfaceServerDao] التالية:


<?php
 
// namespace
namespace Application;
 
interface InterfaceServerDao {
 
  // reading tax administration data
  public function getTaxAdminData(): TaxAdminData;
}
  • السطر 9: تسترد طريقة [getTaxAdminData] بيانات إدارة الضرائب من قاعدة بيانات؛

يتم تنفيذ واجهة [InterfaceServerDao] بواسطة فئة [ServerDao] التالية:


<?php
 
// namespace
namespace Application;
 
// definition of a ImpotsWithDataInDatabase class
class ServerDao implements InterfaceServerDao {
  // the TaxAdminData object containing tax bracket data
  private $taxAdminData;
  // the [Database] type object containing the characteristics of the BD
  private $database;
 
  // manufacturer
  public function __construct(string $databaseFilename) {
    // store the JSON configuration of the bd
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // we prepare the attribute
    $this->taxAdminData = new TaxAdminData();
    try {
      // open the database connection
      $connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
      // we want every SGBD error to trigger an exception
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // start a transaction
      $connexion->beginTransaction();
      // fill in the tax bracket table
      $this->getTranches($connexion);
      // fill in the constants table
      $this->getConstantes($connexion);
      // the transaction is completed successfully
      $connexion->commit();
    } catch (\PDOException $ex) {
      // is there a transaction in progress?
      if (isset($connexion) && $connexion->inTransaction()) {
        // transaction ends in failure
        $connexion->rollBack();
      }
      // trace the exception back to the calling code
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // close the connection
      $connexion = NULL;
    }
  }
 
  // reading data from the database
  private function getTranches($connexion): void {

  }
 
  // reading the constants table
  private function getConstantes($connexion): void {

  }
 
  // returns data for tax calculation
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }
 
}

تم عرض هذا الرمز في القسم المرتبط.

18.2.3. طبقة [الأعمال]

Image

Image

تنفذ طبقة [الأعمال] واجهة [InterfaceServerMetier] التالية:


<?php
 
// namespace
namespace Application;
 
interface InterfaceServerMetier {
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}

يتم تنفيذ واجهة [InterfaceServerMetier] بواسطة فئة [ServerMetier] التالية:


<?php
 
// namespace
namespace Application;
 
class ServerMetier implements InterfaceServerMetier {
  // dao layer
  private $dao;
  // tax administration data
  private $taxAdminData;
 
  //---------------------------------------------
  // setter couche [dao]
  public function setDao(InterfaceServerDao $dao) {
    $this->dao = $dao;
    return $this;
  }
 
  public function __construct(InterfaceServerDao $dao) {
    // a reference is stored on the [dao] layer
    $this->dao = $dao;
    // recover data for tax calculation
    // method [getTaxAdminData] may throw a ExceptionImpots exception
    // we then let it go back to the calling code
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }
 
// tAX CALCULATION
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // result
    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 {

    // result
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }
 
  // revenuImposable=annualwage-discount
  // the allowance has a minimum and a maximum
  private function getRevenuImposable(float $salaire): float {

    // result
    return floor($revenuImposable);
  }
 
// calculates any discount
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // result
    return ceil($décôte);
  }
 
// calculates any reduction
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    ..
    // result
    return ceil($réduction);
  }
}

تمت مناقشة هذا الكود بالفعل في الإصدار 1 في القسم المرتبط. تم عرض نسخته الموجهة للكائنات مع قاعدة بيانات في القسم المرتبط.

18.2.4. نص برمجي الخادم

Image

Image

ينفذ البرنامج النصي للخادم طبقة [الويب] [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
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// definition of constants
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
 
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
 
// retrieve the current query
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// does the user exist?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
  $trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
  $i++;
}
// set the response status code
if (!$trouvé) {
  // not found - code 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
  // error msg
  $response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
  $response->send();
  // end
  exit;
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// mistake?
if ($erreur) {
  $erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}

// marital status is restored
if (!$request->query->has("marié")) {
  $erreurs[] = "paramètre marié manquant";
} else {
  $marié = trim(strtolower($request->query->get("marié")));
  $erreur = $marié !== "oui" && $marié !== "non";
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre marié [$marié] invalide";
  }
}
 
// the number of children
if (!$request->query->has("enfants")) {
  $erreurs[] = "paramètre enfants manquant";
} else {
  $enfants = trim($request->query->get("enfants"));
  // number of children must be an integer >=0
  $erreur = !preg_match("/^\d+$/", $enfants);
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre enfants [$enfants] invalide";
  }
}
 
// we recover the annual salary
if (!$request->query->has("salaire")) {
  $erreurs[] = "paramètre salaire manquant";
} else {
  // salary must be an integer >=0
  $salaire = trim($request->query->get("salaire"));
  $erreur = !preg_match("/^\d+$/", $salaire);
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre salaire [$salaire] invalide";
  }
}
 
// other parameters in the query?
foreach (\array_keys($request->query->all()) as $key) {
  // valid parameter?
  if (!\in_array($key, ["marié", "enfants", "salaire"])) {
    $erreurs[] = "paramètre [$key] invalide";}
}
 
// mistakes?
if ($erreurs) {
  // an error code 400 is sent to the customer
  $response->setStatusCode(Response::HTTP_BAD_REQUEST);
  $response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// we have everything you need to work
// server architecture creation
$msgErreur = "";
try {
  // creation of the [dao] layer
  $dao = new ServerDao($config["databaseFilename"]);
  // creation of the [business] layer
  $métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// we note the error
  $msgErreur = utf8_encode($ex->getMessage());
}
// mistake?
if ($msgErreur) {
  // an error code 500 is sent to the customer
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  $response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
$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 في شكل مصفوفة [‘response’=>’something’]؛
  • الأسطر 64–117: نتحقق من صحة الطلب:
    • طلب GET مع ثلاثة معلمات بالضبط؛
    • معلمة [married] يجب أن تكون قيمتها ‘yes’ أو ‘no’؛
    • معلمة [children] يجب أن تكون قيمتها عددًا صحيحًا >= 0؛
    • معلمة [salary] يجب أن تكون قيمتها عددًا صحيحًا >=0؛
  • السطر 65: في كل مرة يتم فيها اكتشاف خطأ، تتم إضافة رسالة خطأ إلى المصفوفة [$errors]؛
  • الأسطر 120–126: في حالة حدوث خطأ، يتم إرسال رمز HTTP [400 Bad Request] إلى العميل (السطر 122)؛
  • السطر 123: توضح استجابة JSON للخادم سبب الخطأ؛
  • بدءًا من السطر 132، تم التحقق من كل شيء. يمكننا إنشاء مثيلات لطبقات [dao، business]. هذا الإنشاء له تكلفة ويجب ألا يتم إلا إذا كنا متأكدين من أن لدينا طلبًا صالحًا؛
  • الأسطر 130–138: يتم إنشاء بنية الخادم. قد يؤدي إنشاء طبقة [DAO] إلى إثارة استثناء [ExceptionImpots]. إذا حدث هذا الاستثناء، يتم تسجيل الخطأ؛
  • الأسطر 135-138: إذا حدث استثناء، فإننا نرسل رمز حالة HTTP 500 إلى العميل. يشير هذا الرمز إلى تعطل الخادم؛
  • السطر 143: يشرح الرد سبب الخطأ؛
  • السطر 148 : يتم تفويض حساب الضريبة إلى طبقة [business]؛
  • السطور 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]، المعلمات الثلاثة [married, children, salary]؛
  • في [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

نقوم بإيقاف تشغيل نظام إدارة قواعد البيانات MySQL ونطلب عنوان URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

Image

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

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

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

Image

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

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

Image

سيكون [ServerDaoTest] كما يلي:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
 
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// test -----------------------------------------------------
 
class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;
 
  public function __construct() {
    // parent
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  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. اختبارات طبقة [business]

Image

سيكون اختبار [ServerMetierTest] كما يلي:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// test class
class ServerMetierTest extends \Codeception\Test\Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    // creation of the [business] layer
    $this->métier = new ServerMetier($dao);
  }
 
  // tests
  public function test1() {

  }
 
  public function test2() {

  }
 
  ..
 
  public function test11() {

  }
 
}

تعليقات

  • الأسطر 9–24: نقوم بإعداد نفس بيئة العمل الخاصة بخادم [impots-server.php]. ويتم ذلك في الأسطر 9–12 عن طريق تعريف الثابتين اللتين تعتمد عليهما البيئة؛
  • الأسطر 30–38: نقوم بإنشاء مثيل لطبقة [business] المراد اختبارها، كما تم في البرنامج النصي للخادم [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
namespace Application;
 
interface InterfaceClientDao {
 
  // reading taxpayer data
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
 
  // recording results
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
  • السطر 9: تقوم الدالة [getTaxPayersData] بتحميل بيانات دافعي الضرائب من الملف [$taxPayersFilename] إلى الذاكرة. وفي حالة وجود أي أخطاء، يتم تسجيلها في الملف [$errorsFilename]؛
  • السطر 12: تقوم الدالة [calculateTaxes] بحساب ضريبة دافع الضرائب؛
  • السطر 15: تقوم الدالة [saveResults] بحفظ البيانات من المصفوفة [$taxPayersData] — التي تمثل نتائج عدة حسابات ضريبية — في الملف [$resultsFilename]؛

يتم تنفيذ واجهة [InterfaceClientDao] بواسطة فئة [ClientDao] التالية:


<?php
 
namespace Application;
 
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
 
class ClientDao implements InterfaceClientDao {
  // using a Trait
  use TraitDao;
  // attributes
  private $urlServer;
  private $user;
 
  // manufacturer
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }
 
  // tAX CALCULATION
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // create a HTTP customer
    $httpClient = HttpClient::create([
        'auth_basic' => [$this->user["login"], $this->user["passwd"]],
        "verify_peer" => false
    ]);
    // make a request to the server
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
    ]]);
    // the answer is retrieved
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // logs
    // print "$json=json\n";
    // retrieve response status
    $statusCode = $response->getStatusCode();
    // mistake?
    if ($statusCode !== 200) {
      // we have an error - we throw an exception
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    // we return the answer
    return $réponse;
  }
 
}

تعليقات

  • السطر 10: ندرج [TraitDao] (انظر الفقرة المرتبطة) التي تنفذ الطريقتين [getTaxPayersData] و [saveResults]. وبذلك يتبقى فقط تنفيذ الطريقة [calculateTaxes]. ويتم تنفيذ ذلك في الأسطر 22–49؛
  • الأسطر 16–19: يأخذ منشئ فئة [ClientDao] معلمتين:
    • عنوان URL [$urlServer] لخادم حساب الضرائب؛
    • المصفوفة [$user] للمفاتيح "login" و"passwd" التي تحدد المستخدم الذي يقوم بالطلب؛
  • السطر 22: تستقبل طريقة [calculateTaxes] المعلمات الثلاثة المراد إرسالها إلى خادم حساب الضرائب؛
  • الأسطر 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 لمصفوفة [‘response’=>something]. نسترد [something]؛
  • السطر 40: نقوم بتسجيل استجابة JSON في وضع التطوير؛
  • السطر 42: نسترد رمز حالة الاستجابة؛
  • الأسطر 44-49: إذا لم يكن رمز حالة HTTP هو 200، فهذا يعني أن الخادم قد واجه مشكلة. ثم نطلق استثناء [ExceptionImpots] مع رسالة تتكون من استجابة JSON للخادم مرفقة برمز حالة HTTP؛
  • السطر 51: نُرجع النتيجة، وهي مصفوفة ترابطية بمفاتيح [tax, surcharge, discount, reduction, rate]؛

18.3.3. الطبقة [التجارية]

Image

Image

تنفذ طبقة [الأعمال] [8] [BusinessClientInterface] التالية:


<?php
 
// namespace
namespace Application;
 
interface InterfaceClientMetier {
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • السطر 9: تقوم الدالة [calculateTaxes] بحساب الضريبة؛
  • السطر 12: تحسب الدالة [executeBatchImpots] الضريبة للمكلفين الذين توجد بياناتهم في الملف [$taxPayersFileName]، وتكتب النتائج في الملف [$resultsFileName]، وتكتب أي أخطاء تمت مواجهتها في الملف [$errorsFileName]؛

يتم تنفيذ واجهة [BusinessClientInterface] بواسطة فئة [BusinessClient] التالية:


<?php
 
// namespace
namespace Application;
 
class ClientMetier implements InterfaceClientMetier {
  // attribute
  private $clientDao;
 
  // manufacturer
  public function __construct(InterfaceClientDao $clientDao) {
    // the reference is stored on the [dao] layer
    $this->clientDao = $clientDao;
  }
  
  // tAX CALCULATION
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // we let the exceptions coming from the [dao] layer flow upwards
    // retrieve taxpayer data
    $taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // results table
    $results = [];
    // we exploit them
    foreach ($taxPayersData as $taxPayerData) {
      // tax calculation
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // complete [$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // put the result in the results table
      $results [] = $taxPayerData;
    }
    // recording results
    $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
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "../Data/config-client.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// definition of constants
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// symfony dependencies
use Symfony\Component\HttpClient\HttpClient;
 
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$clientMetier = new ClientMetier($clientDao);
 
// tax calculation in batch mode
try {
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
  // error is displayed
  print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;

تعليقات

  • السطر 13: مسار ملف التكوين؛
  • السطر 16: معالجة ملف التكوين؛
  • الأسطر 18–26: تحميل التبعيات؛
  • السطر 37: إنشاء طبقة [dao]. نمرر معلومتين يتوقعهما منشئ الطبقة:
    • عنوان URL لخادم حساب الضرائب؛
    • بيانات اعتماد المستخدم الذي سيقوم بتقديم الطلبات؛
  • السطر 39: إنشاء طبقة [business]. نمرر مرجعًا إلى طبقة [dao] التي تم إنشاؤها للتو إلى منشئ الطبقة؛
  • السطر 43: نطلب من طبقة [business] ما يلي:
    • حساب الضرائب لجميع دافعي الضرائب في الملف $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 فقط دون نظام إدارة قواعد البيانات 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. اختبار طبقة [business]

اختبار [ClientMetierTest.php] هو كما يلي:


<?php

// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
 
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// test class
class ClientMetierTest extends \Codeception\Test\Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // creation of the [business] layer
    $this->métier = new ClientMetier($clientDao);
  }
 
  // tests
  public function test1() {

  }
 
  -------------
 
  public function test11() {

  }
 
}

تعليقات

  • الأسطر 10–26: تعريف بيئة الاختبار. نستخدم نفس البيئة المستخدمة في البرنامج النصي الرئيسي [MainImpotsClient] الموصوف في القسم المرتبط؛
  • الأسطر 33–41: إنشاء طبقتي [dao] و[business]؛
  • السطر 40: تشير السمة [$this→business] إلى طبقة [business]؛
  • الأسطر 44–51: الطرق [test1، test2…، test11] هي تلك الموصوفة في القسم المرتبط؛

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

Image