Skip to content

20. تمرين التطبيق – الإصدار 10

أظهرت النسخة السابقة أن البيانات الضريبية، التي يتشاركها جميع مستخدمي التطبيق، يجب تخزينها في ذاكرة ذات نطاق [Application]. سنستخدم خادم Redis [https://redis.io] لتنفيذ ذلك.

20.1. Redis

سيتم تنفيذ ذاكرة النطاق [Application] بواسطة خادم Redis. وستكون البرامج النصية PHP التي تحتاج إلى ذاكرة التطبيق هذه عملاء لهذا الخادم:

Image

20.2. تثبيت Redis

يأتي Laragon مزودًا بخادم Redis غير مفعل افتراضيًا. لذا يجب البدء بتفعيله:

Image

  • في [3]، قم بتنشيط الخادم [Redis]؛
  • في [4]، اترك المنفذ [6379] الذي يستخدمه عملاء Redis افتراضيًا؛

يتم إعادة تشغيل خدمات Laragon تلقائيًا بعد تفعيل Redis:

Image

20.3. عميل Redis في وضع الأوامر

يمكن الاستعلام عن خادم Redis في وضع الأوامر. نفتح محطة طرفية لـ Laragon (انظر الفقرة "الرابط"):

Image

  • في [1]، يقوم الأمر [redis-cli] بتشغيل العميل في وضع الأوامر لخادم Redis؛

في يوليو 2019، يمكن لعميل Redis استخدام 172 أمرًا للتواصل مع الخادم [https://redis.io/commands#list]. أحدها، [command count] [2]، يعرض هذا الرقم [3].

لن نعرض سوى الأوامر التي سنحتاجها في تطبيقنا PHP. سنستخدم Redis لغرض واحد فقط: تخزين مصفوفة [‘attribut’=>’valeur’] في ذاكرة Redis. ويتم ذلك باستخدام أمر Redis [set attribut valeur] [4]. ويمكن بعد ذلك استرداد القيمة باستخدام الأمر [get attribut] [5]. وهذا كل ما سنحتاج إليه.

قد يكون من الضروري إفراغ ذاكرة Redis. ويتم ذلك باستخدام الأمر [flushdb] [6]. وبعد ذلك، إذا طلبنا قيمة السمة [titre] [7]، فسنحصل على مرجع [nil] [8] يشير إلى أن السمة لم يتم العثور عليها. يمكن أيضًا استخدام الأمر [exists] [9-10] للتحقق من وجود سمة.

للخروج من عميل Redis، اكتب الأمر [quit] [11].

20.4. تثبيت عميل Redis لـ PHP

علينا الآن تثبيت عميل Redis لـ PHP:

Image

توجد عدة مكتبات تُنفذ عميل Redis. سنستخدم المكتبة [Predis] [https://github.com/nrk/predis] (يوليو 2019). يتم تثبيت هذه المكتبة، مثل سابقاتها، باستخدام [composer] في محطة Laragon:

Image

20.5. رمز الخادم

Image

يتطور ملف التكوين [config-server.json] على النحو التالي:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-10",
    "databaseFilename": "Data/database.json",
    "relativeDependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Entities/TaxAdminData.php",
        "/../version-08/Entities/Database.php",
        "/../version-08/Dao/InterfaceServerDao.php",
        "/../version-08/Dao/ServerDao.php",
        "/../version-09/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/../version-09/Utilities/Logger.php",
        "/../version-09/Utilities/SendAdminMail.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": "Data/logs.txt"
}

تعليقات

  • الأسطر 5-15: لا تضيف النسخة 10 أي جديد باستثناء البرنامج النصي [impots-server.php]. وهي تستخدم عناصر من النسختين 08 و09؛
  • السطر 19: تبعيات ضرورية لمكتبة [predis] التي تم تثبيتها للتو؛

يتطور كود الخادم [impots-server.php] على النحو التالي:


<?php

// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);

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

// إدارة الأخطاء بواسطة PHP
ini_set("display_errors", "0");
//
// مسار ملف التكوين
define("CONFIG_FILENAME", "Data/config-server.json");
// الاسم المستعار للفئة
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

// الجلسة
$session = new Session();
$session->start();


// السجل الأول
$logger->write("\n---nouvelle requête\n");

// استرداد الطلب الحالي
$request = Request::createFromGlobals();
// المصادقة في المرة الأولى فقط
if (!$session->has("user")) {

} else {
  // السجل
  $logger->write("Authentification prise en session…\n");
}

// المستخدم صالح - يتم التحقق من المعلمات المستلمة
$erreurs = [];
// يجب أن تكون هناك ثلاثة معلمات GET
$method = strtolower($request->getMethod());


// أخطاء؟
if ($erreurs) {
// يتم إرسال رمز الخطأ 400 HTTP_BAD_REQUEST إلى العميل
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // انتهى
  exit;
} else {
  // السجلات
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}

// لدينا كل ما نحتاجه للعمل
// Redis
\Predis\Autoloader::register();
try {
  // العميل [predis]
  $redis = new \Predis\Client();
  // نقوم بالاتصال بالخادم للتحقق من وجوده
  $redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
  // خطأ داخلي في الخادم
  doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
  // انتهى
  exit;
}

// إنشاء الطبقة [dao]
if (!$redis->get("taxAdminData")) {
  // يتم استخراج البيانات الضريبية من قاعدة البيانات
  $logger->write("données fiscales prises en base de données\n");
  try {
    // إنشاء الطبقة [dao]
    $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
    // يتم وضع البيانات الضريبية في ذاكرة النطاق [application]
    // سيتم استدعاء الطريقة [TaxAdminData]->__toString ضمناً
    $redis->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // يُلاحظ وجود خطأ
    doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
    // انتهى
    exit;
  }
} else {
  // يتم استرداد البيانات الضريبية من ذاكرة النطاق [application]
  $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
  $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
  // إنشاء مثيل للطبقة [dao]
  $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
  // السجلات
  $logger->write("données fiscales prises dans redis\n");
}
// إنشاء الطبقة [métier]
$métier = new ServerMetier($dao);
// حساب الضريبة
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// يتم تقديم الإجابة
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// نهاية
exit;

function doInternalServerError(string $message, Response $response, array $infos,
  Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $message: رسالة الخطأ
  // $response: الرد HTTP
  // $infos: جدول المعلومات لإرسال البريد الإلكتروني
  // $result: جدول النتائج
  // $logger: سجل التطبيق
  // $predisClient: عميل [predis]
  //
  // يتم إرسال بريد إلكتروني إلى المسؤول
  // SendAdminMail يعترض جميع الاستثناءات ويسجلها بنفسه
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // يتم إرسال رمز الخطأ 500 إلى العميل
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}

// وظيفة إرسال الرد HTTP إلى العميل
function sendResponse(Response $response, array $result, int $statusCode,
  array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $response: الرد HTTP
  // $result: جدول النتائج
  // $statusCode: حالة الرد HTTP
  // $headers: رؤوس HTTP التي يجب إدراجها في الرد
  // $logger: مسجل سجلات التطبيق
  // $predisClient: عميل [predis]
  //
  // الحالة HTTTP
  $response->setStatusCode($statusCode);
  // النص الأساسي
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // رؤوس
  $response->headers->add($headers);
  // الإرسال
  $response->send();
  // السجل
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
  // إغلاق الاتصال [redis]
  if ($predisClient != NULL) {
    $predisClient->disconnect();
  }
}

تعليقات

  • السطر 15: يتم تعيين الاسم المستعار [ServerDaoWithRedis] للفئة [\Application\ServerDaoWithSession] لتعكس التغيير في تنفيذ البرنامج النصي للخادم؛
  • السطران 18-19: يتم الاحتفاظ بالجلسة. وهناك معلومتان يجب تذكرهما هنا:
    • حقيقة أن المستخدم قد قام بالمصادقة بشكل صحيح. هذه المعلومة ذات نطاق [session]: فهي مرتبطة بمستخدم محدد وليست صالحة للمستخدمين الآخرين؛
    • بيانات إدارة الضرائب. هذه المعلومة تنتمي إلى النطاق [application]: فهي غير مرتبطة بمستخدم محدد ولكنها صالحة لجميع المستخدمين؛
  • الأسطر 54-64: إنشاء العميل [redis] الذي سيتواصل مع الخادم [redis]. سيتواصل هذا العميل مع المنفذ الافتراضي للخادم. إذا كان الخادم لا يتواصل عبر منفذه الافتراضي أو إذا لم يكن موجودًا على الجهاز [localhost]، فسيكون من الضروري تمرير هذه المعلومات إلى مُنشئ الفئة [\Predis\Client]؛
  • السطر 59: يتم توصيل العميل بالخادم على الفور لمعرفة ما إذا كان يستجيب؛
  • الأسطر 60-65: إذا فشل الاتصال بخادم Redis، يتم إرسال رد خطأ إلى العميل، كما سيتم إرسال بريد إلكتروني إلى مسؤول التطبيق؛
  • السطر 67: نطلب من الخادم [redis] المفتاح [taxAdminData]. إذا لم يتم العثور عليه، يتم أخذ البيانات الضريبية من قاعدة البيانات (السطر 72)؛
  • السطر 75: يتم وضع المفتاح [taxAdminData] في الذاكرة [redis] المرتبطة بالسلسلة jSON للمتغير [$taxAdminData] الذي يمثل كائنًا من النوع [TaxAdminData]. تتوقع الطريقة [$redis→set] سلسلة أحرف كقيمة للمفتاح. لذا، ستحاول تحويل الكائن من النوع [TaxAdminData] إلى النوع [string]. وعندها، سيتم استدعاء الطريقة [TaxAdminData->__toString] ضمناً. وتنتج هذه الطريقة السلسلة jSON للكائن [TaxAdminData]؛
  • السطر 84: المفتاح [taxAdminData] موجود في الذاكرة [redis]، لذا يتم استرداد قيمته. نعلم أن هذه هي السلسلة jSON لكائن [TaxAdminData]. ثم نقوم بفك تشفيرها للحصول على مصفوفة من السمات؛
  • السطر 85: انطلاقًا من هذا المصفوف، يتم إنشاء مثيل جديد للكائن [TaxAdminData]؛
  • السطر 87: يتم إنشاء مثيل للطبقة [dao]؛

20.6. كود العميل

Image

الإصدار 10 من العميل مطابق للإصدار 9. التغيير الوحيد هو ملف التكوين [config-client.json]:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/TaxPayerData.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Utilities/Utilitaires.php",
        "/../version-08/Dao/InterfaceClientDao.php",
        "/../version-08/Dao/TraitDao.php",
        "/../version-09/Dao/ClientDao.php",
        "/../version-08/Métier/InterfaceClientMetier.php",
        "/../version-08/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-10/impots-server.php"
}

التغيير الوحيد هو في السطر 24، وهو URL الخاص بالخادم.

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

Image

النتيجة في وحدة التحكم هي كما يلي:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"[redis], Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée. [tcp:\/\/127.0.0.1:6379]"}
Terminé

