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

22.1. الخادم

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

22.1.1. فئة [Utilities]
نحن نعيد استخدام فئة [Utilities] المستخدمة منذ الإصدار 03 (انظر الفقرة المرتبطة):
<?php
// namespace
namespace Application;
// a class of utility functions
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
…
}
// from https://stackoverflow.com/questions/1397036/how-to-convert-array-to-simplexml
public static function getXmlForArrayOfAttributes(array $arrayOfAttributes,
\SimpleXmlElement &$node): void {
// scan array attributes
foreach ($arrayOfAttributes as $attribute => $value) {
// is the attribute numeric?
if (is_numeric($attribute)) {
// table index case (but also other cases)
$attribute = 'i' . $attribute;
}
// is $value an array?
if (is_array($value)) {
// we'll explore the [$value] array in turn
// we add a node to the XML graph
$subnode = $node->addChild($attribute);
// recursive call to explore the [$value] array
Utilitaires::getXmlForArrayOfAttributes($value, $subnode);
} else {
// we add the node to the XML graph
$node->addChild("$attribute", htmlspecialchars("$value"));
}
}
}
}
تعليقات
- الأسطر 14–36: نقدم الطريقة الثابتة [getXmlForArrayOfAttributes]، التي تُرجع سلسلة XML لمصفوفة [arrayOfAttributes] تم تمريرها كمعلمة. المعلمة الثانية هي مرجع إلى عقدة مخطط XML من النوع [SimpleXmlElement]. بعد التنفيذ، تحتوي هذه العقدة على شجرة XML لمصفوفة [arrayOfAttributes]؛
نكتب الاختبار التالي [testXml.php]:

<?php
// dependency
require __DIR__ . "/Utilitaires.php";
// associative table
$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: تم تضمين فئة [Utilities] الجديدة؛
التغييرات التي طرأت على البرنامج النصي للخادم هي كما يلي:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
…
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/xml");
$response->setCharset("utf-8");
…
// creation of the [business] layer
$métier = new ServerMetier($dao);
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// end
exit;
function doInternalServerError(string $message, Response $response, array $infos,
…
}
// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response : answer HTTP
// $result: results table
// $statusCode: HTTP response status
// $headers: HTTP headers to be included in the response
// $logger: application logger
// $predisClient: a customer [predis]
//
// status HTTTP
$response->setStatusCode($statusCode);
// body XML
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><réponse></réponse>");
Utilitaires::getXmlForArrayOfAttributes($result, $node);
$response->setContent($node->asXML());
// headers
$response->headers->add($headers);
// shipping
$response->send();
// log
if ($logger != NULL) {
// log in jSON
$log = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$logger->write("$log\n");
$logger->close();
}
// close connection [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
التعليقات
- السطر 12: يحدد أن الاستجابة من النوع [application/xml]؛
- الأسطر 29–59: أصبحت استجابة الخادم الآن بتنسيق XML؛
- السطر 41: إنشاء العقدة الجذرية [<response></response>] لمخطط XML؛
- السطر 42: يتم ملء هذه الشجرة بشجرة XML من مصفوفة [$result] للنتائج التي سيتم إرسالها إلى العميل؛
- السطر 43: يتم تحويل شجرة XML إلى سلسلة XML لإرسالها إلى العميل؛
اختبار
أدخل عنوان URL [http://localhost/php7/scripts-web/impots/version-11/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=60000] مباشرة في متصفح Chrome. يتم عرض النتيجة التالية [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;
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// using a Trait
use TraitDao;
// attributes
private $urlServer;
private $user;
private $sessionCookie;
// manufacturer
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// we retrieve the XML response
$réponse = $response->getContent(false);
$xml = new \SimpleXMLElement($réponse);
// logs
// print "$réponse";
// retrieve response status
$statusCode = $response->getStatusCode();
// mistake?
if ($statusCode !== 200) {
// we have an error - we throw an exception
$message = \json_encode(["statut HTTP" => $statusCode, "réponse" => $xml], JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// retrieve the session cookie
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// session cookie ?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// the answer is given in the form of a table
return \json_decode(\json_encode($xml, JSON_UNESCAPED_UNICODE), true);
}
}
تعليقات
- السطران 26-27: تتم قراءة استجابة الخادم. وهي عبارة عن مستند XML [<response>…</response>]. يتم إنشاء كائن [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
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-11");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
…
// test class
class ClientMetierTest extends Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$this->métier = new ClientMetier($clientDao);
}
// tests
…
}
نتائج الاختبار هي كما يلي:
