Skip to content

18. تمرین عملی – نسخهٔ ۸

ما اپلیکیشن نمونه – نسخه ۵ (بخش لینک) را برداشته و آن را به یک برنامه کلاینت/سرور تبدیل خواهیم کرد.

18.1. Introduction

معماری نسخهٔ ۵ به این صورت بود:

Image

  • لایه‌ای به نام [dao] (ابجکت‌های دسترسی به داده) ارتباط با پایگاه داده MySQL و سیستم فایل محلی را مدیریت می‌کند؛
  • لایه‌ای به نام [métier] محاسبه مالیات را انجام می‌دهد؛
  • اسکریپت اصلی به‌عنوان هماهنگ‌کننده عمل می‌کند: این اسکریپت لایه‌های [dao] و [métier] را ایجاد می‌کند، سپس برای انجام وظایف لازم با لایه [métier] ارتباط برقرار می‌کند؛

ما قصد داریم این معماری را به معماری کلاینت/سرور زیر مهاجرت دهیم:

Image

  • در [2]، ما لایه [dao] را از نسخه ۵ مجدداً استفاده خواهیم کرد و متدهای دسترسی به سیستم فایل محلی را حذف می‌کنیم. این متدها به لایه [dao] از کلاینت [6, 7] منتقل خواهند شد؛
  • در [3]، لایه [métier] همانند نسخه ۵ باقی می‌ماند، بدون متدهای [executeBatchImpôts, saveResults] آن که به لایه‌های [dao] و [7] کلاینت منتقل شده‌اند؛
  • در [4]، اسکریپت سرور باید نوشته شود: این اسکریپت باید:
    • لایه‌های [métier]، [dao] و [3, 2] را ایجاد کند؛
    • با اسکریپت کلاینت [5, 7] ارتباط برقرار کند؛
  • در [7]، لایهٔ کلاینت [dao] باید نوشته شود:
    • این یک کلاینت HTTP از اسکریپت سرور [4, 5] خواهد بود؛
    • این روش‌ها را از لایه [dao] نسخه ۵ برای دسترسی به سیستم فایل محلی اتخاذ خواهد کرد؛
  • در [8]، لایهٔ کلاینت [métier] با رابط [InterfaceMetier] از نسخهٔ ۵ مطابقت خواهد داشت. با این حال، پیاده‌سازی آن متفاوت خواهد بود. در نسخهٔ ۵، لایهٔ [métier] محاسبهٔ مالیات را انجام می‌داد. در اینجا، این لایهٔ [métier] سمت سرور است که این محاسبه را انجام می‌دهد. لایه [métier] بنابراین برای برقراری ارتباط با سرور و درخواست محاسبه مالیات، به لایه‌های [dao] و [7] فراخوانی خواهد کرد؛
  • در [9]، اسکریپت کنسول باید لایه‌های [dao, métier] مشتری را نمونه سازی کرده و اجرای آن را آغاز کند؛

18.2. سرور

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

Image

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

Image

18.2.1. اشیاء مبادله شده بین لایه‌ها

Image

موجودیت‌های مبادله‌شده بین لایه‌ها، آن‌هایی هستند که در نسخهٔ ۵ در بخش «لینک» توصیف شده‌اند.

18.2.2. لایه [dao]

Image

لایه [dao] رابط زیر [InterfaceServerDao] را پیاده‌سازی می‌کند:


<?php

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

interface InterfaceServerDao {

  // خواندن داده‌ها از سازمان مالیاتی
  public function getTaxAdminData(): TaxAdminData;
}
  • خط ۹: متد [getTaxAdminData] داده‌ها را از پایگاه داده اداره مالیات بازیابی می‌کند؛

رابط [InterfaceServerDao] توسط کلاس زیر [ServerDao] پیاده‌سازی شده است:


<?php

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