20.7. اختبارات [Codeception] الخاصة بالعميل

Image

فئة الاختبار [ClientMetierTest] في الإصدار 10 مطابقة لتلك الموجودة في الإصدار 09 باستثناء واحد:


<?php

// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);

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

// تعريف الثوابت
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");



}
  • السطر 10: بيئة الاختبار هي بيئة العميل في الإصدار 10؛

قبل بدء الاختبارات، دعونا نحذف باستخدام عميل [redis-cli] المفتاح [taxAdminData] من ذاكرة الخادم [redis]:

Image

الآن، لنقم بتنفيذ الاختبار:

Image

الآن دعونا نفحص سجلات الخادم [logs.txt]:


05/07/19 08:52:16:396 :
---nouvelle requête
05/07/19 08:52:16:403 : Autentification en cours…
05/07/19 08:52:16:403 : Authentification réussie [admin, admin]
05/07/19 08:52:16:403 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
05/07/19 08:52:16:407 : données fiscales prises en base de données
05/07/19 08:52:16:420 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:546 :
---nouvelle requête
05/07/19 08:52:16:555 : Autentification en cours…
05/07/19 08:52:16:555 : Authentification réussie [admin, admin]
05/07/19 08:52:16:556 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
05/07/19 08:52:16:559 : données fiscales prises dans redis
05/07/19 08:52:16:559 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
05/07/19 08:52:16:668 :
---nouvelle requête
05/07/19 08:52:16:675 : Autentification en cours…
05/07/19 08:52:16:675 : Authentification réussie [admin, admin]
05/07/19 08:52:16:675 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
05/07/19 08:52:16:678 : données fiscales prises dans redis
05/07/19 08:52:16:678 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:776 :
---nouvelle requête

