22. تمرين تطبيقي – الإصدار 11
لا يزال من الشائع أن ترسل خدمات الويب استجاباتها في شكل تدفق XML بدلاً من تدفق jSON:
- تدفق jSON أخف وزنًا، لكنه يتطلب دليلًا لاستخدامه لفهمه؛
- أما تدفق XML فهو أكثر تفصيلاً ولكنه موثق ذاتيًا، ويمكن فهمه على الفور؛
نقوم بتعديل الإصدار 11 من العميل/الخادم بحيث يرسل الخادم الآن تدفقًا XML كرد على عملائه:

22.1. الخادم

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

22.1.1. الفئة [Utilitaires]
نستخدم الفئة [Utilitaires] المستخدمة منذ الإصدار 03 (انظر الفقرة "الرابط"):
<?php
// مساحة الأسماء
namespace Application;
// فئة من وظائف المساعدة
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
…
}
// من https://stackoverflow.com/questions/1397036/how-to-convert-array-to-simplexml
public static function getXmlForArrayOfAttributes(array $arrayOfAttributes,
\SimpleXmlElement &$node): void {
// يتم مسح سمات المصفوفة
foreach ($arrayOfAttributes as $attribute => $value) {
// هل السمة رقمية؟
if (is_numeric($attribute)) {
// حالة مؤشر المصفوفة (وكذلك حالات أخرى)
$attribute = 'i' . $attribute;
}
// هل $value مصفوفة؟
if (is_array($value)) {
// سنستكشف المصفوفة [$value] بدورها
// نضيف عقدة إلى الرسم البياني XML
$subnode = $node->addChild($attribute);
// استدعاء متكرر لاستكشاف المصفوفة [$value]
Utilitaires::getXmlForArrayOfAttributes($value, $subnode);
} else {
// يتم إضافة العقدة إلى الرسم البياني XML
$node->addChild("$attribute", htmlspecialchars("$value"));
}
}
}
}
تعليقات
- الأسطر 14-36: نقدم الدالة الثابتة [getXmlForArrayOfAttributes] التي تُرجع السلسلة XML من المصفوفة [arrayOfAttributes] التي تم تمريرها كمعلمة. المعلمة الثانية هي مرجع عقدة في الرسم البياني XML، من النوع [SimpleXmlElement]. بعد التنفيذ، تحتوي هذه العقدة على الرسم البياني XML من المصفوفة [arrayOfAttributes]؛
نكتب الاختبار التالي: [testXml.php]:

<?php
// التبعية
require __DIR__ . "/Utilitaires.php";
// جدول ترابطي
$array = ["nom" => "amédée", "prénom" => "sylvain", "âge" => 40,
"enfants" => [["nom" => "amédée", "prénom" => "béatrice", "âge" => 6],
["nom" => "amédée", "prénom" => "bertrand", "âge" => 4]]];
// xml
header("Content-Type: application/xml");
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><root></root>");
\Application\Utilitaires::getXmlForArrayOfAttributes($array, $node);
print $node->asXML();
عند تشغيل هذا البرنامج النصي [2]، نحصل على النتيجة التالية في متصفح Chrome:

22.1.2. البرنامج النصي الخاص بالخادم
يجب تعديل البرنامج النصي للخادم [impots-server.php] وكذلك ملف التكوين الخاص به [config-server.json]:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-11",
"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",
"/Utilities/Utilitaires.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"
}
تعليقات
- أصبح جذر المشروع الآن هو مجلد الإصدار 11؛
- السطر 16: يتم تضمين الفئة الجديدة [Utilitaires]؛
التعديلات على البرنامج النصي للخادم هي كما يلي:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
…
// إعداد استجابة الخادم JSON
$response = new Response();
$response->headers->set("content-type", "application/xml");
$response->setCharset("utf-8");
…
// إنشاء الطبقة [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,
…
}
// وظيفة إرسال الإجابة 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);
// النص الأساسي XML
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><réponse></réponse>");
Utilitaires::getXmlForArrayOfAttributes($result, $node);
$response->setContent($node->asXML());
// رؤوس الرسالة
$response->headers->add($headers);
// الإرسال
$response->send();
// السجل
if ($logger != NULL) {
// السجل في jSON
$log = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$logger->write("$log\n");
$logger->close();
}
// إغلاق الاتصال [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
تعليقات
- السطر 12: يُشار إلى أن الاستجابة من النوع [application/xml]؛
- الأسطر 29-59: أصبحت استجابة الخادم الآن من النوع XML؛
- السطر 41: إنشاء العقدة الجذرية [<réponse></réponse>] للرسم البياني XML؛
- السطر 42: تم استكمال هذا الرسم البياني بالرسم البياني XML من الجدول [$result] الخاص بالنتائج المراد إرسالها إلى العميل؛
- السطر 43: يتم تحويل المخطط XML إلى سلسلة XML لإرسالها إلى العميل؛
اختبار
مباشرةً في متصفح Chrome، نكتب URL [http://localhost/php7/scripts-web/impots/version-11/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=60000]. نحصل على النتيجة التالية [1] في متصفح Chrome:

22.2. العميل
ننتقل الآن إلى الجزء الخاص بالعميل من التطبيق.

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

في الإصدار الجديد، التغييرات الوحيدة هي:
- ملف التكوين [config-client.json]؛
- طبقة [dao] الخاصة بالعميل؛
يصبح ملف التكوين [config-client.json] كما يلي:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-11",
"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",
"/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-11/impots-server.php"
}
22.2.1. الطبقة [dao]
يتم تعديل العميل [ClientDao.php] (السطر 13 أعلاه) لمراعاة التنسيق الجديد للاستجابة. يتم استخدام [simpleXML] لمعالجة هذه الاستجابة:
<?php
namespace Application;
// التبعيات
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// استخدام سمة
use TraitDao;
// السمات
private $urlServer;
private $user;
private $sessionCookie;
// المنشئ
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// حساب الضريبة
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// استرداد الرد XML
$réponse = $response->getContent(false);
$xml = new \SimpleXMLElement($réponse);
// السجلات
// طباعة "$réponse\n";
// نحصل على حالة الرد
$statusCode = $response->getStatusCode();
// هل هناك خطأ؟
if ($statusCode !== 200) {
// هناك خطأ - يتم إلقاء استثناء
$message = \json_encode(["statut HTTP" => $statusCode, "réponse" => $xml], JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// استرداد ملف تعريف الارتباط الخاص بالجلسة
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// ملف تعريف ارتباط الجلسة؟
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// نقوم بإرجاع الرد في شكل مصفوفة
return \json_decode(\json_encode($xml, JSON_UNESCAPED_UNICODE), true);
}
}
تعليقات
- السطران 26-27: تتم قراءة استجابة الخادم. وهي عبارة عن مستند XML [<réponse>…</réponse>]. يتم إنشاء كائن [SimpleXMLElement] من المستند XML المستلم؛
- السطور 33-37: في حالة حدوث خطأ، ستكون رسالة الاستثناء هي السلسلة jSON من استجابة الخادم بدلاً من السلسلة XML التي تم جمعها. في الواقع، السلسلة jSON أكثر إيجازًا؛
- السطر 53: يتم عرض جدول النتائج على مرحلتين:
- يتم تحويل الكائن [$xml] من النوع [\SimpleXMLElement] إلى jSON؛
- يتم تحويل السلسلة jSON الناتجة إلى مصفوفة ترابطية. هذه هي النتيجة المطلوب عرضها؛
اختبار
إذا تم تشغيل العميل في بيئة صحيحة (قاعدة البيانات، المصادقة، السجلات)، فسنحصل على النتائج المعتادة (تحقق من الملفات [taxpayersdata.json, results.txt, errors.json]). أما على جانب الخادم، فإن السجلات هي كما يلي:
06/07/19 07:41:32:877 :
---nouvelle requête
06/07/19 07:41:32:882 : Autentification en cours…
06/07/19 07:41:32:883 : Authentification réussie [admin, admin]
06/07/19 07:41:32:883 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
06/07/19 07:41:32:908 : données fiscales prises en base de données
06/07/19 07:41:32:959 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
06/07/19 07:41:33:070 :
---nouvelle requête
06/07/19 07:41:33:077 : Authentification prise en session…
06/07/19 07:41:33:077 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
06/07/19 07:41:33:099 : données fiscales prises dans redis
06/07/19 07:41:33:100 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
06/07/19 07:41:33:189 :
---nouvelle requête
06/07/19 07:41:33:202 : Authentification prise en session…
06/07/19 07:41:33:202 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
06/07/19 07:41:33:233 : données fiscales prises dans redis
06/07/19 07:41:33:233 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
06/07/19 07:41:33:318 :
…
22.2.2. اختبارات [Codeception]

الاختبار [ClientMetierTest] هو التالي:
<?php
// الالتزام الصارم بأنواع المعلمات المعلنة للدوال
declare (strict_types=1);
// مساحة الأسماء
namespace Application;
// تعريف الثوابت
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-11");
// مسار ملف التكوين
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// يتم استرداد التكوين
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
…
// فئة الاختبار
class ClientMetierTest extends Unit {
// طبقة الأعمال
private $métier;
public function __construct() {
parent::__construct();
// استرداد التكوين
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// إنشاء الطبقة [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// إنشاء الطبقة [métier]
$this->métier = new ClientMetier($clientDao);
}
// اختبارات
…
}
نتائج الاختبار هي كما يلي:
