Skip to content

14. عملاء HTTP جافا سكريبت لخدمة حساب الضريبة

14.1. Introduction

نقترح هنا كتابة عميل [node.js] للإصدار 14 من خدمة حساب الضريبة. ستكون بنية العميل/الخادم كما يلي:

Image

سنقوم بدراسة نسختين من العميل:

  • سيكون للإصدار 1 من العميل بنية [main, dao] في الطبقات التالية:

Image

  • سيكون للإصدار 2 من العميل بنية [main, métier, dao]. سيتم نقل الطبقة [métier] من الخادم إلى العميل:

Image

14.2. العميل HTTP 1

Image

كما ذكرنا سابقًا، يقوم العميل HTTP 1 بتنفيذ بنية العميل/الخادم التالية:

Image

سنقوم بتنفيذ:

  • الطبقة [dao] في شكل فئة؛
  • الطبقة [main] في شكل برنامج نصي يستخدم هذه الفئة؛

14.2.1. الطبقة [dao]

سيتم تنفيذ الطبقة [dao] بواسطة الفئة التالية [Dao1.js]:


'use strict';

// استيرادات
import qs from 'qs'

class Dao1 {

  // منشئ
  constructor(axios) {
    // مكتبة axios لإجراء الطلبات HTTP
    this.axios = axios;
    // ملف تعريف ارتباط الجلسة
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // بدء الجلسة
  async  initSession() {
    // خيارات الطلب HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // معلمات URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // تنفيذ الاستعلام HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // خيارات الطلب HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // نص POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // معلمات URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // تنفيذ الطلب HTTP
    return await this.getRemoteData(options);
  }

  // حساب الضريبة
  async  calculerImpot(marié, enfants, salaire) {
    // خيارات الطلب HHTP [post /main.php?action=calculer-impot]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // نص POST [marié, enfants, salaire]
      data: qs.stringify({
        marié: marié,
        enfants: enfants,
        salaire: salaire
      }),
      // معلمات URL
      params: {
        action: 'calculer-impot'
      }
    };
    // تنفيذ الاستعلام HTTP
    const data = await this.getRemoteData(options);
    // النتيجة
    return data;
  }

  // قائمة المحاكاة
  async  listeSimulations() {
    // خيارات الاستعلام HHTP [get /main.php?action=lister-simulations]
    const options = {
      method: "GET",
      // معلمات URL
      params: {
        action: 'lister-simulations'
      },
    };
    // تنفيذ الاستعلام HTTP
    const data = await this.getRemoteData(options);
    // النتيجة
    return data;
  }

  // قائمة المحاكاة
  async  supprimerSimulation(index) {
    // خيارات الاستعلام HHTP  [get /main.php?action=supprimer-simulation&numéro=index]
    const options = {
      method: "GET",
      // معلمات URL
      params: {
        action: 'supprimer-simulation',
        numéro: index
      },
    };
    // تنفيذ الاستعلام HTTP
    const data = await this.getRemoteData(options);
    // النتيجة
    return data;
  }

  async  getRemoteData(options) {
    // لملف تعريف الارتباط الخاص بالجلسة
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // تنفيذ الطلب HTTP
    let response;
    try {
      // طلب غير متزامن
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // المعلمة [error] هي حالة استثنائية - يمكن أن تتخذ أشكالًا مختلفة
      if (error.response) {
        // رد الخادم موجود في [error.response]
        response = error.response;
      } else {
        // يتم إعادة إرسال الخطأ
        throw error;
      }
    }
    // response هي مجمل استجابة الخادم HTTP (رؤوس HTTP + الاستجابة نفسها)
    // يتم استرداد ملف تعريف ارتباط الجلسة إن وجد
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie هو مصفوفة
      // يتم البحث عن ملف تعريف ارتباط الجلسة في هذا المصفوفة
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // نبحث عن ملف تعريف ارتباط الجلسة
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // يتم حفظ ملف تعريف ارتباط الجلسة
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // تم العثور عليه
          trouvé = true;
        } else {
          // العنصر التالي
          i++;
        }
      }
    }
    // رد الخادم موجود في [response.data]
    return response.data;
  }
}

// تصدير الفئة
export default Dao1;
  • نستخدم هنا ما تعلمناه في الفقرة الرابط، حيث قدمنا مكتبة [axios] التي تسمح بإجراء استعلامات HTTP سواء في [node.js] أو في متصفح. سننظر بشكل خاص إلى البرنامج النصي في الفقرة الرابط؛
  • الأسطر 9-15: منشئ الفئة. سيكون لهذه الفئة ثلاث خصائص:
    • [axios]: الكائن [axios] الذي يسمح بإجراء الاستعلامات HTTP. يتم تمرير هذا الكائن بواسطة الكود المستدعي؛
    • [sessionCookieName]: تختلف أسماء ملفات تعريف الارتباط للجلسة باختلاف الخوادم. هنا، هو [PHPSESSID]؛
    • [sessionCookie]: ملف تعريف الارتباط للجلسة الذي يرسله الخادم ويخزنه العميل؛
  • الأسطر 53-76: تقوم الدالة غير المتزامنة [calculerImpot] بإجراء الطلب [post /main.php?action=calculer-impot] عن طريق إرسال المعلمات [marié, enfants, salaire]. وهي تعرض السلسلة jSON المرسلة من الخادم في شكل كائن جافا سكريبت؛
  • السطور 79-92: تقوم الدالة غير المتزامنة [listeSimulations] بإجراء الطلب [get /main.php?action=lister-simulations. وهي تعرض السلسلة jSON التي أرسلها الخادم في شكل كائن جافا سكريبت؛
  • الأسطر 95-109: تقوم الدالة غير المتزامنة [supprimerSimulation] بإجراء الطلب [get /main.php?action=supprimer-simulation&numéro=index]. وتقوم بإرجاع السلسلة jSON المرسلة من الخادم في شكل كائن جافا سكريبت؛
  • السطر 121: يتم استخدام الترميز [this.axios] لأن الكائن [axios] الذي تم إرساله إلى المنشئ قد تم تخزينه في الخاصية [this.axios]؛
  • السطر 161: يتم تصدير الفئة [Dao1] حتى يمكن استخدامها؛

14.2.2. البرنامج النصي [main1.js]

يقوم البرنامج النصي [main1.js] بإجراء سلسلة من المكالمات إلى الخادم باستخدام الفئة [Dao1]:

  • تهيئة جلسة jSON؛
  • التوثيق باستخدام [admin, admin
  • طلب ثلاثة حسابات ضريبية؛
  • طلب قائمة المحاكاة؛
  • حذف إحداها؛

الرمز هو التالي:


// استيراد axios
import axios from 'axios';
// استيراد الفئة Dao1
import Dao from './Dao1';

// وظيفة غير متزامنة [main]
async function main() {
  // تكوين axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // إنشاء مثيل الطبقة [dao]
  const dao = new Dao(axios);
  // استخدام الطبقة [dao]
  try {
    // بدء الجلسة
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // المصادقة
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // حسابات الضرائب
    log("-----------calculer-impot x 3");
    response = await Promise.all([
      dao.calculerImpot("oui", 2, 45000),
      dao.calculerImpot("non", 2, 45000),
      dao.calculerImpot("non", 1, 30000)
    ]);
    log(response);
    // قائمة المحاكاة
    log("-----------liste-des-simulations");
    response = await dao.listeSimulations();
    log(response);
    // حذف محاكاة
    log("-----------suppression simulation n° 1");
    response = await dao.supprimerSimulation(1);
    log(response);
  } catch (error) {
    // تسجيل الخطأ
    console.log("erreur=", error.message);
  }
}

// سجل jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// التنفيذ
main();

تعليقات

  • السطر 2: يتم استيراد المكتبة [axios]؛
  • السطر 4: يتم استيراد الفئة [Dao]؛
  • السطر 7: الدالة [main] التي تتواصل مع الخادم هي دالة غير متزامنة؛
  • السطران 9-10: التكوين الافتراضي لطلبات HTTP التي سيتم إرسالها إلى الخادم:
    • السطر 9: [timeout] لمدة ثانيتين؛
    • السطر 10: جميع URL لها البادئة URL الأساسية للإصدار 14 من خادم حساب الضريبة؛
  • السطر 12: تم إنشاء الطبقة [Dao]. يمكن الآن استخدامها؛
  • الأسطر 46-48: الغرض من الدالة [log] هو عرض السلسلة jSON لكائن جافا سكريبت في شكل منمق: بشكل عمودي مع مسافة بادئة بمقدار مسافتين (المعلمة الثالثة)؛
  • الأسطر 15-18: تهيئة الجلسة jSON؛
  • الأسطر 19-22: المصادقة؛
  • الأسطر 23-30: يتم طلب ثلاثة حسابات ضريبية بالتوازي. بفضل [await Promise.all]، يتم تعليق التنفيذ حتى يتم الحصول على النتائج الثلاثة جميعها؛
  • الأسطر 31-34: قائمة المحاكاة؛
  • الأسطر 35-38: حذف محاكاة؛
  • الأسطر 39-42: إدارة الاستثناء المحتمل؛

نتائج التنفيذ هي كما يلي:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 1\main1.js"
"-----------init-session"
{
  "action": "init-session",
  "état": 700,
  "réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
  "action": "authentifier-utilisateur",
  "état": 200,
  "réponse": "Authentification réussie [admin, admin]"
}
"-----------calculer-impot x 3"
[
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14
    }
  },
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "non",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 3250,
      "surcôte": 370,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.3
    }
  },
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14
    }
  }
]
"-----------liste-des-simulations"
{
  "action": "lister-simulations",
  "état": 500,
  "réponse": [
    {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 3250,
      "surcôte": 370,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.3,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14,
      "arrayOfAttributes": null
    }
  ]
}
"-----------suppression simulation n° 1"
{
  "action": "supprimer-simulation",
  "état": 600,
  "réponse": [
    {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14,
      "arrayOfAttributes": null
    }
  ]
}

[Done] exited with code=0 in 0.516 seconds

14.3. العميل HTTP 2

Image

بنية العميل HTTP2 هي كما يلي:

Image

تم نقل الطبقة [métier] من الخادم إلى عميل Javascript. على عكس ما قمنا به في الدورة التدريبية PHP7، لن تضطر الطبقة [main] هنا إلى المرور عبر الطبقة [métier] للوصول إلى الطبقة [dao]. سنستخدم هاتين الطبقتين كمراكز للخبرات:

  • تمر الطبقة [main] عبر الطبقة [dao] بمجرد احتياجها إلى البيانات الموجودة على الخادم؛
  • تطلب الطبقة [main] من الطبقة [métier] إجراء حسابات الضريبة؛
  • الطبقة [métier] مستقلة عن الطبقة [dao] ولا تستدعيها أبدًا؛

14.3.1. فئة جافا سكريبت [Métier]

تم وصف جوهر الفئة [Métier] في PHP في المقالة المرتبطة. إنه كود معقد إلى حد ما نذكره هنا، ليس لشرحه، بل لترجمته إلى جافا سكريبت:


<?php

// مساحة الأسماء
namespace Application;

class Metier implements InterfaceMetier {
  // طبقة Dao
  private $dao;
  // بيانات الإدارة الضريبية
  private $taxAdminData;

  //---------------------------------------------
  // مُعيّن الطبقة [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceDao $dao) {
    // يتم تخزين مرجع على الطبقة [dao]
    $this->dao = $dao;
    // يتم استرداد البيانات التي تسمح بحساب الضريبة
    // يمكن أن تطلق الطريقة [getTaxAdminData] استثناءً ExceptionImpots
    // ثم نتركها تعود إلى الكود المستدعي
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// حساب الضريبة
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié: نعم، لا
    // $enfants: عدد الأطفال
    // $salaire: الراتب السنوي
    // $this->taxAdminData: بيانات مصلحة الضرائب
    //
    // نتحقق من وجود بيانات مصلحة الضرائب
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // حساب الضريبة مع وجود أطفال
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // حساب الضريبة بدون أطفال
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // تطبيق الحد الأقصى للمعامل الأسري
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART يورو للطفلين الأولين
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART يورو للطفلين الأولين، ضعف ذلك للأطفال التاليين
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // يتم أخذ الضريبة الأعلى
    if ($impot1 > $impot2) {
      $impot = $impot1;
      $taux = $result1["taux"];
      $surcôte = $result1["surcôte"];
    } else {
      $surcôte = $impot2 - $impot1 + $result2["surcôte"];
      $impot = $impot2;
      $taux = $result2["taux"];
    }
    // حساب أي خصم محتمل
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // حساب التخفيض الضريبي المحتمل
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // النتيجة
    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 {
    // $marié: نعم، لا
    // $enfants: عدد الأطفال
    // $salaire: الراتب السنوي
    // $this->taxAdminData: بيانات مصلحة الضرائب
    //
    // عدد الحصص
    $marié = strtolower($marié);
    if ($marié === "oui") {
      $nbParts = $enfants / 2 + 2;
    } else {
      $nbParts = $enfants / 2 + 1;
    }
    // حصة واحدة لكل طفل ابتداءً من الطفل الثالث
    if ($enfants >= 3) {
      // نصف حصة إضافية لكل طفل ابتداءً من الطفل الثالث
      $nbParts += 0.5 * ($enfants - 2);
    }
    // الدخل الخاضع للضريبة
    $revenuImposable = $this->getRevenuImposable($salaire);
    // الزيادة
    $surcôte = floor($revenuImposable - 0.9 * $salaire);
    // لمشاكل التقريب
    if ($surcôte < 0) {
      $surcôte = 0;
    }
    // الحصة العائلية
    $quotient = $revenuImposable / $nbParts;
    // حساب الضريبة
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // يتم وضعه في نهاية جدول الحدود لإنهاء الحلقة التالية
    $limites[count($limites) - 1] = $quotient;
    // البحث عن معدل الضريبة
    $i = 0;
    while ($quotient > $limites[$i]) {
      $i++;
    }
    // نظرًا لوضع $quotient في نهاية الجدول $limites، فإن الحلقة السابقة
    // لا يمكن أن تتجاوز الجدول $limites
    // الآن يمكننا حساب الضريبة
    $impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
    // النتيجة
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=الراتب السنوي - الخصم
  // الخصم له حد أدنى وحد أقصى
  private function getRevenuImposable(float $salaire): float {
    // خصم بنسبة 10% من الراتب
    $abattement = 0.1 * $salaire;
    // لا يمكن أن يتجاوز هذا الخصم $this->taxAdminData->getAbattementDixPourCentMax()
    if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMax();
    }
    // لا يجوز أن يقل الخصم عن $this->taxAdminData->getAbattementDixPourcentMin()
    if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMin();
    }
    // الدخل الخاضع للضريبة
    $revenuImposable = $salaire - $abattement;
    // النتيجة
    return floor($revenuImposable);
  }

// يحسب أي خصم محتمل
  private function getDecôte(string $marié, float $salaire, float $impots): float {
    // في البداية، خصم صفر
    $décôte = 0;
    // الحد الأقصى لمبلغ الضريبة للحصول على الخصم
    $plafondImpôtPourDécôte = $marié === "oui" ?
      $this->taxAdminData->getPlafondImpotCouplePourDecote() :
      $this->taxAdminData->getPlafondImpotCelibatairePourDecote();
    if ($impots < $plafondImpôtPourDécôte) {
      // الحد الأقصى لمبلغ الخصم
      $plafondDécôte = $marié === "oui" ?
        $this->taxAdminData->getPlafondDecoteCouple() :
        $this->taxAdminData->getPlafondDecoteCelibataire();
      // الخصم النظري
      $décôte = $plafondDécôte - 0.75 * $impots;
      // لا يمكن أن يتجاوز الخصم مبلغ الضريبة
      if ($décôte > $impots) {
        $décôte = $impots;
      }
      // لا يوجد خصم <0
      if ($décôte < 0) {
        $décôte = 0;
      }
    }
    // النتيجة
    return ceil($décôte);
  }

// حساب التخفيض المحتمل
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    // الحد الأقصى للدخل للحصول على خصم 20%
    $plafondRevenuPourRéduction = $marié === "oui" ?
      $this->taxAdminData->getPlafondRevenusCouplePourReduction() :
      $this->taxAdminData->getPlafondRevenusCelibatairePourReduction();
    $plafondRevenuPourRéduction += $enfants * $this->taxAdminData->getValeurReducDemiPart();
    if ($enfants > 2) {
      $plafondRevenuPourRéduction += ($enfants - 2) * $this->taxAdminData->getValeurReducDemiPart();
    }
    // الدخل الخاضع للضريبة
    $revenuImposable = $this->getRevenuImposable($salaire);
    // التخفيض
    $réduction = 0;
    if ($revenuImposable < $plafondRevenuPourRéduction) {
      // تخفيض بنسبة 20%
      $réduction = 0.2 * $impots;
    }
    // النتيجة
    return ceil($réduction);
  }

  // حساب الضرائب في الوضع الدفعي
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // السماح بإعادة توجيه الاستثناءات الواردة من الطبقة [dao]
    // استرداد بيانات دافعي الضرائب
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // جدول النتائج
    $results = [];
    // يتم تحليلها
    foreach ($taxPayersData as $taxPayerData) {
      // يتم حساب الضريبة
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // يتم إكمالها [$taxPayerData]
      $taxPayerData->setMontant($result["impôt"]);
      $taxPayerData->setDécôte($result["décôte"]);
      $taxPayerData->setSurCôte($result["surcôte"]);
      $taxPayerData->setTaux($result["taux"]);
      $taxPayerData->setRéduction($result["réduction"]);
      // يتم إدراج النتيجة في جدول النتائج
      $results [] = $taxPayerData;
    }
    // تسجيل النتائج
    $this->dao->saveResults($resultsFileName, $results);
  }

}
  • الأسطر 19-26: منشئ الفئة PHP. نظرًا لأننا ذكرنا أننا نبني طبقة [métier] مستقلة عن الطبقة [dao]، فسنقوم بإجراء تعديلين على هذا المنشئ في Javascript:
    • لن يتلقى مثيلًا من الطبقة [dao] (لم يعد بحاجة إليه)؛
    • لن يطلب البيانات الضريبية من الإدارة [taxAdminData] إلى الطبقة [dao]: فالرمز المستدعي هو الذي سينقل هذه البيانات إلى المنشئ؛
  • السطور 197-122: لن نقوم بتنفيذ الطريقة [executeBatchImpots] التي كان الغرض النهائي منها هو تسجيل نتائج المحاكاة في ملف نصي. نريد كودًا يعمل في كل من [node.js] وفي متصفح. لكن حفظ البيانات على نظام ملفات الجهاز الذي يشغل متصفح العميل غير ممكن؛

مع هذه القيود، يكون كود فئة جافا سكريبت [Métier] كما يلي:


'use strict';

// فئة Métier
class Métier {

  // المنشئ
  constructor(taxAdmindata) {
    // this.taxAdminData: بيانات إدارة الضرائب
    this.taxAdminData = taxAdmindata;
  }

