20. تمرین کاربردی – نسخه ۱۰
نسخه قبلی نشان داد که دادههای مالیاتی، که توسط همه کاربران برنامه به اشتراک گذاشته میشوند، باید در یک حافظه محدود [Application] ذخیره شوند. ما برای پیادهسازی این کار از یک سرور Redis [https://redis.io] استفاده خواهیم کرد.
20.1. Redis
استخر حافظه [Application] توسط یک سرور Redis پیادهسازی خواهد شد. اسکریپتهای PHP که به این حافظه برنامه نیاز دارند، بهعنوان کلاینتهای این سرور عمل خواهند کرد:

20.2. نصب ردیس
Laragon همراه با یک سرور Redis عرضه میشود که بهصورت پیشفرض فعال نیست. بنابراین باید با فعالسازی آن شروع کنید:

- در [3]، سرور [Redis] را فعال کنید؛
- در [4]، پورت [6379] را که کلاینتهای Redis بهطور پیشفرض از آن استفاده میکنند، بدون تغییر باقی بگذارید؛
خدمات Laragon پس از فعالسازی Redis بهطور خودکار مجدداً راهاندازی میشوند:

20.3. کلاینت ردیس در حالت فرمان
سرور Redis را میتوان در حالت فرمان پرسوجو کرد. یک ترمینال Laragon را باز کنید (به بخش «لینک» مراجعه کنید):

- در [1]، دستور [redis-cli] کلاینت را در حالت فرمان برای سرور Redis راهاندازی میکند؛
از ژوئیه ۲۰۱۹، کلاینت ردیس میتواند از ۱۷۲ دستور برای تعامل با سرور [https://redis.io/commands#list] استفاده کند. یکی از این دستورات، [command count] [2]، این عدد [3] را نمایش میدهد.
ما فقط آنهایی را پوشش میدهیم که در برنامه کاربردی خود PHP به آنها نیاز خواهیم داشت. ما از Redis فقط برای یک هدف استفاده خواهیم کرد: ذخیره یک آرایه [‘attribut’=>’valeur’] در حافظه Redis. این کار با استفاده از دستور Redis به صورت [set attribut valeur] [4] انجام میشود. سپس میتوان مقدار را با استفاده از دستور [get attribut] [5] بازیابی کرد. این تمام چیزی است که نیاز خواهیم داشت.
ممکن است لازم باشد حافظه ردیس را پاک کنید. این کار با استفاده از دستور [flushdb] [6] انجام میشود. سپس، اگر مقدار ویژگی [titre] [7] را استعلام کنیم، یک مرجع [nil] [8] دریافت میکنیم که نشان میدهد ویژگی پیدا نشده است. همچنین میتوانید از دستور [exists] [9-10] برای بررسی وجود یک ویژگی استفاده کنید.
برای خروج از کلاینت ردیس، دستور [quit] [11] را تایپ کنید.
20.4. نصب کلاینت ردیس برای PHP
اکنون باید یک کلاینت ردیس برای PHP نصب کنیم:

چندین کتابخانه وجود دارند که یک کلاینت ردیس را پیادهسازی میکنند. ما از کتابخانه [Predis] [https://github.com/nrk/predis] (ژوئیه ۲۰۱۹) استفاده خواهیم کرد. مانند موارد قبلی، این کتابخانه نیز با استفاده از [composer] در یک ترمینال Laragon نصب میشود:

20.5. کد سرور

فایل پیکربندی [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"
}
توضیحات
- خطوط ۵–۱۵: نسخه ۱۰ به جز اسکریپت [impots-server.php] چیز جدیدی معرفی نمیکند. این نسخه از عناصر نسخههای ۰۸ و ۰۹ استفاده میکند؛
- خط ۱۹: وابستگی مورد نیاز برای کتابخانه [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");
}
// ما همه چیز لازم برای کار را داریم
//ردیس
\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();
// یک کد خطای ۵۰۰ به کلاینت ارسال میشود
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();
}
}
توضیحات
- خط ۱۵: نام مستعار [ServerDaoWithRedis] به کلاس [\Application\ServerDaoWithSession] اختصاص داده شده است تا تغییر در پیادهسازی اسکریپت سرور را منعکس کند؛
- خطوط ۱۸–۱۹: جلسه حفظ میشود. دو نکته اطلاعاتی در اینجا قابل توجه است:
- اینکه کاربر با موفقیت احراز هویت شده است. این اطلاعات دارای دامنه [session] است: به کاربر مشخصی مرتبط است و شامل سایر کاربران نمیشود؛
- دادههای مرجع مالیاتی. این اطلاعات دارای دامنه [application] است: این اطلاعات به کاربر خاصی مرتبط نیست، بلکه برای همه کاربران اعمال میشود؛
- خطوط 54–64: ایجاد کلاینت [redis]، که با سرور [redis] ارتباط برقرار خواهد کرد. این کلاینت با پورت پیشفرض سرور ارتباط برقرار خواهد کرد. اگر سرور روی پورت پیشفرض خود ارتباط برقرار نمیکرد یا روی ماشین [localhost] قرار نداشت، این اطلاعات باید به سازنده کلاس [\Predis\Client] ارسال میشد؛
- خط ۵۹: کلاینت بلافاصله به سرور متصل میشود تا بررسی کند که آیا پاسخ میدهد یا خیر؛
- خطوط 60–65: اگر اتصال به سرور Redis ناموفق باشد، یک پاسخ خطا به کلاینت ارسال میشود و یک ایمیل برای مدیر برنامه ارسال میگردد؛
- خط ۶۷: از سرور [redis] برای کلید [taxAdminData] پرسوجو میشود. اگر یافت نشود، دادههای مالیاتی از پایگاه داده بازیابی میشوند (خط ۷۲);
- خط ۷۵: کلید [taxAdminData] در حافظه [redis] قرار میگیرد که با رشته jSON متغیر [$taxAdminData] مرتبط است، که یک شی از نوع QZXW2HTMLP005335Z استQX. متد [$redis→set] انتظار دارد یک رشتهٔ کاراکتری بهعنوان مقدار کلید دریافت کند. بنابراین تلاش خواهد کرد شیء از نوع [TaxAdminData] را به نوع [string] تبدیل کند. در این صورت، بهطور ضمنی، متد [TaxAdminData->__toString] فراخوانی خواهد شد. این متد رشته jSON را از شیء [TaxAdminData] تولید میکند؛
- خط ۸۴: کلید [taxAdminData] در حافظه [redis] قرار دارد، بنابراین مقدار آن بازیابی میشود. میدانیم که این رشته jSON متعلق به یک شیء [TaxAdminData] است. سپس آن را رمزگشایی میکنیم تا یک آرایه از ویژگیها به دست آوریم؛
- خط ۸۵: از این آرایه، یک شیء جدید [TaxAdminData] ایجاد میشود؛
- خط ۸۷: لایه [dao] نمونه برداری میشود؛
20.6. کد کلاینت

نسخه ۱۰ مشتری با نسخه ۹ یکسان است. تنها تغییر در فایل پیکربندی [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"
}
تنها تغییر در خط ۲۴ است، جایی که URL سرور بهروزرسانی شده است.
نتایج همانند نسخه 09 است. بیایید به سادگی یک سناریوی خطای جدید را آزمایش کنیم:

نتیجه در کنسول به شرح زیر است:
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]