// تعریف کلاس ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
  // شیء از نوع TaxAdminData حاوی داده‌های رده مالیاتی
  private $taxAdminData;
  // شیء از نوع [Database] که شامل ویژگی‌های BD است
  private $database;

  // تولیدکننده
  public function __construct(string $databaseFilename) {
    //پیکربندی پایگاه داده JSON ذخیره می‌شود
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // ویژگی را آماده می‌کند
    $this->taxAdminData = new TaxAdminData();
    try {
      // اتصال به پایگاه داده را باز کنید
      $connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
      // ما می‌خواهیم هرگاه خطایی در SGBD رخ دهد، یک استثنا پرتاب شود
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // شروع یک تراکنش
      $connexion->beginTransaction();
      //جدول رده‌های مالیاتی پر می‌شود
      $this->getTranches($connexion);
      //جدول ثابت‌ها پر می‌شود
      $this->getConstantes($connexion);
      //تراکنش با موفقیت تکمیل شد
      $connexion->commit();
    } catch (\PDOException $ex) {
      //آیا تراکنشی در حال انجام است؟
      if (isset($connexion) && $connexion->inTransaction()) {
        // در صورت خطا، تراکنش را خاتمه دهید
        $connexion->rollBack();
      }
      // استثناء به کد فراخوانی منتقل می‌شود
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      //بستن اتصال
      $connexion = NULL;
    }
  }

  // خواندن داده‌ها از پایگاه داده
  private function getTranches($connexion): void {

  }

  // خواندن از جدول ثابت‌ها
  private function getConstantes($connexion): void {

  }

  // داده‌های مورد نیاز برای محاسبه مالیات را بازمی‌گرداند
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }

}

این کد در بخش «لینک» ارائه شده است.

18.2.3. لایه [métier]

Image

Image

لایه [métier] رابط زیر [InterfaceServerMetier] را پیاده‌سازی می‌کند:


<?php

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

interface InterfaceServerMetier {

  //محاسبه مالیات مودی
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}

رابط [InterfaceServerMetier] توسط کلاس زیر [ServerMetier] پیاده‌سازی شده است:


<?php

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

class ServerMetier implements InterfaceServerMetier {
  //لایه DAO
  private $dao;
  //داده‌های اداره مالیات
  private $taxAdminData;

  //---------------------------------------------
  // [dao] تنظیم‌کننده لایه
  public function setDao(InterfaceServerDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceServerDao $dao) {
    // یک مرجع در لایه [dao] ذخیره می‌شود
    $this->dao = $dao;
    // داده‌های مورد نیاز برای محاسبه مالیات را بازیابی می‌کند
    // روش [getTaxAdminData] ممکن است یک استثنا پرتاب کند ExceptionImpots
    //سپس اجازه داده می‌شود که به کد فراخواننده بازتاب داده شود
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

//محاسبه مالیات
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // نتیجه
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

// --------------------------------------------------------------------------
  private function calculerImpot2(string $marié, int $enfants, float $salaire): array {

    // نتیجه
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=حقوق سالانه-مزایا
  // مبلغ معافیت حداقل و حداکثری دارد
  private function getRevenuImposable(float $salaire): float {

    // نتیجه
    return floor($revenuImposable);
  }

// هرگونه کاهش را محاسبه می‌کند
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // نتیجه
    return ceil($décôte);
  }

//هرگونه کاهش را محاسبه می‌کند
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    ..
    // نتیجه
    return ceil($réduction);
  }
}

این کد قبلاً در نسخهٔ ۱ در بخش «لینک» مورد بحث قرار گرفته است. نسخهٔ شیءگرا آن با پایگاه داده در بخش «لینک» ارائه شده است.

18.2.4. اسکریپت سمت سرور

Image