  // حساب الضريبة
  // --------------------------------------------------------------------------
  calculerImpot(marié, enfants, salaire) {
    // متزوج: نعم، لا
    // الأطفال: عدد الأطفال
    // الراتب: الراتب السنوي
    // this.taxAdminData: بيانات مصلحة الضرائب
    //
    // حساب الضريبة مع الأطفال
    const result1 = this.calculerImpot2(marié, enfants, salaire);
    const impot1 = result1["impôt"];
    // حساب الضريبة بدون أطفال
    let result2, impot2, plafondDemiPart;
    if (enfants !== 0) {
      result2 = this.calculerImpot2(marié, 0, salaire);
      impot2 = result2["impôt"];
      // تطبيق الحد الأقصى للمعامل الأسري
      plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
      if (enfants < 3) {
        // PLAFOND_QF_DEMI_PART يورو للطفلين الأولين
        impot2 = impot2 - enfants * plafondDemiPart;
      } else {
        // PLAFOND_QF_DEMI_PART يورو لأول طفلين، ضعف المبلغ للأطفال التاليين
        impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
      }
    } else {
      // لا إعادة حساب للضريبة
      impot2 = impot1;
      result2 = result1;
    }
    // يتم أخذ الضريبة الأعلى في [impot1, impot2]
    let impot, taux, surcôte;
    if (impot1 > impot2) {
      impot = impot1;
      taux = result1["taux"];
      surcôte = result1["surcôte"];
    } else {
      surcôte = impot2 - impot1 + result2["surcôte"];
      impot = impot2;
      taux = result2["taux"];
    }
    // حساب أي خصم محتمل
    const décôte = this.getDecôte(marié, impot);
    impot -= décôte;
    // حساب أي تخفيض محتمل للضرائب
    const réduction = this.getRéduction(marié, salaire, enfants, impot);
    impot -= réduction;
    // النتيجة
    return {
      "impôt": Math.floor(impot), "surcôte": surcôte, "décôte": décôte, "réduction": réduction,
      "taux": taux
    };
  }

  // --------------------------------------------------------------------------
  calculerImpot2(marié, enfants, salaire) {
    // متزوج: نعم، لا
    // الأطفال: عدد الأطفال
    // الراتب: الراتب السنوي
    // this->taxAdminData: بيانات مصلحة الضرائب
    //
    // عدد الحصص
    marié = marié.toLowerCase();
    let nbParts;
    if (marié === "oui") {
      nbParts = enfants / 2 + 2;
    } else {
      nbParts = enfants / 2 + 1;
    }
    // حصة واحدة لكل طفل ابتداءً من الطفل الثالث
    if (enfants >= 3) {
      // نصف حصة إضافية لكل طفل ابتداءً من الطفل الثالث
      nbParts += 0.5 * (enfants - 2);
    }
    // الدخل الخاضع للضريبة
    const revenuImposable = this.getRevenuImposable(salaire);
    // الزيادة
    let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
    // بشأن مشاكل التقريب
    if (surcôte < 0) {
      surcôte = 0;
    }
    // الحصة العائلية
    const quotient = revenuImposable / nbParts;
    // حساب الضريبة
    const limites = this.taxAdminData.limites;
    const coeffR = this.taxAdminData.coeffR;
    const coeffN = this.taxAdminData.coeffN;
    // يتم وضعه في نهاية جدول الحدود لإنهاء الحلقة التالية
    limites[limites.length - 1] = quotient;
    // البحث عن معدل الضريبة
    let i = 0;
    while (quotient > limites[i]) {
      i++;
    }
    // نظرًا لوضع الحصيلة في نهاية جدول الحدود، فإن الحلقة السابقة
    // لا يمكن أن تتجاوز حدود الجدول
    // الآن يمكننا حساب الضريبة
    const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
    // النتيجة
    return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
  }

  // revenuImposable=الراتب السنوي - الخصم
  // الخصم له حد أدنى وحد أقصى
  getRevenuImposable(salaire) {
    // خصم بنسبة 10% من الراتب
    let abattement = 0.1 * salaire;
    // لا يمكن أن يتجاوز هذا الخصم taxAdminData.getAbattementDixPourCentMax()
    if (abattement > this.taxAdminData.abattementDixPourCentMax) {
      abattement = this.taxAdminData.abattementDixPourcentMax;
    }
    // لا يمكن أن يقل الخصم عن taxAdminData.getAbattementDixPourcentMin()
    if (abattement < this.taxAdminData.abattementDixPourcentMin) {
      abattement = this.taxAdminData.abattementDixPourcentMin;
    }
    // الدخل الخاضع للضريبة
    const revenuImposable = salaire - abattement;
    // النتيجة
    return Math.floor(revenuImposable);
  }

  // يحسب أي خصم محتمل
  getDecôte(marié, impots) {
    // في البداية، خصم صفر
    let décôte = 0;
    // الحد الأقصى للضريبة للحصول على الخصم
    let plafondImpôtPourDécôte = marié === "oui" ?
      this.taxAdminData.plafondImpotCouplePourDecote :
      this.taxAdminData.plafondImpotCelibatairePourDecote;
    let plafondDécôte;
    if (impots < plafondImpôtPourDécôte) {
      // الحد الأقصى لمبلغ الخصم
      plafondDécôte = marié === "oui" ?
        this.taxAdminData.plafondDecoteCouple :
        this.taxAdminData.plafondDecoteCelibataire;
      // الخصم النظري
      décôte = plafondDécôte - 0.75 * impots;
      // لا يمكن أن يتجاوز الخصم مبلغ الضريبة
      if (décôte > impots) {
        décôte = impots;
      }
      // لا يوجد خصم <0
      if (décôte < 0) {
        décôte = 0;
      }
    }
    // النتيجة
    return Math.ceil(décôte);
  }

  // حساب التخفيض المحتمل
  getRéduction(marié, salaire, enfants, impots) {
    // الحد الأقصى للدخل للحصول على التخفيض بنسبة 20%
    let plafondRevenuPourRéduction = marié === "oui" ?
      this.taxAdminData.plafondRevenusCouplePourReduction :
      this.taxAdminData.plafondRevenusCelibatairePourReduction;
    plafondRevenuPourRéduction += enfants * this.taxAdminData.valeurReducDemiPart;
    if (enfants > 2) {
      plafondRevenuPourRéduction += (enfants - 2) * this.taxAdminData.valeurReducDemiPart;
    }
    // الدخل الخاضع للضريبة
    const revenuImposable = this.getRevenuImposable(salaire);
    // التخفيض
    let réduction = 0;
    if (revenuImposable < plafondRevenuPourRéduction) {
      // تخفيض بنسبة 20%
      réduction = 0.2 * impots;
    }
    // النتيجة
    return Math.ceil(réduction);
  }
}

// تصدير الفئة
export default Métier;
  • يتبع كود جافا سكريبت بدقة كود PHP؛
  • تم تصدير الفئة [Métier]، السطر 187؛

14.3.2. فئة جافا سكريبت [Dao2]

Image

تنفذ الفئة [Dao2] الطبقة [dao] لعميل جافا سكريبت أعلاه بالطريقة التالية:


'use strict';

// استيرادات
import qs from 'qs'

class Dao2 {

  // المنشئ
  constructor(axios) {
    this.axios = axios;
    // ملف تعريف ارتباط الجلسة
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // بدء الجلسة
  async  initSession() {
    // خيارات الطلب HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // معلمات URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // تنفيذ الاستعلام HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // خيارات الطلب HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // نص POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // معلمات URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // تنفيذ الاستعلام HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // خيارات الاستعلام HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // معلمات الاستعلام URL
      params: {
        action: 'get-admindata'
      }
    };
    // تنفيذ الاستعلام HTTP
    const data = await this.getRemoteData(options);
    // النتيجة
    return data;
  }

  async  getRemoteData(options) {
    // لملف تعريف الارتباط الخاص بالجلسة
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // تنفيذ الطلب HTTP
    let response;
    try {
      // طلب غير متزامن
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // المعلمة [error] هي حالة استثنائية - يمكن أن تتخذ أشكالًا مختلفة
      if (error.response) {
        // رد الخادم موجود في [error.response]
        response = error.response;
      } else {
        // يتم إعادة إرسال الخطأ
        throw error;
      }
    }
    // response هي مجمل استجابة الخادم HTTP (رؤوس HTTP + الاستجابة نفسها)
    // يتم استرداد ملف تعريف ارتباط الجلسة إن وجد
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie هو مصفوفة
      // يتم البحث عن ملف تعريف ارتباط الجلسة في هذا المصفوفة
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // نبحث عن ملف تعريف ارتباط الجلسة
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // يتم حفظ ملف تعريف ارتباط الجلسة
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // تم العثور عليه
          trouvé = true;
        } else {
          // العنصر التالي
          i++;
        }
      }
    }
    // رد الخادم موجود في [response.data]
    return response.data;
  }
}

// تصدير الفئة
export default Dao2;

تعليقات

  • لا تنفذ الفئة [Dao2] سوى ثلاثة من الطلبات الممكنة الموجهة إلى خادم حساب الضرائب:
    • [init-session] (الأسطر 17-29): لتهيئة الجلسة jSON؛
    • [authentifier-utilisateur] (الأسطر 31-50): للمصادقة؛
    • [get-admindata] (الأسطر 52-65): للحصول على بيانات إدارة الضرائب التي ستسمح بإجراء حسابات الضريبة، من جانب العميل؛
  • الأسطر 52-65: نضيف إجراءً جديدًا [get-admindata] إلى الخادم. لم يكن هذا الإجراء مطبقًا حتى الآن. ونقوم بذلك الآن.

14.3.3. تعديل خادم حساب الضريبة