سبق أن ذكرنا أنه في كل اختبار، يتم إعادة تشغيل منشئ فئة الاختبار، مما يعني أن الفئة [ClientDao] التي يتم اختبارها يتم إنشاء مثيل لها في كل اختبار باستخدام ملف تعريف ارتباط جلسة غير موجود. وبالتالي، يبدو الأمر كما لو أن الاختبارات الـ 11 تمثل 11 مستخدمًا مختلفًا، مع 11 جلسة مختلفة.

  • السطر 6: يتم أخذ البيانات الضريبية من قاعدة البيانات؛
  • السطران 13 و20: يتم استخراج البيانات الضريبية من الذاكرة [redis]. وبذلك، فإننا أمام ذاكرة ذات نطاق [application] مشتركة بين جميع مستخدمي التطبيق؛

20.8. واجهة الويب لخادم [Redis]

لقد رأينا أن الخادم [Redis] يمكن إدارته في وضع الأوامر. كما يمكن إدارته أيضًا من خلال واجهة ويب:

Image

  • في [4]، واجهة الإدارة URL؛
  • في [5]، المفاتيح المخزنة لدى الخادم؛
  • في [6]، الحالة الحالية للخادم؛

بالنقر على [5]، يمكن الحصول على معلومات حول المفتاح [taxAdminData]:

Image

  • في [7]، يظهر URL الذي يتيح الوصول إلى معلومات المفتاح [taxAdminData] و[8]؛
  • إلى [9]، وهو حالة المفتاح؛
  • في [10]، قيمته: يمكن التعرف على السلسلة jSON الخاصة بكائن من النوع [TaxAdminData]؛
  • في [11]، يمكن حذف المفتاح؛
  • في [12]، يمكن إضافة مفتاح آخر؛