Image

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


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
    "databaseFilename": "Data/database.json",
    "taxAdminDataFileName": "Data/taxadmindata.json",
    "relativeDependencies": [
        "/Entities/BaseEntity.php",
        "/Entities/ExceptionImpots.php",
        "/Entities/TaxAdminData.php",
        "/Entities/Database.php",
        "/Dao/InterfaceServerDao.php",
        "/Dao/ServerDao.php",
        "/Métier/InterfaceServerMetier.php",
        "/Métier/ServerMetier.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ]
}
  • خط ۱: دایرکتوری ریشه که مسیر فایل‌ها از آن اندازه‌گیری خواهد شد؛
  • خط ۲: فایل پیکربندی jSON برای پایگاه داده MySQL؛
  • خط ۳: فایل داده مرجع مالیاتی jSON;
  • خطوط ۵–۱۴: فایل‌های برنامه؛
  • خط ۱۵: وابستگی به کتابخانه‌های شخص ثالث، در این مورد Symfony؛
  • خطوط ۱۶–۲۰: جدول کاربران مجاز به استفاده از برنامه؛

فایل‌های jSON و [database.json, taxadmindata.json] مربوط به نسخهٔ ۵ هستند، همان‌طور که در بخش مرتبط توضیح داده شده است.

اسکریپت [impots-server] لایه [web] را به شرح زیر پیاده‌سازی می‌کند:


<?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";
}

// تعریف ثابت‌ها
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
//وابستگی‌های Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;

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

//بازیابی درخواست فعلی
$request = Request::createFromGlobals();
//احراز هویت
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
//آیا کاربر وجود دارد؟
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
  $trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
  $i++;
}
// تنظیم کد وضعیت پاسخ
if (!$trouvé) {
  // یافت نشد – کد 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
  // پیام خطا
  $response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
  $response->send();
  // پایان
  exit;
}
// کاربر معتبری داریم – پارامترهای دریافتی را بررسی می‌کنیم
$erreurs = [];
//سه پارامتر لازم است GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// خطا؟
if ($erreur) {
  $erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}

// بازیابی وضعیت تأهل
if (!$request->query->has("marié")) {
  $erreurs[] = "paramètre marié manquant";
} else {
  $marié = trim(strtolower($request->query->get("marié")));
  $erreur = $marié !== "oui" && $marié !== "non";
  //خطا؟
  if ($erreur) {
    $erreurs[] = "paramètre marié [$marié] invalide";
  }
}

// بازیابی تعداد فرزندان
if (!$request->query->has("enfants")) {
  $erreurs[] = "paramètre enfants manquant";
} else {
  $enfants = trim($request->query->get("enfants"));
  // تعداد فرزندان باید یک عدد صحیح ≥۰ باشد
  $erreur = !preg_match("/^\d+$/", $enfants);
  //خطا؟
  if ($erreur) {
    $erreurs[] = "paramètre enfants [$enfants] invalide";
  }
}

// استخراج حقوق سالانه
if (!$request->query->has("salaire")) {
  $erreurs[] = "paramètre salaire manquant";
} else {
  // حقوق باید یک عدد صحیح باشد که ≥ 0
  $salaire = trim($request->query->get("salaire"));
  $erreur = !preg_match("/^\d+$/", $salaire);
  //خطا؟
  if ($erreur) {
    $erreurs[] = "paramètre salaire [$salaire] invalide";
  }
}

// آیا پارامتر دیگری در پرس‌وجو وجود دارد؟
foreach (\array_keys($request->query->all()) as $key) {
  //آیا پارامتر معتبر است؟
  if (!\in_array($key, ["marié", "enfants", "salaire"])) {
    $erreurs[] = "paramètre [$key] invalide";}
}

//خطاها؟
if ($erreurs) {
  //یک کد خطای ۴۰۰ به کلاینت ارسال می‌شود
  $response->setStatusCode(Response::HTTP_BAD_REQUEST);
  $response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// ما همه چیز لازم برای ادامه را داریم
//ایجاد معماری سرور
$msgErreur = "";
try {
  //ایجاد لایه [dao]
  $dao = new ServerDao($config["databaseFilename"]);
  //ایجاد لایه [métier]
  $métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
//خطا ثبت شد
  $msgErreur = utf8_encode($ex->getMessage());
}
// خطا؟
if ($msgErreur) {
  // یک کد خطای 500 به کلاینت ارسال می‌شود
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  $response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
//محاسبه مالیات
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// بازگرداندن پاسخ
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();

نظرات

  • خط 16: فایل پیکربندی پردازش می‌شود؛
  • خطوط ۱۸–۲۶: تمام وابستگی‌ها بارگذاری می‌شوند؛
  • خط ۲۹: نام فایل [database.json]؛
  • خطوط ۳۲–۳۳: کلاس‌های کتابخانه‌های شخص ثالث که قرار است استفاده شوند، اعلام می‌شوند؛
  • خطوط ۳۶–۳۸: یک پاسخ jSON آماده می‌شود؛
  • خطوط ۴۰–۵۲: بررسی می‌کنیم که کاربر درخواست‌کننده واقعاً یکی از کاربران مجاز است؛
  • خطوط ۵۴–۶۳: اگر اینطور نباشد، کد HTTP 401 ارسال می‌شود که نشان‌دهنده رد دسترسی است. پس از دریافت این کد و سربرگ‌های HTTP و [WWW-Authenticate => Basic realm=]، اکثر مرورگرها یک پنجره احراز هویت نمایش می‌دهند و از کاربر می‌خواهند که وارد شود؛
  • خط ۵۹: پاسخ سرور jSON علت خطا را توضیح می‌دهد. تمام پاسخ‌های سرور رشته jSON از آرایه [‘réponse’=>’qq chose’] خواهند بود؛
  • خطوط ۶۴–۱۱۷: اعتبار درخواست بررسی می‌شود:
    • یک درخواست GET با دقیقاً سه پارامتر؛
    • یک پارامتر [marié] که مقدار آن باید 'yes' یا 'no' باشد؛
    • یک پارامتر [enfantsکه مقدار آن باید یک عدد صحیح ≥ 0 باشد؛
    • یک پارامتر [salaireکه مقدار آن باید یک عدد صحیح ≥ 0 باشد؛
  • خط ۶۵: هرگاه خطایی تشخیص داده شود، یک پیام خطا به آرایه [$erreurs] اضافه می‌شود؛
  • خطوط ۱۲۰–۱۲۶: اگر خطایی رخ دهد، کد HTTP [400 Bad Request] به کلاینت ارسال می‌شود (خط ۱۲۲);
  • خط ۱۲۳: پاسخ سرور jSON علت خطا را توضیح می‌دهد؛
  • از خط ۱۳۲ به بعد، همه چیز تأیید شده است. لایه‌های [dao, métier] را می‌توان نمونه‌سازی کرد. این نمونه‌سازی هزینه در بر دارد و تنها در صورتی باید انجام شود که از داشتن یک درخواست معتبر اطمینان داشته باشیم؛
  • خطوط ۱۳۰–۱۳۸: معماری سرور ایجاد می‌شود. ساخت لایه [dao] ممکن است یک استثنای [ExceptionImpots] را فعال کند. اگر این استثنا رخ دهد، خطا ثبت می‌شود؛
  • خطوط ۱۳۵–۱۳۸: اگر یک استثنا رخ داده باشد، کد HTTP 500 به کلاینت ارسال می‌شود. این کد نشان می‌دهد که سرور با خطایی مواجه شده است؛
  • خط 143: پاسخ علت خطا را توضیح می‌دهد؛
  • خط ۱۴۸ : محاسبه مالیات به لایه [métier] واگذار می‌شود؛
  • خطوط ۱۵۰–۱۵۱: پاسخ ارسال می‌شود؛

بیایید این اسکریپت را در یک مرورگر آزمایش کنیم. بیایید URL امن را از [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000] درخواست کنیم:

Image

  • در [1]، URL امن درخواست شده است؛
  • به [2]، سه پارامتر [marié, enfants, salaire]؛
  • در [3]، سرور آپاچی Laragon یک گواهی خودامضا SSL ارسال کرد. مرورگر این موضوع را تشخیص داد و یک هشدار امنیتی نمایش داد: این گواهی را نشانه‌ای از عدم اعتبار سایت سرور می‌داند؛
  • در [4]، ادامه می‌دهیم؛

Image

  • در [6]، ادامه می‌دهیم؛

Image

  • در [7]، مرورگر پنجره‌ای برای ورود کاربر نمایش می‌دهد؛
  • در [9,10]، وارد [admin] و [admin] شوید؛

Image

  • در [13]، پاسخ سرور jSON است؛

بیایید چند تست خطا را اجرا کنیم:

ما URL و [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x] را درخواست می‌کنیم

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

Image

ما SGBD و MySQL را حذف می‌کنیم و URL و [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000] را درخواست می‌کنیم:

Image

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

هر بار که نسخه جدیدی از سرور را می‌سازیم، لایه‌های [métier] و [dao] را آزمایش خواهیم کرد، همان‌طور که از نسخه 04 انجام شده است (به پیوندهای پاراگراف و پاراگراف مراجعه کنید).

ابتدا پروژه [scripts-web] را به تست‌های [Codeception] لینک می‌کنیم. برای این کار، همان روندی را که برای پروژه [scripts-console] در پاراگراف لینک توضیح داده شده است، دنبال کنید. این کار یک پروژه به نام [scripts-web] را در اختیار ما قرار می‌دهد که شامل یک پوشه به نام [Test Files] است:

Image

ما یک تست برای لایه [dao] و یک تست برای لایه [métier] ایجاد خواهیم کرد.

18.2.5.1. آزمایش‌ها برای لایه [dao]

Image

آزمون لایه [ServerDaoTest] به شرح زیر خواهد بود:


<?php

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

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

// تعریف ثابت‌ها
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// مسیر فایل پیکربندی
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 ServerDao(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

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

  }

}

نظرات

  • خطوط ۹–۲۴: ما همان محیط کاری سرور [impots-server.php] را راه‌اندازی می‌کنیم. این کار در خطوط ۹–۱۲ با تعریف دو ثابتی که محیط بر آن‌ها وابسته است، انجام می‌شود؛
  • خطوط ۳۲–۴۰: یک نمونه از لایه [dao] که قرار است آزمایش شود، ایجاد می‌شود، همان‌طور که در اسکریپت سرور [impots-server.php] انجام شد؛
  • از این نقطه به بعد، شرایط مانند اسکریپت سرور [impots-server.php] است: اکنون می‌توان تست‌ها را اجرا کرد؛
  • خطوط ۴۳–۴۵: متد [testTaxAdminData] همان متدی است که در بخش «لینک» توضیح داده شده است؛

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

Image

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

Image

آزمون [ServerMetierTest] به شرح زیر خواهد بود:


<?php

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

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

// تعریف ثابت‌ها
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// مسیر فایل پیکربندی
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";
}

// کلاس تست
class ServerMetierTest extends \Codeception\Test\Unit {
  // لایه کسب‌وکار
  private $métier;

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

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

  }

  public function test2() {

  }

  ..

  public function test11() {

  }

}

نظرات

  • خطوط ۹–۲۴: ما همان محیط کاری سرور [impots-server.php] را راه‌اندازی می‌کنیم. این کار در خطوط ۹–۱۲ با تعریف دو ثابتی که محیط بر آن‌ها وابسته است، انجام می‌شود؛
  • خطوط ۳۰–۳۸: یک نمونه از لایه [métier] که قرار است آزمایش شود، ایجاد می‌شود، همان‌طور که در اسکریپت سرور [impots-server.php] انجام شد؛
  • از این نقطه به بعد، شرایط همانند اسکریپت سرور [impots-server.php] است: اکنون می‌توان تست‌ها را اجرا کرد؛
  • خطوط ۴۰–۵۳: متدهای موجود در [test1, test2…, test11] همان‌هایی هستند که در بخش مرتبط توصیف شده‌اند؛

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

Image

18.3. کلاینت

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

Image

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

Image

18.3.1. اشیاء مبادله‌شده بین لایه‌ها

Image

موجودیت‌های فهرست‌شده در بالا همگی توصیف شده‌اند و هم‌اکنون در حال استفاده هستند:

18.3.2. لایه [dao]

Image

لایه [dao] رابط زیر را پیاده‌سازی می‌کند: [InterfaceClientDao]


<?php

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

interface InterfaceClientDao {

  // خواندن داده‌های مؤدی
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  //محاسبه مالیات مودی
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  //ثبت نتایج
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
  • خط ۹: تابع [getTaxPayersData] داده‌های مالیات‌دهنده را از فایل [$taxPayersFilename] به حافظه بارگذاری می‌کند. اگر خطایی رخ دهد، در فایل [$errorsFilename] ثبت می‌شود؛
  • خط ۱۲: تابع [calculerImpots] مالیات مودی را محاسبه می‌کند؛
  • خط ۱۵: تابع [saveResults] داده‌ها را از جدول [$taxPayersData] – که نمایانگر نتایج چندین محاسبه مالیاتی است – به فایل [$resultsFilename] ذخیره می‌کند؛

رابط [InterfaceClientDao] توسط کلاس زیر [ClientDao] پیاده‌سازی شده است:


<?php

namespace Application;

//وابستگی‌ها
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // استفاده از یک ویژگی
  use TraitDao;
  // ویژگی‌ها
  private $urlServer;
  private $user;

  // سازنده
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }

  //محاسبه مالیات
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // ایجاد مشتری 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
    ]]);
    //بازیابی پاسخ
    $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);
    }
    // پاسخ را بازمی‌گرداند
    return $réponse;
  }

}