يجب أن يقوم خادم حساب الضريبة بتنفيذ إجراء جديد. سنقوم بذلك على الإصدار 14 من الخادم. يتميز الإجراء المطلوب تنفيذه بالخصائص التالية:

  • يتم طلبها بواسطة عملية [get /main.php?action=get-admindata]؛
  • تُرجع السلسلة jSON لكائن يغلف بيانات الإدارة الضريبية؛

سنراجع كيفية إضافة إجراء إلى خادمنا.

سيتم إجراء التعديل في Netbeans:

Image

في [2]، نقوم بتعديل الملف [config.json] لإضافة الإجراء الجديد:


{
    "databaseFilename": "Config/database.json",
    "corsAllowed": true,
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-14",
    "relativeDependencies": [

        "/Entities/BaseEntity.php",
        "/Entities/Simulation.php",
        "/Entities/Database.php",
        "/Entities/TaxAdminData.php",
        "/Entities/ExceptionImpots.php",

        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php",        

        "/Model/InterfaceServerDao.php",
        "/Model/ServerDao.php",
        "/Model/ServerDaoWithSession.php",
        "/Model/InterfaceServerMetier.php",
        "/Model/ServerMetier.php",

        "/Responses/InterfaceResponse.php",
        "/Responses/ParentResponse.php",
        "/Responses/JsonResponse.php",
        "/Responses/XmlResponse.php",
        "/Responses/HtmlResponse.php",

        "/Controllers/InterfaceController.php",
        "/Controllers/InitSessionController.php",
        "/Controllers/ListerSimulationsController.php",
        "/Controllers/AuthentifierUtilisateurController.php",
        "/Controllers/CalculerImpotController.php",
        "/Controllers/SupprimerSimulationController.php",
        "/Controllers/FinSessionController.php",
        "/Controllers/AfficherCalculImpotController.php",
        "/Controllers/AdminDataController.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php",
        "C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
    ],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Logs/logs.txt",
    "actions":
            {
                "init-session": "\\InitSessionController",
                "authentifier-utilisateur": "\\AuthentifierUtilisateurController",
                "calculer-impot": "\\CalculerImpotController",
                "lister-simulations": "\\ListerSimulationsController",
                "supprimer-simulation": "\\SupprimerSimulationController",
                "fin-session": "\\FinSessionController",
                "afficher-calcul-impot": "\\AfficherCalculImpotController",
                "get-admindata": "\\AdminDataController"
            },
    "types": {
        "json": "\\JsonResponse",
        "html": "\\HtmlResponse",
        "xml": "\\XmlResponse"
    },
    "vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
    "vue-erreurs": "vue-erreurs.php"
}

يتكون التعديل من:

  • السطر 67: إضافة الإجراء [get-admindata] وربطه بوحدة تحكم؛
  • السطر 36: إعلان هذا المتحكم في قائمة الفئات التي يجب تحميلها بواسطة التطبيق PHP؛

الخطوة التالية هي تنفيذ وحدة التحكم [AdminDataController] [3]:


<?php

namespace Application;

// تبعيات Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// اسم مستعار للطبقة [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

class AdminDataController implements InterfaceController {

  // $config هي تكوين التطبيق
  // معالجة طلب Request
  // تستخدم الجلسة Session ويمكنها تعديلها
  // $infos هي معلومات إضافية خاصة بكل وحدة تحكم
  // تُرجع مصفوفة [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // يجب أن يكون هناك معلمة واحدة فقط GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      // يتم تسجيل الخطأ
      $message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
      $état = 1001;
      // إرجاع النتيجة إلى وحدة التحكم الرئيسية
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }

    // يمكن العمل
    // Redis
    \Predis\Autoloader::register();
    try {
      // العميل [predis]
      $redis = new \Predis\Client();
      // يتم الاتصال بالخادم لمعرفة ما إذا كان موجودًا
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // حدث خطأ
      // إرجاع النتيجة مع خطأ إلى وحدة التحكم الرئيسية
      $état = 1050;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }

    // استرداد البيانات من مصلحة الضرائب
    // نبحث أولاً في ذاكرة التخزين المؤقت [redis]
    if (!$redis->get("taxAdminData")) {
      try {
        // البحث عن البيانات الضريبية في قاعدة البيانات
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // taxAdminData
        $taxAdminData = $dao->getTaxAdminData();
        // نضع البيانات المسترجعة في Redis
        $redis->set("taxAdminData", $taxAdminData);
      } catch (\RuntimeException $ex) {
        // حدث خطأ
        // إرجاع النتيجة مع خطأ إلى وحدة التحكم الرئيسية
        $état = 1041;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // يتم أخذ البيانات الضريبية من الذاكرة [redis] ذات النطاق [application]
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      // يتم إنشاء مثيل لكائن [TaxAdminData] من جدول السمات السابق
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
    }

    // إرجاع النتيجة إلى وحدة التحكم الرئيسية
    $état = 1000;
    return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
  }

}

تعليقات

  • السطر 12: مثل وحدات التحكم الأخرى في الخادم، تقوم [AdminDataController] بتنفيذ واجهة [InterfaceController] المكونة من الطريقة [execute] في الأسطر 19-79؛
  • السطر 78: كما هو الحال مع وحدات التحكم الأخرى في الخادم، تُرجع الطريقة [AdminDataController.execute] مصفوفة [$status, $état, [‘réponse’=>$response]] مع:
    • [$status]: رمز حالة الاستجابة HTTP؛
    • [$état]: رمز داخلي للتطبيق يمثل الحالة التي يكون عليها الخادم بعد تنفيذ طلب العميل؛
    • [$response]: مصفوفة تحتوي على الرد المراد إرساله إلى العميل. هنا، سيتم تحويل هذه المصفوفة لاحقًا إلى سلسلة jSON؛
  • الأسطر 25-34: يتم التحقق من صحة صياغة الإجراء [get-admindata] الخاص بالعميل؛
  • الأسطر 37-74: يتم استرداد كائن [TaxAdminData] الذي تم العثور عليه إما:
    • الأسطر 56-59: في قاعدة البيانات إذا لم يتم العثور عليه في ذاكرة التخزين المؤقت [redis]؛
    • الأسطر 70-73: في ذاكرة التخزين المؤقت [redis]؛

يستند هذا الرمز إلى رمز وحدة التحكم [CalculerImpotController] الموضح في المقالة المرتبطة. في الواقع، كان على وحدة التحكم هذه أيضًا استرداد الكائن [TaxAdminData] الذي يغلف بيانات الإدارة الضريبية.

أثناء اختبارات عميل جافا سكريبت، تسبب النموذج jSON من [TaxAdminData] في مشكلة عندما تم العثور على هذا الكائن في ذاكرة التخزين المؤقتة [redis]. لفهم ذلك، دعونا ننظر في الشكل الذي يتم تخزين هذا الكائن به في [redis]:

Image

Image

  • في [5-7]، نلاحظ أن القيم الرقمية قد تم تخزينها على شكل سلاسل أحرف. وقد تعاملت PHP مع ذلك لأن علامة الجمع (+) في العمليات الحسابية بين الأرقام والسلاسل تؤدي ضمناً إلى تغيير نوع السلسلة إلى رقم. لكن جافا سكريبت تفعل العكس: يؤدي عامل + في العمليات الحسابية بين الأرقام وسلاسل الأحرف ضمناً إلى تغيير نوع الرقم إلى سلسلة أحرف. وبالتالي، فإن العمليات الحسابية لفئة جافا سكريبت [Métier] تكون خاطئة؛

لعلاج هذه المشكلة، نقوم بتعديل الطريقة [TaxAdminData.setFromArrayOfAttributes] المستخدمة في السطر 71 من وحدة التحكم لإنشاء مثيل لكائن [TaxAdminData] (انظر المقالة) من السلسلة jSON الموجودة في ذاكرة التخزين المؤقت [redis]:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // شرائح الضريبة
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // ثوابت حساب الضريبة
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // التهيئة
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // الأصل
    parent::setFromJsonFile($taxAdminDataFilename);
    // التحقق من قيم السمات
    $this->checkAttributes();
    // إرجاع الكائن
    return $this;
  }

  protected function check($value): \stdClass {
    // $value هو مصفوفة من العناصر من نوع string أو عنصر واحد
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // يتم تحويل مصفوفة السلاسل إلى مصفوفة من الأعداد الحقيقية
    $newTableau = [];
    $result = new \stdClass();
    // يجب أن تكون عناصر المصفوفة أرقامًا عشرية موجبة أو صفرية
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // نضع العدد العائم في newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // يتم تسجيل الخطأ
        $result->erreur = TRUE;
        // نخرج
        return $result;
      }
    }
    // نُرجع النتيجة
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // قيمة واحدة
      $result->value = $newTableau[0];
    } else {
      // قائمة من القيم
      $result->value = $newTableau;
    }
    return $result;
  }

  // التهيئة بواسطة مصفوفة من السمات
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // الأصل
    parent::setFromArrayOfAttributes($arrayOfAttributes);
    // يتم التحقق من قيم السمات
    $this->checkAttributes();
    // إرجاع الكائن
    return $this;
  }

  // التحقق من قيم السمات
  protected function checkAttributes() {
    // التحقق من أن قيم السمات أعداد حقيقية >=0
    foreach ($this as $key => $value) {
      if (is_string($value)) {
        // يجب أن يكون $value عددًا حقيقيًا >=0 أو مصفوفة من الأعداد الحقيقية >=0
        $result = $this->check($value);
        // خطأ؟
        if ($result->erreur) {
          // يتم إلقاء استثناء
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // يتم تسجيل القيمة
          $this->$key = $result->value;
        }
      }
    }

    // يتم إرجاع الكائن
    return $this;
  }

  // مُستردات ومُعيّنات
  ...

}

تعليقات

  • السطر 5: الفئة [TaxAdminData] تمتد من الفئة [BaseEntity] التي تحتوي بالفعل على الطريقة [setFromArrayOfAttributes]. ونظرًا لأن هذه الطريقة غير مناسبة، فإننا نعيد تعريفها في الأسطر 67-75؛
  • السطر 70: تُستخدم طريقة [setFromArrayOfAttributes] للفئة الأصلية أولاً لتهيئة سمات الفئة؛
  • السطر 72: تتحقق الطريقة [checkAttributes] من أن القيم المرتبطة هي أرقام بالفعل. إذا كانت سلاسل، يتم تحويلها إلى أرقام؛
  • السطر 74: يصبح الكائن [$this] المعروض كائنًا بسمات ذات قيم رقمية؛
  • الأسطر 78-93: تتحقق الطريقة [checkAttributes] من أن القيم المرتبطة بسمات الكائن هي بالفعل أرقام؛
  • السطر 80: يتم استعراض قائمة السمات؛
  • السطر 81: إذا كانت قيمة أحد السمات من النوع [string]؛
  • السطر 83: يتم التحقق من أن هذه السلسلة تمثل رقمًا؛
  • السطر 90: إذا كان الأمر كذلك، يتم تحويل السلسلة إلى رقم وتعيينها إلى السمة التي تم اختبارها؛
  • السطران 85-86: إذا لم يكن الأمر كذلك، يتم إصدار استثناء؛
  • السطور 32-65: تقوم الدالة [check] بأكثر مما هو ضروري. فهي تعالج الجداول والقيم الفردية على حد سواء. ولكن هنا، يتم استدعاؤها فقط للتحقق من قيمة من النوع [string]. وهي تُرجع كائنًا بخصائص [erreur, value] حيث:
    • [erreur] هو قيمة منطقية تشير إلى وجود خطأ أم لا؛
    • [value] هو المعلمة [value] في السطر 32، التي تم تحويلها إلى رقم أو مصفوفة أرقام حسب الحالة؛

تم تعديل الفئة [BaseEntity] التي كان من الممكن أن تحتوي على سمة باسم [arrayOfAttributes] بحيث لم تعد تحتوي عليها: فهي تلوث السلسلة jSON من [TaxAdminData]. يتم إعادة كتابة الفئة على النحو التالي:


<?php

namespace Application;

class BaseEntity {
  
  // التهيئة من ملف JSON
  public function setFromJsonFile(string $jsonFilename) {
    // يتم استرداد محتوى ملف البيانات الضريبية
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // خطأ؟
    if (!$fileContents) {
      // تسجيل الخطأ
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // استرداد الرمز JSON من ملف التكوين في جدول مرتبط
      $arrayOfAttributes = \json_decode($fileContents, true);
      // خطأ؟
      if ($arrayOfAttributes === FALSE) {
        // يتم تسجيل الخطأ
        $erreur = TRUE;
        $message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // خطأ؟
    if ($erreur) {
      // يتم إلقاء استثناء
      throw new ExceptionImpots($message);
    }
    // تهيئة سمات الفئة
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // التحقق من وجود جميع السمات
    $this->checkForAllAttributes($arrayOfAttributes);
    // إرجاع الكائن
    return $this;
  }

  public function checkForAllAttributes($arrayOfAttributes) {
    // التحقق من تهيئة جميع المفاتيح
    foreach (\array_keys($arrayOfAttributes) as $key) {
      if (!isset($this->$key)) {
        throw new ExceptionImpots("L'attribut [$key] de la classe "
          . get_class($this) . " n'a pas été initialisé");
      }
    }
  }

  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // تتم تهيئة بعض سمات الفئة (ليس بالضرورة جميعها)
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // يتم إرجاع الكائن
    return $this;
  }

  // toString
  public function __toString() {
    // سمات الكائن
    $arrayOfAttributes = \get_object_vars($this);
    // سلسلة jSON للكائن
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }

}

تعليقات

  • السطر 20: تم تحويل السمة [$this→arrayOfAttributes] إلى متغير يجب الآن تمريره إلى الطريقة [checkForAllAttributes]، السطر 38 التي كانت تعمل سابقًا على السمة [$this→arrayOfAttributes]؛

بسبب هذا التغيير في [BaseEntity]، يجب أيضًا تعديل الفئة [Database] بشكل طفيف:


<?php

namespace Application;

class Database extends BaseEntity {
  // السمات
  protected $dsn;
  protected $id;
  protected $pwd;
  protected $tableTranches;
  protected $colLimites;
  protected $colCoeffR;
  protected $colCoeffN;
  protected $tableConstantes;
  protected $colPlafondQfDemiPart;
  protected $colPlafondRevenusCelibatairePourReduction;
  protected $colPlafondRevenusCouplePourReduction;
  protected $colValeurReducDemiPart;
  protected $colPlafondDecoteCelibataire;
  protected $colPlafondDecoteCouple;
  protected $colPlafondImpotCelibatairePourDecote;
  protected $colPlafondImpotCouplePourDecote;
  protected $colAbattementDixPourcentMax;
  protected $colAbattementDixPourcentMin;

  // مُعيّن
  // التهيئة
  public function setFromJsonFile(string $jsonFilename) {
    // الأصل
    parent::setFromJsonFile($jsonFilename);
    // يتم إرجاع الكائن
    return $this;
  }

  // مُستردات ومُعيّنات
  ...
}

تعليقات

  • في الكود الأصلي، بعد السطر 30، كان يتم استدعاء الطريقة [parent::checkForAllAttributes]. لم يعد من الضروري القيام بذلك لأن الطريقة [parent::setFromJsonFile($jsonFilename)] تتولى ذلك تلقائيًا الآن؛

14.3.4. اختبارات [Postman] للخادم

تم عرض [Postman] في المقالة الرابط.

نستخدم اختبارات Postman التالية:

Image

Image

Image

النتيجة jSON لهذا الطلب الأخير هي كما يلي:

Image

  • في [5-8]، يمكن ملاحظة أن سمات السلسلة jSON تحتوي بالفعل على قيم رقمية (وليس سلاسل أحرف). ستسمح هذه النتيجة لفئة Javascript [Métier] بالتنفيذ بشكل طبيعي؛

14.3.5. البرنامج النصي الرئيسي [main]

Image

النص البرمجي الرئيسي [main] لعميل جافا سكريبت هو التالي:


// الاستيرادات
import axios from 'axios';

// الاستيرادات
import Dao from './Dao2';
import Métier from './Métier';

// وظيفة غير متزامنة [main]
async function main() {
  // تكوين axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // إنشاء مثيل الطبقة [dao]
  const dao = new Dao(axios);
  // الطلبات HTTP
  let taxAdminData;
  try {
    // بدء الجلسة
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // المصادقة
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // البيانات الضريبية
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response.réponse;
  } catch (error) {
    // تسجيل الخطأ
    console.log("erreur=", error.message);
    // نهاية
    return;
  }

  // إنشاء مثيل الطبقة [métier]
  const métier = new Métier(taxAdminData);

  // حسابات الضريبة
  log("-----------calculer-impot x 3");
  const simulations = [];
  simulations.push(métier.calculerImpot("oui", 2, 45000));
  simulations.push(métier.calculerImpot("non", 2, 45000));
  simulations.push(métier.calculerImpot("non", 1, 30000));
  // قائمة المحاكاة
  log("-----------liste-des-simulations");
  log(simulations);
  // حذف محاكاة
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

// سجل jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// التنفيذ
main();

تعليقات

  • السطران 5-6: استيراد فئتي [Dao] و [Métier]؛
  • السطر 9: الدالة غير المتزامنة [main] التي ستنظم الحوار مع الخادم بفضل الفئة [Dao] وتطلب من الفئة [Métier] إجراء حسابات الضريبة؛
  • الأسطر 10-36: يستدعي البرنامج النصي بشكل متتالي وبطريقة مانعة، طرق [initSession, authentifierUtilisateur, getAdminData] من الطبقة [dao]؛
  • السطر 38: لم نعد بحاجة إلى الطبقة [dao]. لدينا جميع العناصر اللازمة لتشغيل الطبقة [métier] لعميل Javascript؛
  • الأسطر 41-46: نقوم بثلاثة حسابات للضريبة ونجمع النتائج في جدول [simulations]؛
  • السطر 49: نعرض جدول المحاكاة؛
  • السطر 52: نحذف إحدى المحاكاة؛

نتائج تنفيذ البرنامج النصي الرئيسي هي كما يلي:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 2\main2.js"
"-----------init-session"
{
  "action": "init-session",
  "état": 700,
  "réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
  "action": "authentifier-utilisateur",
  "état": 200,
  "réponse": "Authentification réussie [admin, admin]"
}
"-----------get-admindata"
{
  "action": "get-admindata",
  "état": 1000,
  "réponse": {
    "limites": [
      9964,
      27519,
      73779,
      156244,
      0
    ],
    "coeffR": [
      0,
      0.14,
      0.3,
      0.41,
      0.45
    ],
    "coeffN": [
      0,
      1394.96,
      5798,
      13913.69,
      20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
  }
}
"-----------calculer-impot x 3"
"-----------liste-des-simulations"
[
  {
    "impôt": 502,
    "surcôte": 0,
    "décôte": 857,
    "réduction": 126,
    "taux": 0.14
  },
  {
    "impôt": 3250,
    "surcôte": 370,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.3
  },
  {
    "impôt": 1687,
    "surcôte": 0,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.14
  }
]
"-----------suppression simulation n° 1"
[
  {
    "impôt": 502,
    "surcôte": 0,
    "décôte": 857,
    "réduction": 126,
    "taux": 0.14
  },
  {
    "impôt": 1687,
    "surcôte": 0,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.14
  }
]

[Done] exited with code=0 in 0.583 seconds

14.4. العميل HTTP 3

Image

في هذا القسم، نقوم بنقل التطبيق [Client HTTP 2] إلى متصفح وفقًا للبنية التالية:

Image

لا يتم النقل على الفور. إذا كان [node.js] قادرًا على تنفيذ جافا سكريبت ES6، فإن هذا لا ينطبق بشكل عام على المتصفحات. لذلك، يجب استخدام أدوات تقوم بترجمة كود ES6 إلى كود ES5 الذي تفهمه المتصفحات الحديثة. لحسن الحظ، هذه الأدوات قوية وسهلة الاستخدام في الوقت نفسه.

لقد اتبعنا هنا المقالة [How to write ES6 code thats safe to run in the browser - Web Developer's Journal].

في المجلد [client HTTP 3/src]، وضعنا عناصر [main.js, Métier.js, Dao2.js] من تطبيق [Client Http 2] الذي قمنا بتطويره للتو.

14.4.1. تهيئة المشروع

سنعمل في المجلد [client http 3]. نفتح محطة طرفية في [VSCode] وننتقل إلى هذا المجلد:

Image

نقوم بتهيئة هذا المشروع باستخدام الأمر [npm init] ونقبل الإجابات المقترحة افتراضيًا على الأسئلة المطروحة:

Image

  • في [4-5]، ملف تكوين المشروع [package.json] الذي تم إنشاؤه بناءً على الإجابات المختلفة المقدمة؛

14.4.2. تثبيت تبعيات المشروع

سنقوم بتثبيت التبعيات التالية:

  • [@babel/core]: جوهر أداة [Babel] [https://babeljs.io] التي تحول كود ES 2015+ إلى كود قابل للتنفيذ على المتصفحات الحديثة والقديمة؛
  • [@babel/preset-env]: جزء من مجموعة أدوات Babel. يتم استخدامه قبل عملية التحويل ES6 → ES5؛
  • [babel-loader]: تسمح هذه التبعية للأداة [webpack] بالاستعانة بالأداة [Babel]؛
  • [webpack]: قائد الأوركسترا. يستدعي [webpack] أداة Babel لإجراء تحويل الأكواد ES6 → ES5 ثم يقوم هو بتجميع جميع الملفات الناتجة في ملف واحد؛
  • [webpack-cli]: ضروري لـ [webpack]؛
  • [@webpack-cli/init]: يُستخدم لتكوين [webpack
  • [webpack-dev-server]: يوفر خادم ويب للتطوير يعمل افتراضيًا على المنفذ 8080. وعند تعديل الملفات المصدرية، يقوم بإعادة تحميل تطبيق الويب تلقائيًا؛

يتم تثبيت تبعيات المشروع بالطريقة التالية في محطة طرفية لـ [VSCode]:

npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

Image

بعد تثبيت التبعيات، تطور الملف [package.json] على النحو التالي:


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "client jS du serveur de calcul de l'impôt",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "cross-env": "^6.0.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  }
}
  • الأسطر 12-19: تبعيات المشروع هي [devDependencies]: نحتاجها خلال مرحلة التطوير ولكن لا نحتاجها في مرحلة الإنتاج. في الواقع، في مرحلة الإنتاج، يتم استخدام الملف [dist/main.js]. وهو مكتوب بلغة ES5 ولم يعد بحاجة إلى أدوات تحويل الكود من ES6 إلى ES5؛

علينا إضافة تبعيتين إلى المشروع:

  • [core-js]: يحتوي على "بولي فيلز" لـ ECMAScript 2019. يسمح البوليفيل بتنفيذ كود حديث، مثل ECMAScript 2019 (سبتمبر 2019)، على المتصفحات القديمة؛
  • [regenerator-runtime]: وفقًا لموقع المكتبة --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today]؛

تحل هاتان التبعيتان، بدءًا من Babel 7، محل التبعية [@babel/polyfill] التي كانت تؤدي هذا الدور سابقًا والتي أصبحت الآن (سبتمبر 2019) مهملة. يتم تثبيتهما بالطريقة التالية:

Image

ثم يتطور الملف [package.json] على النحو التالي:


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "regenerator-runtime": "^0.13.3"
  }
}

يتطلب استخدام التبعيات [core-js, regenerator-runtime] وضع ما يلي من [imports] (السطران 3-4) في البرنامج النصي الرئيسي [src/main.js]:


// عمليات الاستيراد
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// عمليات الاستيراد
import Dao from './Dao2';
import Métier from './Métier';

14.4.3. تكوين [webpack]

[webpack] هي الأداة التي ستقوم بتوجيه:

  • تحويل ES6 → ES5 لجميع ملفات Javascript في المشروع؛
  • تجميع الملفات التي تم إنشاؤها في ملف واحد؛

يتم تشغيل هذه الأداة بواسطة ملف تكوين [webpack.config.js] الذي يمكن إنشاؤه بفضل تابع يسمى [@webpack-cli/init] (سبتمبر 2019). تم تثبيت هذا التابع مع التبعيات الأخرى المذكورة في الفقرة "الرابط".

نقوم بتنفيذ الأمر [npx webpack-cli init] في محطة طرفية [VSCode]:

Image

بعد الإجابة على الأسئلة المختلفة (حيث يمكن قبول معظم الإجابات المقترحة افتراضيًا)، يتم إنشاء ملف [webpack.config.js] في جذر مشروع [4]:

يبدو ملف [webpack.config.js] كما يلي:


/* eslint-disable */

const path = require('path');
const webpack = require('webpack');

/*
 * SplitChunksPlugin is enabled by default and replaced
 * deprecated CommonsChunkPlugin. It automatically identifies modules which
 * should be splitted of chunk by heuristics using module duplication count and
 * module category (i. e. node_modules). And splits the chunks…
 *
 * It is safe to remove "splitChunks" from the generated configuration
 * and was added as an educational example.
 *
* https://webpack.js.org/plugins/split-chunks-plugin/
 *
 */

const HtmlWebpackPlugin = require('html-webpack-plugin');

/*
 * We've enabled HtmlWebpackPlugin for you! This generates a html
 * page for you when you compile webpack, which will make you start
 * developing and prototyping faster.
 *
* https://github.com/jantimon/html-webpack-plugin
 *
 */

module.exports = {
    mode: 'development',
    entry: './src/index.js',

    output: {
        filename: '[name].[chunkhash].js',
        path: path.resolve(__dirname, 'dist')
    },

    plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],

    module: {
        rules: [
            {
                test: /.(js|jsx)$/,
                include: [path.resolve(__dirname, 'src')],
                loader: 'babel-loader',

                options: {
                    plugins: ['syntax-dynamic-import'],

                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                modules: false
                            }
                        ]
                    ]
                }
            }
        ]
    },

    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    priority: -10,
                    test: /[\\/]node_modules[\\/]/
                }
            },

            chunks: 'async',
            minChunks: 1,
            minSize: 30000,
            name: true
        }
    },

    devServer: {
        open: true
    }
};

لا أفهم كل تفاصيل هذا الملف، لكن يمكن ملاحظة بعض النقاط:

  • السطر 1: لا يحتوي الملف على كود ES6. ثم يعلن [Eslint] عن أخطاء تعود إلى جذر مشروع [javascript]. وهذا أمر مزعج. لمنع Eslint من تحليل ملف ما، يكفي وضع تعليق على السطر 1؛
  • السطر 31: نعمل في وضع [développement]؛
  • السطر 32: البرنامج النصي المدخل هنا هو [src/index.js]. سنضطر إلى تغيير ذلك؛
  • السطر 36: المجلد الذي سيتم فيه تخزين منتجات [webpack] هو المجلد [dist]؛
  • السطر 46: نرى أن [webpack] يستخدم [babel-loader]، وهو أحد التبعيات التي قمنا بتثبيتها؛
  • السطر 54: نرى أن [webpack] يستخدم [@babel-preset/env]، وهو أحد التبعيات التي قمنا بتثبيتها؛

أدى تهيئة [webpack] إلى تعديل الملف [package.json] (يطلب الإذن):


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "regenerator-runtime": "^0.13.3"
  }
}
  • السطر 4: تم تعديله؛
  • السطران 8-9 و18-19: تمت إضافتهما؛
  • السطر 8: المهمة [npm] التي تسمح بتجميع المشروع؛
  • السطر 9: المهمة [npm] التي تسمح بتنفيذه؛
  • السطر 18: ؟
  • السطر 19: يسمح بإنشاء ملف [dist/index.html] الذي يدمج تلقائيًا البرنامج النصي [dist/main.js] الذي تم إنشاؤه بواسطة [webpack]، وهذا هو الملف الذي يتم استخدامه عند تنفيذ المشروع؛

وأخيرًا، أدى تكوين [webpack] إلى إنشاء ملف [src/index.js]:

Image

محتوى [index.js] هو كما يلي (سبتمبر 2019):


console.log("Hello World from your main file!");

14.4.4. تجميع المشروع وتنفيذه

يحتوي الملف [package.json] على ثلاث مهام [npm]:


"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
},

يتم تضمين هذه المهام في [VSCode] الذي يقترحها للتنفيذ:

