Skip to content

19. تمرین عملی – نسخه ۹

در این نسخه، سرور را به شرح زیر بهبود می‌دهیم:

  • در حال حاضر، با هر درخواست، داده‌های سازمان مالیاتی از پایگاه داده بازیابی می‌شود. ما از یک جلسه استفاده خواهیم کرد:
    • وقتی کاربر اولین درخواست خود را ارسال می‌کند، داده‌های سازمان مالیاتی از پایگاه داده بازیابی شده و در جلسه (session) ذخیره می‌شود؛
    • برای درخواست‌های بعدی از همان کاربر، داده‌های سازمان مالیاتی از جلسه بازیابی می‌شود. می‌توان انتظار داشت که با توجه به منابع‌بر بودن پرس‌وجوهای پایگاه داده، بهبود جزئی در زمان اجرا رخ دهد؛
  • سرور رویدادهای کلیدی زیر را در یک فایل متنی ثبت خواهد کرد:
    • موفقیت یا شکست احراز هویت؛
    • این‌که آیا پارامترهای ارسال‌شده توسط کلاینت معتبر هستند یا خیر؛
    • نتیجه محاسبه مالیات؛
    • سناریوهای مختلف خطا؛
  • در صورت وقوع خطای مرگبار، ایمیلی برای مدیر برنامه ارسال خواهد شد؛

کلاینت نیز باید اصلاح شود تا کوکی جلسه را که برایش ارسال می‌شود، مدیریت کند.

19.1. سرور

ما بر روی سمت سرور برنامه تمرکز می‌کنیم.

Image

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

Image

19.1.1. خدمات

Image

19.1.1.1. کلاس [Logger]

کلاس [Logger] برای نوشتن لاگ‌ها در یک فایل متنی استفاده خواهد شد:


<?php

namespace Application;

class Logger {
  // ویژگی
  private $resource;

  // سازنده
  public function __construct(string $logsFilename) {
    // باز کردن فایل
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  //نوشتن یک پیام در لاگ‌ها
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }

  //بستن فایل لاگ
  public function close() {
    fclose($this->resource);
  }

}

نظرات

  • خط ۷: منبع فایل لاگ؛
  • خط ۱۰: سازنده کلاس نام فایل لاگ را به‌عنوان پارامتر می‌پذیرد؛
  • خط ۱۲: فایل متنی در حالت الحاق (a+) باز می‌شود: فایل باز شده و محتویات آن حفظ می‌شود. داده‌های جدید پس از محتوای موجود نوشته می‌شوند؛
  • خطوط ۱۳–۱۵: اگر فایل باز نشود، یک استثنا پرتاب می‌شود؛
  • خطوط ۱۹–۲۱: متد [write] پیام [$message] را به همراه تاریخ و زمان در فایل لاگ می‌نویسد؛
  • خطوط 24–16: متد [close] برای بستن فایل لاگ استفاده می‌شود؛