توضیحات

  • خط ۱۰: [TraitDao] وارد شده است (به بخش «لینک» مراجعه کنید)، که متدهای [getTaxPayersData] و [saveResults] را پیاده‌سازی می‌کند. این امر تنها متد [calculerImpots] را برای پیاده‌سازی باقی می‌گذارد. این متد در خطوط 22–49 پیاده‌سازی شده است؛
  • خطوط 16–19: سازنده کلاس [ClientDao] دو پارامتر می‌گیرد:
    • URL و [$urlServer] از سرور محاسبه مالیات؛
    • آرایه [$user] شامل کلیدهای 'login' و 'passwd'، که کاربر درخواست‌کننده را تعریف می‌کند؛
  • خط ۲۲: متد [calculerImpots] سه پارامتر را که باید به سرور محاسبه مالیات ارسال شوند، دریافت می‌کند؛
  • خطوط ۲۴–۲۷: یک کلاینت HTTP با موارد زیر ایجاد می‌شود:
    • خط ۲۵: اعتبارنامه‌های کاربری که درخواست را ارسال می‌کند؛
    • خط ۲۶: گزینه‌ای که تضمین می‌کند کلاینت HTTP اعتبار گواهی SSL ارسال‌شده توسط سرور را بررسی نخواهد کرد؛
  • خطوط ۲۹–۳۴: سرور با سه پارامتر مورد انتظار خود پرس‌وجو می‌شود؛
  • خط ۳۶: پاسخ jSON از سرور بازیابی می‌شود. اگر پارامتر [false] به متد [Response::getContent] پاس داده نشود، در این صورت اگر وضعیت پاسخ سرور در بازه [3xx-5xx] (مورد خطا) قرار گیرد، شیء [Response] به محض اینکه تلاشی برای بازیابی محتوای پاسخ [Response::getContent] یا هدرهای آن HTTP و [Response::getHeaders] انجام شود، یک استثنا (exception) پرتاب می‌کند. در اینجا، صرف‌نظر از وضعیت پاسخ (HTTP)، می‌خواهیم بتوانیم به محتوای آن دسترسی داشته باشیم، حتی اگر فقط برای ثبت آن باشد (خط ۴۰)؛
  • خطوط ۳۷–۳۸: پاسخ سرور رشته jSON از یک آرایه [‘réponse’=>qqChose] است. ما [qqChose] را بازیابی می‌کنیم؛
  • خط ۴۰: پاسخ jSON در حالت توسعه ثبت می‌شود؛
  • خط ۴۲: کد وضعیت را از پاسخ بازیابی می‌کنیم؛
  • خطوط ۴۴–۴۹: اگر کد وضعیت HTTP برابر با ۲۰۰ نباشد، آنگاه سرور ما با مشکلی مواجه شده است. سپس یک استثنای [ExceptionImpots] را با پیامی که شامل پاسخ سرور jSON به همراه کد پاسخ HTTP است، پرتاب می‌کنیم؛
  • خط ۵۱: نتیجه را بازمی‌گردانیم که یک آرایهٔ asociative با کلیدهای [impôt, surcôte, décôte, réduction, taux] است؛