Image

  • في [1-3]، يتم تجميع المشروع؛
  • في [4]: يتم تجميع المشروع في [dist/main.hash.js] ويتم إنشاء صفحة [dist/index.html

الصفحة [index.html] التي تم إنشاؤها هي التالية:


<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="main.87afc226fd6d648e7dea.js"></script></body>
</html>

تقتصر هذه الصفحة إذن على تغليف الملف [main.hash.js] الذي تم إنشاؤه بواسطة [webpack].

يتم تنفيذ المشروع بواسطة المهمة [start]:

Image

ثم يتم تحميل الصفحة [dist/index.html] على خادم، ينتمي إلى المجموعة [webpack]، ويعمل على المنفذ 8080 للجهاز المحلي ويتم عرضها بواسطة متصفح الجهاز الافتراضي:

Image

  • في [2]، منفذ خدمة خادم الويب لـ [webpack]؛
  • إلى [3]، نص الصفحة [dist/index.html] فارغ؛
  • في [4]، علامة التبويب [console] لأدوات تطوير المتصفح، هنا Firefox (F12
  • في [5]، نتيجة تنفيذ الملف [src/index.js]. نذكر أن محتوى هذا الملف كان كما يلي:
console.log("Hello World from your main file!");

الآن، لنغير هذا المحتوى إلى السطر التالي:

console.log("Bonjour le monde");

تلقائيًا (دون إعادة تجميع)، يتم إنشاء ملفات [main.js, index.html] جديدة ويتم تحميل الملف الجديد [index.html] في المتصفح:

Image

ليس من الضروري تنفيذ المهمة [build] قبل المهمة [start]: فهذه الأخيرة تقوم أولاً بتجميع المشروع. وهي لا تخزن نواتج هذا التجميع في المجلد [dist]. وللتأكد من ذلك، يكفي حذف هذا المجلد. وسنلاحظ عندئذ أن المهمة [start] تقوم بتجميع المشروع وتنفيذه دون إنشاء المجلد [dist]. يبدو أنها تخزن مخرجاتها [index.html, main.hash.js] في مجلد خاص بـ [webpackdev-server]. هذا السلوك كافٍ لاختباراتنا.

عند تشغيل خادم التطوير، تؤدي أي تعديلات يتم حفظها في أحد ملفات المشروع إلى إعادة التجميع. ولهذا السبب، نقوم بتعطيل الوضع [Auto Save] في [VSCode]. فنحن لا نرغب في إعادة التجميع بمجرد كتابة أحرف في أحد ملفات المشروع. نريد إعادة التحويل البرمجي فقط عند حفظ التعديلات:

Image

  • في [2]، يجب عدم تحديد الخيار [Auto Save]؛

14.4.5. اختبارات عميل جافا سكريبت لخادم حساب الضريبة

لاختبار عميل جافا سكريبت لخادم حساب الضريبة، يجب تعيين [main.js] [1] كنقطة دخول للمشروع في الملف [webpack.config.js] [2-3]:

Image

لا ننسى أن البرنامج النصي [main.js] يجب أن يتضمن استيرادين إضافيين مقارنة بإصداره في [Client http 2]:

Image

بالإضافة إلى ذلك، قمنا بتعديل الكود بشكل طفيف لمعالجة الأخطاء التي قد يرسلها الخادم:


// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// استيرادات
import Dao from './Dao2';
import Métier from './Métier';

// وظيفة غير متزامنة [main]
async function main() {
  // تكوين axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // إنشاء مثيل الطبقة [dao]
  const dao = new Dao(axios);
  // الطلبات HTTP
  let taxAdminData;
  try {
    // بدء الجلسة
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    if (response.état != 700) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // المصادقة
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    if (response.état != 200) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // البيانات الضريبية
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    if (response.état != 1000) {
      throw new Error(JSON.stringify(response.réponse));
    }
    taxAdminData = response.réponse;
  } catch (error) {
    // تسجيل الخطأ
    console.log("erreur=", error.message);
    // نهاية
    return;
  }

  // إنشاء مثيل الطبقة [métier]
  const métier = new Métier(taxAdminData);

  // حسابات الضريبة
  log("-----------calculer-impot x 3");
  const simulations = [];
  simulations.push(métier.calculerImpot("oui", 2, 45000));
  simulations.push(métier.calculerImpot("non", 2, 45000));
  simulations.push(métier.calculerImpot("non", 1, 30000));
  // قائمة المحاكاة
  log("-----------liste-des-simulations");
  log(simulations);
  // حذف محاكاة
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

// سجل jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// التنفيذ
main();

تعليقات

  • في الأسطر [24-26] و [31-33] و [38-40]، يتم اختبار الرمز [response.état] المرسل في استجابة الخادم jSON. إذا كان هذا الرمز يشير إلى وجود خطأ، يتم إصدار استثناء مع رسالة خطأ هي السلسلة jSON من استجابة الخادم [response.réponse]؛

وبعد ذلك، نقوم بتنفيذ المشروع [5-6].

ثم يتم إنشاء الصفحة [index.html] وتحميلها في المتصفح:

Image

  • في [7]، نرى أن الإجراء [init-session] لم يتمكن من الاكتمال بسبب مشكلة [CORS] (Cross-Origin Resource Sharing

تنبع المشكلة CORS من العلاقة بين العميل والخادم:

  • تم تنزيل عميل Javascript الخاص بنا على الجهاز [http://localhost:8080]؛
  • يعمل خادم حساب الضرائب على الجهاز [http://localhost:80
  • وبالتالي، لا يقع العميل والخادم في نفس المجالات (نفس الجهاز ولكن ليس نفس المنفذ)؛
  • المتصفح الذي يقوم بتشغيل عميل جافا سكريبت الذي تم تحميله من الجهاز [http://localhost:8080] يحظر أي طلب لا يستهدف [http://localhost:80]. وهذا إجراء أمني. ولذلك، فإنه يحظر طلب العميل الموجه إلى الخادم الذي يعمل على الجهاز [http://localhost:80]؛

في الواقع، لا يحظر المتصفح الطلب تمامًا. بل ينتظر في الواقع حتى "يخبره" الخادم أنه يقبل الطلبات بين المجالات. إذا حصل على هذا الإذن، فسيقوم المتصفح عندئذٍ بإرسال الطلب بين المجالات.

يمنح الخادم الإذن عن طريق إرسال رؤوس HTTP معينة:

1
2
3
4
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Headers: Accept, Content-Type
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Credentials: true
  • السطر 1: يعمل عميل جافا سكريبت على المجال [http://localhost:8080]. يجب على الخادم أن يرد صراحةً بأنه يقبل هذا المجال؛
  • السطر 2: سيستخدم عميل جافا سكريبت في طلباته الرؤوس HTTP [Accept, Content-Type]:
    • [Accept]: يتم إرسال هذا الرأس في كل طلب؛
    • [Content-Type]: يتم استخدام هذا الرأس في عمليات POST للإشارة إلى نوع معلمات POST؛

يجب أن يقبل الخادم صراحة هذين الرأسين HTTP؛

  • السطر 3: سيستخدم عميل جافا سكريبت الطلبات GET و POST. يجب أن يقبل الخادم صراحة هذين النوعين من الطلبات؛
  • السطر 4: سيقوم عميل جافا سكريبت بإرسال ملفات تعريف الارتباط للجلسة. يقبلها الخادم مع رأس السطر 4؛

لذلك، يتعين علينا تعديل الخادم. نقوم بذلك في [Netbeans]. مشكلة CORS هي مشكلة تحدث فقط في وضع التطوير. في بيئة الإنتاج، سيعمل العميل والخادم في نفس المجال [http://localhost:80] ولن تكون هناك مشكلة CORS. لذلك نحتاج إلى وسيلة للسماح أو عدم السماح بطلبات CORS من خلال تكوين الخادم.

Image

تتم تعديلات الخادم في ثلاثة أماكن:

  • [1, 4]: في ملف التكوين [config.json] لإضافة متغير منطقي يتحكم في قبول أو رفض الطلبات عبر النطاقات؛
  • [2]: في الفئة [ParentResponse] التي ترسل الرد إلى عميل جافا سكريبت. وهي التي سترسل رؤوس CORS التي يتوقعها متصفح العميل؛
  • [3]: في الفئات [HtmlResponse, JsonResponse, XmlResponse] التي تولد الردود لجلسات [html, json, xml] على التوالي. يجب أن تمرر هذه الفئات إلى فئتها الأم [2]، القيمة المنطقية [corsAllowed] الموجودة في [4]. ويتم ذلك في [5]، عن طريق تمرير مصفوفة الصور من الملف jSON إلى [2]؛

تتطور الفئة [ParentResponse] [2] على النحو التالي:


<?php

namespace Application;

// تبعيات Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ParentResponse {

  // int $statusCode: رمز حالة الاستجابة HTTP
  // سلسلة $content: نص الاستجابة المراد إرسالها
  // حسب الحالة، تكون سلسلة JSON، XML، HTML
  // مصفوفة $headers: الرؤوس HTTP المراد إضافتها إلى الرد

  public function sendResponse(
    Request $request,
    int $statusCode,
    string $content,
    array $headers,
    array $config): void {

    // إعداد نص الرد من الخادم
    $response = new Response();
    $response->setCharset("utf-8");
    // رمز الحالة
    $response->setStatusCode($statusCode);
    // رؤوس لطلبات عبر النطاقات
    if ($config['corsAllowed']) {
      $origin = $request->headers->get("origin");
      if (strpos($origin, "http://localhost") === 0) {
        $headers = array_merge($headers,
          ["Access-Control-Allow-Origin" => $origin,
            "Access-Control-Allow-Headers" => "Accept, Content-Type",
            "Access-Control-Allow-Methods" => "GET, POST",
            "Access-Control-Allow-Credentials" => "true"
        ]);
      }
    }
    foreach ($headers as $text => $value) {
      $response->headers->set($text, $value);
    }
    // حالة خاصة لطريقة [OPTIONS]
    // فقط الرؤوس مهمة في هذه الحالة
    $method = strtolower($request->getMethod());
    if ($method === "options") {
      $content = "";
      $response->setStatusCode(Response::HTTP_OK);
    }
    // يتم إرسال الرد
    $response->setContent($content);
    $response->send();
  }

}
  • السطر 29: يتم التحقق مما إذا كان يجب معالجة الطلبات عبر النطاقات. إذا كان الأمر كذلك، فسيتم إنشاء الرؤوس HTTP CORS (الأسطر 33-37) حتى لو لم يكن الطلب الحالي طلبًا عبر النطاقات. في هذه الحالة الأخيرة، ستكون الرؤوس CORS عديمة الفائدة ولن يستخدمها العميل؛
  • السطر 30: في طلب عبر النطاقات، يرسل متصفح العميل الذي يستعلم من الخادم رأس HTTP [Origin: http://localhost:8080] (في الحالة المحددة لعميل Javascript الخاص بنا). السطر 30، يتم استرداد رأس HTTP هذا في الطلب [$request]؛
  • السطر 31: لن يتم قبول الطلبات عبر النطاقات إلا من الجهاز [http://localhost]. نذكر أن هذه الطلبات لا تحدث إلا في وضع تطوير المشروع؛
  • الأسطر 32-36: نضيف الرؤوس CORS إلى الرؤوس الموجودة بالفعل في الجدول [$headers]؛
  • الأسطر 45-49: قد تختلف الطريقة التي يطلب بها متصفح العميل أذونات CORS باختلاف العميل الذي يتم تشغيله. في بعض الأحيان، يطلب متصفح العميل هذه الأذونات باستخدام الأمر HTTP [OPTIONS]. وهذا أمر جديد بالنسبة لخادمنا الذي تم تصميمه لخدمة الأوامر [GET, POST] فقط. في حالة الأمر [OPTIONS]، يقوم الخادم حاليًا بإنشاء استجابة خطأ. في الأسطر 46-49، نقوم بتصحيح ذلك في اللحظة الأخيرة: إذا لاحظنا في السطر 46 أن الأمر الحالي هو أمر [OPTIONS]، فإننا ننشئ للعميل:
    • السطور 47 و51: استجابة فارغة [$content]؛
    • السطر 48: رمز حالة 200 يشير إلى نجاح الطلب. الشيء الوحيد المهم لهذا الطلب هو إرسال رؤوس CORS من الأسطر 33-36. هذا ما يتوقعه متصفح العميل؛

بمجرد تصحيح الخادم بهذه الطريقة، يعمل عميل جافا سكريبت بشكل أفضل ولكنه يعرض خطأً جديدًا:

Image

  • في [1]، يتم تهيئة الجلسة jSON بشكل صحيح؛
  • في [2]، تفشل الإجراء [authentifier-utilisateur]: يشير الخادم إلى عدم وجود جلسة عمل جارية. وهذا يعني أن عميل جافا سكريبت لم يرسل إليه بشكل صحيح ملف تعريف الارتباط الخاص بالجلسة الذي أرسله أثناء الإجراء [init-session]؛

دعونا نلقي نظرة على التبادلات الشبكية التي حدثت:

Image

  • في [4]، الطلب [init-session]. وقد سارت العملية بشكل جيد مع رمز 200 لحالة الاستجابة؛
  • في [5]، الطلب [authentifier-utilisateur]. وقد فشل هذا الطلب برمز 400 (Bad Request) [6] لحالة الاستجابة؛

إذا فحصنا الرؤوس HTTP [7] لطلب [5]، يمكننا أن نرى أن عميل جافا سكريبت لم يرسل الرأس HTTP [Cookie] الذي كان سيسمح له بإعادة إرسال ملف تعريف الارتباط للجلسة الذي أرسله الخادم في البداية. وهذا هو السبب في أن الخادم يعلن أنه لا توجد جلسة.

لكي يرسل العميل ملف تعريف ارتباط الجلسة، يجب إضافة تكوين إلى الكائن [axios]:


// الاستيرادات
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// استيرادات
import Dao from './Dao2';
import Métier from './Métier';

// وظيفة غير متزامنة [main]
async function main() {
  // تكوين axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  axios.defaults.withCredentials = true;
  // إنشاء مثيل الطبقة [dao]
  const dao = new Dao(axios);
  // الطلبات HTTP
  let taxAdminData;
...

يطلب السطر 15 تضمين ملفات تعريف الارتباط في رؤوس HTTP لطلب [axios]. تجدر الإشارة إلى أن هذا لم يكن ضروريًا في بيئة [node.js]. لذلك، هناك اختلافات في الكود بين البيئتين.

بمجرد تصحيح هذا الخطأ، يعمل عميل جافا سكريبت بشكل طبيعي:

Image

Image

14.5. تحسين العميل HTTP 3

عندما يتم تنفيذ الفئة [Dao2] السابقة داخل متصفح، لا تكون هناك حاجة لإدارة ملف تعريف ارتباط الجلسة. في الواقع، يقوم المتصفح الذي يستضيف الطبقة [dao] بإدارة ملف تعريف ارتباط الجلسة: فهو يعيد تلقائيًا أي ملف تعريف ارتباط يرسله إليه الخادم. وبالتالي، يمكن إعادة كتابة الفئة [Dao2] لتصبح الفئة [Dao3] التالية:


"use strict";

// عمليات الاستيراد
import qs from "qs";

class Dao3 {
  // منشئ
  constructor(axios) {
    this.axios = axios;
  }

  // بدء الجلسة
  async initSession() {
    // خيارات الطلب HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // معلمات URL
      params: {
        action: "init-session",
        type: "json"
      }
    };
    // تنفيذ الاستعلام HTTP
    return await this.getRemoteData(options);
  }

  async authentifierUtilisateur(user, password) {
    // خيارات الاستعلام HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        "Content-type": "application/x-www-form-urlencoded"
      },
      // نص POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // معلمات URL
      params: {
        action: "authentifier-utilisateur"
      }
    };
    // تنفيذ الاستعلام HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // خيارات الاستعلام HHTP  [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // معلمات URL
      params: {
        action: "get-admindata"
      }
    };
    // تنفيذ الطلب HTTP
    const data = await this.getRemoteData(options);
    // النتيجة
    return data;
  }

  async getRemoteData(options) {
    // تنفيذ الاستعلام HTTP
    let response;
    try {
      // استعلام غير متزامن
      response = await this.axios.request("main.php", options);
    } catch (error) {
      // المعلمة [error] هي حالة استثنائية - يمكن أن تتخذ أشكالًا مختلفة
      if (error.response) {
        // رد الخادم موجود في [error.response]
        response = error.response;
      } else {
        // يتم إعادة إرسال الخطأ
        throw error;
      }
    }
    // response هي مجموعة استجابة الخادم HTTP (رؤوس HTTP + الاستجابة نفسها)
    // رد الخادم موجود في [response.data]
    return response.data;
  }
}

// تصدير الفئة
export default Dao3;

اختفى كل ما يتعلق بإدارة ملف تعريف الارتباط الإداري.

نقوم بتعديل المشروع السابق بالطريقة التالية:

Image

في المجلد [src]، أضفنا ملفين:

  • فئة [Dao3] التي قدمناها للتو؛
  • الملف [main3] المسؤول عن تشغيل الإصدار الجديد؛

يظل الملف [main3] مطابقًا للملف [main] من الإصدار السابق، لكنه يستخدم الآن الفئة [Dao3]:


// عمليات الاستيراد
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";

// الاستيرادات
import Dao from "./Dao3";
import Métier from "./Métier";

// وظيفة غير متزامنة [main]
async function main() {
  // تكوين axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL =
    "http://localhost/php7/scripts-web/impots/version-14";
  axios.defaults.withCredentials = true;
  // إنشاء مثيل الطبقة [dao]
  const dao = new Dao(axios);
  // طلبات HTTP
  ...
}

// سجل jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// التنفيذ
main();

تم تعديل الملف [webpack.config] ليقوم الآن بتنفيذ البرنامج النصي [main3]:


/* eslint-disable */

const path = require("path");
const webpack = require("webpack");

/*
 * SplitChunksPlugin is enabled by default and replaced
 * deprecated CommonsChunkPlugin. It automatically identifies modules which
 * should be splitted of chunk by heuristics using module duplication count and
 * module category (i. e. node_modules). And splits the chunks…
 *
 * It is safe to remove "splitChunks" from the generated configuration
 * and was added as an educational example.
 *
* https://webpack.js.org/plugins/split-chunks-plugin/
 *
 */

const HtmlWebpackPlugin = require("html-webpack-plugin");

/*
 * We've enabled HtmlWebpackPlugin for you! This generates a html
 * page for you when you compile webpack, which will make you start
 * developing and prototyping faster.
 *
* https://github.com/jantimon/html-webpack-plugin
 *
 */

module.exports = {
  mode: "development",
  //الإدخال: "./src/mainjs"،
  entry: "./src/main3.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },

  plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};

بعد ذلك، يتم تنفيذ المشروع بعد تشغيل خادم حساب الضريبة:

Image

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

14.6. Conclusion

لدينا الآن جميع الأدوات اللازمة لتطوير كود جافا سكريبت لتطبيق ويب. يمكننا:

  • استخدام أحدث كود ECMAScript؛
  • اختبار عناصر منفصلة من هذا الكود في بيئة [node.js] أبسط للتصحيح والاختبار؛
  • نقل هذا الكود بعد ذلك إلى متصفح بفضل أدوات [babel] و [webpack]؛