توجه: برنامهٔ سرور ممکن است به طور همزمان به چندین مشتری خدمت‌رسانی کند. با این حال، برای همهٔ آن‌ها تنها یک فایل لاگ وجود دارد. بنابراین هنگام نوشتن در فایل، خطر دسترسی همزمان وجود دارد. از این رو، نوشتن‌ها باید همگام‌سازی شوند تا با هم مخلوط نشوند. برای رفع این مشکل، کلاس PHP از نیم‌صندوق‌های [https://www.php.net/manual/fr/book.sem.php] استفاده می‌کند. در اینجا به همگام‌سازی نوشتن نمی‌پردازیم، اما مهم است که از این موضوع آگاه باشید.

19.1.1.2. کلاس [SendAdminMail]

کلاس [SendAdminMail] این امکان را فراهم می‌کند که در صورت از کار افتادن برنامه، ایمیلی برای مدیر برنامه ارسال شود:


<?php

namespace Application;

class SendAdminMail {
  // ویژگی‌ها
  private $config;
  private $logger;

  // سازنده
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }

  public function send() {
    // ارسال $this->config['message'] به سرور SMTP $this->config['smtp-server'] روی پورت $infos[smt-port]
    // اگر $this->config['tls'] درست باشد، سرویس TLS استفاده خواهد شد
    //ایمیل به نمایندگی از $this->config['from'] ارسال می‌شود
    // به گیرنده $this->config['to']
    // پیام دارای موضوع $this->config['subject']
    //ایمیل شامل پیوست‌ها از $this->config['attachments'] است
    //نتیجهٔ متد
    try {
      // ایجاد پیام
      $message = (new \Swift_Message())
        //موضوع پیام
        ->setSubject($this->config["subject"])
        // فرستنده
        ->setFrom($this->config["from"])
        // گیرندگان با استفاده از یک فرهنگ لغت (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // متن پیام
        ->setBody($this->config["message"])
      ;
      // پیوست‌ها
      foreach ($this->config["attachments"] as $attachment) {
        // مسیر پیوست
        $fileName = __DIR__ . $attachment;
        //بررسی وجود فایل
        if (file_exists($fileName)) {
          // پیوست کردن سند به پیام
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // خطا
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // TLS پروتکل؟
      if ($this->config["tls"] === "TRUE") {
        // TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
          ->setUsername($this->config["user"])
          ->setPassword($this->config["password"]);
      } else {
        // خیر TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // مدیر ارسال
      $mailer = new \Swift_Mailer($transport);
      // پیام ارسال شد
      $mailer->send($message);
      // پایان
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // خطا
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }

}

نظرات

  • خط ۱۱: سازنده دو پارامتر می‌گیرد:
    • [$config]: یک آرایهٔ asociative حاوی تمام اطلاعات مورد نیاز برای ارسال ایمیل؛
    • [$logger]: یک لاگر که برای ثبت لحظات کلیدی در حین ارسال ایمیل استفاده می‌شود؛

مجموعهٔ asociative array شکل زیر را خواهد داشت:

1
2
3
4
5
6
7
8
9
{
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
}
  • خطوط 16–76: متد [send] برای ارسال ایمیل استفاده می‌شود. این کد در بخش مرتبط ارائه و توصیف شده است؛

19.1.2. لایه [dao]

Image

اسکریپت [ServeurDaoWithSession.php] به شرح زیر است:


<?php

// فضای نام
namespace Application;

// تعریف کلاس ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {

  // سازنده
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // ساده‌ترین مورد
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      //کنترل به کلاس والد منتقل می‌شود
      parent::__construct($databaseFilename);
    }
  }

}

توضیحات

  • خط ۷: کلاس [ServerDaoWithSession] در نسخه 09، کلاس [ServerDao] از نسخه 08 را گسترش می‌دهد. این به این دلیل است که کلاس [ServerDao] قادر به استفاده از پایگاه داده است. تنها کاری که باقی مانده، در نظر گرفتن حالتی است که داده‌های سازمان مالیاتی قبلاً بازیابی شده باشد:
  • خط ۱۰: سازنده اکنون دو پارامتر می‌گیرد:
    • [string $databaseFilename]: نام فایلی که حاوی اطلاعات مورد نیاز برای اتصال به پایگاه داده است، اگر داده‌های سازمان مالیاتی هنوز بازیابی نشده باشد؛ در غیر این صورت، NULL؛
    • [TaxAdminData $taxAdminData]: داده‌های سازمان مالیاتی در صورتی که قبلاً بازیابی شده باشد؛ در غیر این صورت، NULL؛

هنگامی که یک جلسه وب آغاز می‌شود، لایه [dao] با استفاده از یک شیء [$databaseFilename] که NULL نیست و یک شیء [taxAdminData] NULL ساخته خواهد شد. سپس داده‌های مرجع مالیاتی از پایگاه داده بازیابی شده و در جلسه ذخیره می‌شوند. در درخواست‌های بعدی در همان جلسه، لایه [dao] با استفاده از یک شیء [databaseFilename یک شیء NULL و یک شیء [taxAdminData] از جلسه ساخته خواهد شد، و نه یک شیء NULL. بنابراین هیچ پرس‌وجوی پایگاه‌داده وجود نخواهد داشت.

19.1.3. اسکریپت سرور

اسکریپت سرور [impots-server.php] توسط فایل زیر پیکربندی می‌شود: jSON [config-server.json]:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
    "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",
        "/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/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] به شرح زیر تکامل می‌یابد:


<?php

// پابندی سخت‌گیرانه به انواع اعلام‌شدهٔ پارامترهای تابع
declare (strict_types=1);

// فضای نام
namespace Application;

// مدیریت خطا از طریق PHP
ini_set("display_errors", "0");
//
// مسیر فایل پیکربندی
define("CONFIG_FILENAME", "Data/config-server.json");

//بازیابی پیکربندی
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

//وابستگی‌های لازم اسکریپت گنجانده شده‌اند
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
//وابستگی‌های مطلق (کتابخانه‌های شخص ثالث)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
//وابستگی‌های Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

// سشن
$session = new Session();
$session->start();

//آماده‌سازی پاسخ سرور JSON
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// ایجاد فایل لاگ
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // خطای داخلی سرور
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // تکمیل شد
  exit;
}

// اولین لاگ
$logger->write("\n---nouvelle requête\n");

// بازیابی درخواست فعلی
$request = Request::createFromGlobals();
//احراز هویت فقط در اولین بار لازم است
if (!$session->has("user")) {
  // لاگ
  $logger->write("Autentification en cours…\n");
  // احراز هویت

  }
  // آیا کاربر پیدا شد؟
  if (!$trouvé) {
    // یافت نشد – کد 401 HTTP_UNAUTHORIZED
    sendResponse(
      $response,
      ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
      Response::HTTP_UNAUTHORIZED,
      ["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
      $logger
    );
    // تکمیل شد
    exit;
  } else {
    // جلسه به‌روزرسانی می‌شود تا نشان دهد کاربر احراز هویت شده است
    $session->set("user", TRUE);
    // لاگ
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // لاگ
  $logger->write("Authentification prise en session…\n");
}
// ما یک کاربر معتبر داریم – پارامترهای دریافتی را بررسی می‌کنیم
$erreurs = [];
//باید سه پارامتر وجود داشته باشد: GET


//خطاها؟
if ($erreurs) {
//یک کد خطا ۴۰۰ (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");
}
// ما همه چیز لازم برای کار را داریم
//ایجاد لایه [dao]
if (!$session->has("taxAdminData")) {
  //داده‌ها از پایگاه داده بازیابی می‌شوند
  $logger->write("données fiscales prises en base de données\n");
  try {
    //ساخت لایه [dao]
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    //داده‌ها در جلسه بارگذاری می‌شوند
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    //خطا ثبت می‌شود
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // تکمیل شد
    exit;
  }
} else {
  //داده‌ها از جلسه بازیابی می‌شوند
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // لاگ‌ها
  $logger->write("données fiscales prises en session\n");
}
//ایجاد لایه [métier]
$métier = new ServerMetier($dao);
//محاسبه مالیات
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// پاسخ بازگردانده می‌شود
sendResponse($response, $result, Response::HTTP_OK, [], $logger);
//پایان
exit;

function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  //ارسال ایمیل به مدیر
  // SendAdminMail تمام استثناها را می‌گیرد و خود آن‌ها را ثبت می‌کند
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  //یک کد خطای 500 به کلاینت ارسال می‌شود
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}

//تابع ارسال پاسخ HTTP به کلاینت
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  //$response: پاسخ HTTP
  //$result: جدول نتایج
  // $statusCode: وضعیت پاسخ HTTP
  // $headers: سربرگ‌های HTTP برای درج در پاسخ
  // $logger: لاگ‌گیر برنامه
  //
  //وضعیت 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();
  }
}

نظرات

  • خطوط ۳۴–۳۵: یک جلسه آغاز می‌شود؛
  • خطوط ۳۸–۴۰: یک پاسخ jSON آماده می‌شود؛
  • خطوط ۴۲–۵۰: تلاشی برای ایجاد فایل لاگ انجام می‌شود. اگر خطایی رخ دهد، متد [doInternalServer] (خطوط ۱۳۲–۱۴۰) فراخوانی می‌شود؛
  • خط ۱۳۲: متد [doInternalServer] چهار پارامتر را می‌پذیرد:
    • [$message]: پیامی که باید ثبت شود. باید در UTF-8 رمزگذاری شود؛
    • [$response]: شیء [Response] که پاسخ سرور به کلاینت خود را در بر می‌گیرد؛
    • [$logger]: ابجکت [Logger] مورد استفاده برای تولید لاگ‌ها؛
    • [$infos]: اطلاعاتی که برای ارسال ایمیل به مدیر برنامه استفاده می‌شود؛
  • خطوط ۱۳۵–۱۳۷: ایمیلی برای مدیر برنامه ارسال می‌شود؛
  • خط ۱۳۹: پاسخ به کلاینت ارسال می‌شود:
    • $response: پاسخ HTTP;
    • $result: سرور رشته jSON را از آرایه [‘réponse’=>["erreur" => $message]] ارسال می‌کند؛
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR]، کد وضعیت 500؛
    • $headers: [], هیچ هدر HTTP برای افزودن به پاسخ وجود ندارد؛
    • $logger: لاگ‌گیر برنامه؛
  • خط ۵۸: به لطف راه‌اندازی جلسه، کلاینت تنها یک بار احراز هویت خواهد شد:
    • پس از احراز هویت مشتری، کلید [user] در جلسه ذخیره خواهد شد (خط ۷۸)؛
    • وقتی همان کلاینت درخواست بعدی را ارسال می‌کند، خط ۵۸ از احراز هویت جلوگیری می‌کند، زیرا دیگر نیازی به آن نیست؛
  • خط ۱۰۳: به لطف جلسه‌ای که برقرار شده است، ما فقط یک بار از پایگاه داده داده‌برداری خواهیم کرد:
    • در طول اولین درخواست، پرس‌وجوی پایگاه داده انجام می‌شود (خط ۱۰۸). داده‌های بازیابی‌شده سپس در جلسه (خط ۱۱۰) ذخیره شده و با کلید [taxAdminData] مرتبط می‌شوند؛
    • برای درخواست‌های بعدی، کلید [taxAdminData] در جلسه (خط ۱۰۳) یافت می‌شود و سپس داده‌های روی دیسک مستقیماً به لایه [dao] ارسال می‌شوند (خط ۱۱۹);
  • خطوط ۱۱۱–۱۱۶: جستجو برای داده‌های مالیاتی در پایگاه داده ممکن است ناموفق باشد. در این صورت، کد [500 Internal Server Error] به کلاینت ارسال می‌شود؛
  • خط ۱۱۳: پیام خطا برای استثنای درایور MySQL به صورت ISO 8859-1 رمزگذاری شده است. این پیام به UTF-8 تبدیل می‌شود تا به درستی ثبت شود؛
  • بقیه کد تقریباً با نسخه قبلی یکسان است؛
  • خطوط 143–164: تابع [sendResponse] تمام پاسخ‌ها را به کلاینت ارسال می‌کند؛
  • خطوط ۱۴۴–۱۴۸: توضیح پارامترها؛
  • خط 153: پاسخ همیشه رشته jSON از آرایه [‘résultat’=>qqChose] است؛
  • خط ۱۵۶: گاهی اوقات سربرگ‌های HTTP باید به پاسخ اضافه شوند. این مورد در خط ۷۱ اتفاق می‌افتد؛
  • خط 158: پاسخ ارسال می‌شود؛
  • خطوط 160–163: پاسخ ثبت می‌شود و لاگ‌گیر بسته می‌شود؛

19.1.4. آزمایش‌ها [Codeception]

Image

ما فقط لایه [dao] را آزمایش خواهیم کرد، زیرا تنها لایه‌ای است که تغییر کرده است.

کد تست [ServerDaoTest] به شرح زیر است:


<?php

// پابندی سخت به انواع اعلام‌شدهٔ پارامترهای تابع
declare (strict_types=1);

// فضای نام
namespace Application;

// تعریف ثابت‌ها
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// مسیر فایل پیکربندی
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");

// بازیابی پیکربندی
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// شامل وابستگی‌های مورد نیاز اسکریپت
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// وابستگی‌های مطلق (کتابخانه‌های شخص ثالث)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// test -----------------------------------------------------

class ServerDaoTest extends \Codeception\Test\Unit {
  //TaxAdminData
  private $taxAdminData;

  public function __construct() {
    // پدر
    parent::__construct();
    // بازیابی پیکربندی
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // ایجاد لایه [dao]
    $dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // آزمایش‌ها
  public function testTaxAdminData() {

  }

}
  • خطوط ۹–۲۴: ما یک محیط زمان اجرا (runtime) یکسان با محیط اسکریپت سرور [impots-server] ایجاد می‌کنیم؛
  • خط ۳۸: برای ساخت لایه [dao]، کلاس [ServerDaoWithSession] نمونه سازی می‌شود؛

نتایج آزمون به شرح زیر است:

Image

19.2. کلاینت

ما بر سمت کلاینت برنامه تمرکز داریم.

Image

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

Image

در نسخه جدید، تنها تغییرات عبارتند از:

  • فایل پیکربندی [config-client.json]؛
  • لایهٔ کلاینت [dao];

19.2.1. لایه [dao]

لایه [Dao] به شرح زیر تغییر کرده است:


<?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 {
    // کوکی جلسه؟
    if (!$this->sessionCookie) {
      // ایجاد مشتری HTTP
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // یک درخواست به سرور بدون کوکی جلسه ارسال می‌شود
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      //درخواستی به سرور با کوکی جلسه ارسال می‌شود
      //یک کلاینت ایجاد می‌شود: HTTP
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    //پاسخ بازیابی می‌شود
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // لاگ‌ها
    print "$json=json\n";
    //بازیابی وضعیت پاسخ
    $statusCode = $response->getStatusCode();
    //خطا؟
    if ($statusCode !== 200) {
      // یک خطا رخ داده است – یک استثنا پرتاب می‌شود
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, 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 $réponse;
  }

}

نظرات

تغییر در لایه [dao] اکنون شامل مدیریت یک جلسه است:

  • خط ۱۴: کوکی جلسه؛
  • خطوط ۲۵–۳۹: در اولین درخواست، این کوکی وجود ندارد؛ بنابراین درخواست به همراه جزئیات احراز هویت به سرور ارسال می‌شود (خط ۲۸)؛
  • خطوط ۴۰–۵۳: برای درخواست‌های بعدی، کوکی جلسه معمولاً موجود است. بنابراین جزئیات احراز هویت ارسال نمی‌شوند (خطوط ۴۲–۴۴)؛
  • خطوط ۶۹–۸۲: پاسخ سرور به اولین درخواست حاوی یک کوکی جلسه خواهد بود. ما آن را بازیابی می‌کنیم. این کد قبلاً در بخش مرتبط استفاده و توضیح داده شده است؛
  • خط ۷۸: کوکی جلسه بازیابی‌شده در ویژگی کلاس [$sessionCookie] ذخیره می‌شود؛

توجه: ما می‌توانستیم نسخه قدیمی لایه [dao] را حفظ کرده و با هر درخواست، احراز هویت را انجام دهیم، زیرا هزینه این کار ناچیز است. با این حال، به دلایل آموزشی، ما می‌خواستیم نشان دهیم که یک کلاینت HTTP چگونه می‌تواند یک جلسه را مدیریت کند.

19.2.2. فایل پیکربندی

فایل پیکربندی jSON به شرح زیر تغییر می‌کند:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
    "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-09/impots-server.php"
}

تنها URL در خط ۲۴ تغییر می‌کند.

19.3. برخی آزمون‌ها

19.3.1. آزمون ۱

ابتدا، کلاینت را در یک محیط عاری از خطا اجرا می‌کنیم. نتایج همچنان مانند نسخه‌های قبلی است. با این حال، در سمت سرور، اکنون یک فایل لاگ به نام [logs.txt] داریم:


04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête

  • خطوط ۳–۷: در طول اولین درخواست، احراز هویت انجام می‌شود و داده‌ها از پایگاه داده بازیابی می‌شوند؛
  • خطوط ۹–۱۴: در طول درخواست بعدی، احراز هویت بیشتری انجام نمی‌شود و داده‌ها از جلسه بازیابی می‌شوند. این کار برای درخواست‌های بعدی (از خط ۱۵ به بعد) تکرار می‌شود؛

19.3.2. آزمون ۲

اکنون پایگاه داده را آفلاین می‌کنیم: MySQL. در سمت کلاینت، خروجی کنسول زیر را دریافت می‌کنیم:


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

در سمت سرور، لاگ‌های زیر از [logs.txt] را داریم:


04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête

برای مشاهده ایمیل دریافتی توسط مدیر برنامه، از اسکریپت [imap-03.php] در بخشی که به فایل پیکربندی زیر [config-imap-01.json] لینک شده است، استفاده می‌کنیم:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

این کار نتیجه زیر را تولید می‌کند:

Image

فایل [message_1.txt] حاوی متن زیر است:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.

19.3.3. آزمون ۳

حالا بیایید مطمئن شویم که فایل [logs.txt] ایجاد نشود. برای این کار، کافی است یک پوشه به نام [logs.txt] بسازید:

Image

پس از انجام این کار، بیایید کلاینت را اجرا کنیم.

در سمت کلاینت، خروجی کنسول زیر را دریافت می‌کنیم:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé

در سمت سرور، هیچ لاگی وجود ندارد، اما مدیر ایمیل زیر را دریافت می‌کند:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable

Echec lors de la création du fichier de logs [Data/logs.txt]

19.3.4. آزمون ۴

این بار، بیایید در فایل پیکربندی کلاینت، اعتبارنامه‌های نادرستی را برای کلاینت متصل‌شونده مشخص کنیم.

کلاینت خروجی کنسول زیر را نمایش می‌دهد:


L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé

در سمت سرور، لاگ‌های زیر ظاهر می‌شوند:


---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}

19.3.5. آزمون ۵

بیایید کاربر صحیح، [admin, admin را دوباره در فایل پیکربندی کلاینت قرار دهیم.

اکنون بیایید URL و [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] را مستقیماً از سرور در یک مرورگر بدون ارسال هیچ پارامترهایی درخواست کنیم:

در فایل لاگ سرور [logs.txt]، خطوط زیر را مشاهده می‌کنیم:


---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}

19.4. تست‌های [Codeception]

مانند نسخه‌های قبلی، ما تست‌های [Codeception] را برای نسخه 09 خواهیم نوشت.

Image

19.4.0.1. آزمایش لایه [métier]

آزمون [ClientMetierTest.php] به شرح زیر است:


<?php

// پابندی سخت به انواع اعلام‌شدهٔ پارامترهای تابع
declare (strict_types=1);

// فضای نام
namespace Application;

// تعریف ثابت‌ها
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09");

// مسیر فایل پیکربندی
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");

// بازیابی پیکربندی
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// شامل وابستگی‌های مورد نیاز اسکریپت
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
//وابستگی‌های مطلق (کتابخانه‌های شخص ثالث)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// استفاده می‌کند
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;

// کلاس تست
class ClientMetierTest extends Unit {
  
}

نظرات

  • در مقایسه با کلاس تست برای نسخه 08، تنها تغییر در خط 10 است که دایرکتوری ریشه کلاینت مورد آزمایش را مشخص می‌کند؛

نتایج آزمون به شرح زیر است:

Image

شایان ذکر است که لاگ‌های سرور [logs.txt] را بررسی کنید:


04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête

---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}

می‌توانیم ببینیم که داده‌های مرجع مالیاتی همیشه از پایگاه داده بازیابی می‌شوند و هرگز از جلسه (session) نیستند. بیایید به کد تست اجرا شده بازگردیم:


<?php

// پابندی سخت به انواع اعلام‌شده پارامترهای تابع
declare (strict_types=1);

// فضای نام
namespace Application;



// کلاس تست
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);
  }

  // آزمایش
  public function test1() {

  }

  public function test2() {

  }

  public function test3() {

  }



}

در کلاس تست [Codeception]، سازنده برای هر تست فراخوانی می‌شود.

  • خط ۲۱: بنابراین برای هر تست یک [ClientDao] جدید با یک کوکی جلسه NULL ایجاد می‌شود. این توضیح می‌دهد که چرا این کلاینت از هیچ جلسه‌ای بهره‌مند نمی‌شود؛

این مثال به ما نشان می‌دهد که جلسه (session) مکان مناسبی برای ذخیره‌سازی داده‌های اداره مالیات نیست. در واقع، این داده‌ها توسط همه کاربران برنامه مشترک است. با این حال، در اینجا در هر یک از جلسات آن‌ها کپی می‌شود.

در برنامه‌نویسی وب، سه نوع دید برای داده‌های مشترک وجود دارد:

  • داده‌های مشترک بین همه کاربران برنامه وب. این داده‌ها عموماً فقط-خواندنی هستند. PHP به طور بومی از این نوع ذخیره‌سازی پشتیبانی نمی‌کند؛
  • داده‌های مشترک بین درخواست‌های یک کلاینت. این داده‌ها در جلسه (session) ذخیره می‌شوند. از اصطلاح «جلسه کلاینت» برای اشاره به حافظه کلاینت استفاده می‌شود. تمام درخواست‌های یک کلاینت به این جلسه دسترسی دارند. آنها می‌توانند اطلاعات را در آن ذخیره و بخوانند. در اسکریپت‌های قبلی، این جلسه توسط شیء Symfony با شناسه [HttpFoundation\Session\Session] پیاده‌سازی شده است؛
  • دامنهٔ درخواست، یا زمینهٔ درخواست. درخواست یک کاربر ممکن است توسط چندین اقدام متوالی پردازش شود. زمینه درخواست به اکشن ۱ اجازه می‌دهد اطلاعات را به اکشن ۲ منتقل کند. در اسکریپت‌های قبلی، درخواست توسط شی Symfony با شناسه [HttpFoundation\Request] و زمینه آن توسط ویژگی [HttpFoundation\Request::attributes] پیاده‌سازی شده است؛

Image

کتابخانه‌های شخص ثالث در دسترس هستند تا حافظهٔ برنامه را به PHP ارائه دهند. نسخهٔ جدید تمرین عملی استفاده از یکی از این کتابخانه‌ها را نشان می‌دهد.