کلاس تست [ClientMetierTest] در نسخه 10 با نسخه 09 یکسان است، با یک استثنا:
<?php
// پابندی سختگیرانه به انواع اعلامشدهٔ پارامترهای تابع
declare (strict_types=1);
// فضای نام
namespace Application;
// تعریف ثابتها
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");
…
}
- خط ۱۰: محیط تست، محیط کلاینت در نسخه ۱۰ است؛
قبل از شروع آزمایشها، بیایید با استفاده از کلاینت [redis-cli] کلید [taxAdminData] را از حافظه سرور [redis] حذف کنیم:

اکنون، بیایید تست را اجرا کنیم:

اکنون بیایید لاگهای سرور [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] که در حال آزمایش است، برای هر تست با یک کوکی جلسه (session cookie) ناموجود نمونهسازی (instantiated) میشود. بنابراین، گویی که ۱۱ تست، ۱۱ کاربر مختلف را با ۱۱ جلسه مختلف نمایندگی میکنند.
- خط ۶: دادههای مالیاتی از پایگاه داده بازیابی میشوند؛
- خطوط ۱۳ و ۲۰: دادههای مالیاتی از حافظه [redis] بازیابی میشوند. بنابراین این حافظه به وضوح یک حافظه با دامنه [application] است که توسط همه کاربران برنامه مشترکاً استفاده میشود؛
20.8. رابط وب سرور [Redis]
ما دیدیم که سرور [Redis] را میتوان در حالت فرمان مدیریت کرد. همچنین میتوان آن را از طریق رابط وب مدیریت کرد:

- در [4]، سرور اداری URL؛
- [5]، کلیدهای ذخیرهشده توسط سرور؛
- [6]، وضعیت فعلی سرور؛
کلیک روی [5] اطلاعاتی درباره کلید [taxAdminData] ارائه میدهد:

- در [7]، URL که دسترسی به اطلاعات مربوط به کلید [taxAdminData] [8] را فراهم میکند؛
- به [9]، وضعیت کلید؛
- در [10]، مقدار آن: رشته jSON به عنوان متعلق به یک شیء از نوع [TaxAdminData] شناخته میشود؛
- در [11]، کلید را میتوان حذف کرد؛
- در [12]، کلید دیگری میتواند اضافه شود؛