18.3.3. لایه [métier]

Image

Image

لایه [métier] [8] رابط زیر را پیاده‌سازی می‌کند [InterfaceClientMetier]:


<?php

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

interface InterfaceClientMetier {

  //محاسبه مالیات‌های یک مؤدی
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  //محاسبه مالیات‌ها در حالت دسته‌ای
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • خط ۹: تابع [calculerImpots] مالیات را محاسبه می‌کند؛
  • خط ۱۲: تابع [executeBatchImpots] مالیات را برای مالیات‌دهندگانی که داده‌هایشان در فایل [$taxPayersFileName] موجود است محاسبه می‌کند، نتایج را در فایل [$resultsFileName] و هرگونه خطای پیش‌آمده را در فایل [$errorsFileName] می‌نویسد؛

رابط [InterfaceClientMetier] توسط کلاس زیر [ClientMetier] پیاده‌سازی شده است:


<?php

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

class ClientMetier implements InterfaceClientMetier {
  // ویژگی
  private $clientDao;

  // سازنده
  public function __construct(InterfaceClientDao $clientDao) {
    // مرجع در لایه [dao] ذخیره شده است
    $this->clientDao = $clientDao;
  }
  
  // محاسبه مالیات
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }

  //محاسبه مالیات در حالت دسته‌ای
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    //اجازه می‌دهد استثناهای ناشی از لایه [dao] منتقل شوند
    //بازیابی داده‌های مالیات‌دهنده
    $taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // جدول نتایج
    $results = [];
    // پردازش نتایج
    foreach ($taxPayersData as $taxPayerData) {
      // مالیات را محاسبه می‌کند
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // فرم را تکمیل کنید [$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // ما نتیجه را در جدول نتایج وارد می‌کنیم
      $results [] = $taxPayerData;
    }
    //ذخیره نتایج
    $this->clientDao->saveResults($resultsFileName, $results);
  }

}

نظرات

  • خطوط ۱۱–۱۴: سازنده کلاس [ClientMetier] یک ارجاع به لایه [dao] را به‌عنوان پارامتر می‌پذیرد؛
  • خطوط 17–19: محاسبه مالیات به لایه [dao] واگذار شده است؛
  • خطوط ۲۰–۳۸: تابع [executeBatchImpots] در بخش «link» توصیف شده است؛

18.3.4. اسکریپت اصلی

Image

Image

اسکریپت کلاینت [MainImpotsClient.php] لایه‌های [console] و [9] را پیاده‌سازی می‌کند. این اسکریپت توسط فایل‌های زیر پیکربندی می‌شود: jSON و [conf-client.json]:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "Entities/BaseEntity.php",
        "Entities/TaxPayerData.php",
        "Entities/ExceptionImpots.php",
        "Utilities/Utilitaires.php",
        "Dao/InterfaceClientDao.php",
        "Dao/TraitDao.php",
        "Dao/ClientDao.php",
        "Métier/InterfaceClientMetier.php",
        "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-08/impots-server.php"
}
  • خط ۱: پوشه ریشه مشتری؛
  • خط ۲: فایل jSON حاوی داده‌های مالیات‌دهنده؛
  • خط ۳: فایل jSON حاوی نتایج؛
  • خط ۴: فایل jSON حاوی خطاها؛
  • خطوط ۶–۱۹: وابستگی‌های مختلف پروژهٔ کلاینت؛
  • خطوط ۲۰–۲۳: ارسال درخواست‌ها توسط کاربر به سرور محاسبه مالیات؛
  • خط ۲۴: فایل امن URL از سرور محاسبه مالیات؛

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


<?php

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

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

// مدیریت خطا توسط PHP
//ini_set("display_errors", "0");
//
//مسیر فایل پیکربندی
define("CONFIG_FILENAME", "../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";
}

// تعریف‌های ثابت
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
//وابستگی‌های Symfony
use Symfony\Component\HttpClient\HttpClient;

//ایجاد لایه [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// ایجاد لایه [métier]
$clientMetier = new ClientMetier($clientDao);

//محاسبه مالیات در حالت دسته‌ای
try {
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
  //نمایش خطا
  print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// پایان
print "Terminé\n";
exit;

توضیحات

  • خط ۱۳: مسیر فایل پیکربندی؛
  • خط ۱۶: پردازش فایل پیکربندی؛
  • خطوط ۱۸–۲۶: بارگذاری وابستگی‌ها؛
  • خط ۳۷: ایجاد لایه [dao]. ما دو مورد اطلاعات مورد نیاز سازنده لایه را ارسال می‌کنیم:
    • URL از سرور محاسبه مالیات؛
    • اطلاعات احراز هویت کاربری که درخواست‌ها را ارسال خواهد کرد؛
  • خط ۳۹: ایجاد لایه [métier]. ما یک مرجع به لایه [dao] که به تازگی ایجاد شده است را به سازنده لایه می‌دهیم؛
  • خط ۴۳: به لایه [métier] دستور داده می‌شود که:
    • محاسبه مالیات‌ها برای همه مودیان در فایل $config["taxPayerDataFileName"];
    • نتایج را در فایل $config["resultsFileName"] بنویسد؛
    • خطاها را در فایل $config["errorsFileName"] بنویسد؛
  • خط ۴۳ ممکن است استثناءها را پرتاب کند؛
  • خط ۴۶: پیام خطای استثنا را نمایش می‌دهد؛

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

  • [Data/taxpayersdata.json]: داده‌های مودیانی که مبلغ مالیات برای آن‌ها محاسبه شده است؛
  • [Data/results.json]: نتایج مربوط به مالیات‌دهندگان مختلف در فایل [Data/taxpayersdata.json]؛
  • [Data/errors.json]: هرگونه خطایی که ممکن است هنگام پردازش فایل [Data/taxpayersdata.json] رخ داده باشد؛

بیایید به سناریوهای خطای احتمالی نگاهی بیندازیم. اول از همه، بیایید سرور Laragon را متوقف کنیم. نتایج در کنسول مشتری به شرح زیر است:


Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé

حالا فقط سرور آپاچی را راه‌اندازی کنیم و نه SGBD یا MySQL:

Image

نتایج در کنسول کلاینت به شرح زیر است:


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é

حالا، بیایید MySQL را اجرا کنیم و سپس کاربر وارد شده در [config-client] را تغییر دهیم:

1
2
3
4
    "user": {
        "login": "x",
        "passwd": "x"
},

نتایج در کنسول کلاینت به شرح زیر است:


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

18.3.5. [Codeception] تست‌ها

همان‌طور که برای نسخه‌های قبلی انجام دادیم، قصد داریم تست‌های [Codeception] را برای نسخه 08 بنویسیم.

Image

18.3.5.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-08");

// مسیر فایل پیکربندی
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";
}
//
// کلاس تست
class ClientMetierTest extends \Codeception\Test\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 test11() {

  }

}

توضیحات

  • خطوط ۱۰–۲۶: تعریف محیط آزمایش. ما از همان محیطی استفاده می‌کنیم که در اسکریپت اصلی [MainImpotsClient] که در بخش مرتبط‌شده توصیف شده است، به کار رفته است؛
  • خطوط ۳۳–۴۱: ساخت لایه‌های [dao] و [métier];
  • خط ۴۰: ویژگی [$this→métier] به لایه [métier] ارجاع می‌دهد؛
  • خطوط 44–51: متدهای [test1, test2…, test11] همان‌هایی هستند که در بخش «لینک» توصیف شده‌اند؛

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

Image