22. تمرین عملی – نسخه ۱۱
هنوز هم معمول است که سرویسهای وب پاسخ خود را بهصورت جریان XML ارسال کنند تا جریان jSON:
- جریان jSON مختصرتر است، اما برای درک آن به راهنما نیاز دارید؛
- جریان XML مفصلتر است اما خود مستند است. بلافاصله قابل درک است؛
ما در حال اصلاح نسخه ۱۱ نرمافزار مشتری/سرور هستیم تا سرور اکنون در پاسخ به مشتریان خود یک جریان XML ارسال کند:

22.1. سرور

این معماری توسط اسکریپتهای زیر پیادهسازی خواهد شد:

22.1.1. کلاس [Utilitaires]
ما از کلاس [Utilitaires] که از نسخه ۰۳ به کار رفته است، مجدداً استفاده میکنیم (به بخش «لینک» مراجعه کنید):
<?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] را اجرا میکنیم، در مرورگر کروم خروجی زیر را دریافت میکنیم:

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"
}
نظرات
- ریشه پروژه اکنون پوشه نسخه ۱۱ است؛
- خط ۱۶: کلاس جدید [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();
}
}
توضیحات
- خط ۱۲: مشخص کنید که پاسخ از نوع [application/xml] است؛
- خطوط ۲۹–۵۹: پاسخ سرور اکنون از نوع XML است؛
- خط ۴۱: ایجاد گره ریشه [<réponse></réponse>] از گراف XML;
- خط ۴۲: این گراف با گراف XML از جدول نتایج [$result] که باید به کلاینت ارسال شود، تکمیل میشود؛
- خط ۴۳: گراف XML به رشته XML برای ارسال به کلاینت تبدیل میشود؛
آزمایش
مستقیماً در مرورگر کروم، URL [http://localhost/php7/scripts-web/impots/version-11/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=60000] را وارد کنید. نتیجهٔ زیر، [1]، در مرورگر کروم نمایش داده میشود:

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] (خط ۱۳ بالا) برای در نظر گرفتن فرمت جدید پاسخ اصلاح میشود. [simpleXML] برای پردازش این پاسخ استفاده میشود:
<?php
namespace Application;
//وابستگیها
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// استفاده از یک Trait
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);
// لاگها
// print "$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) {
// cookie جلسه را بازیابی میکند
$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);
}
}
نظرات
- خطوط ۲۶–۲۷: پاسخ سرور خوانده میشود. این یک سند XML [<réponse>…</réponse>] است. یک شیء [SimpleXMLElement] از سند دریافتی XML ساخته میشود؛
- خطوط ۳۳–۳۷: در صورت بروز خطا، پیام استثنا به جای رشته جمعآوریشده XML، رشته jSON از پاسخ سرور خواهد بود. این به این دلیل است که رشته jSON مختصرتر است؛
- خط ۵۳: آرایه نتایج در دو مرحله بازگردانده میشود:
- شیء [$xml] از نوع [\SimpleXMLElement] به jSON تبدیل میشود؛
- رشته حاصل jSON به یک آرایهٔ asociative تبدیل میشود. این نتیجهٔ قابل بازگشت است؛
آزمایش
اگر کلاینت در محیط صحیح (پایگاه داده، احراز هویت، لاگها) اجرا شود، نتایج معمول به دست میآیند (فایلهای [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);
}
// آزمایشها
…
}
نتایج آزمون به شرح زیر است:
