Skip to content

11. تمرین عملی – نسخهٔ ۴

برنامه محاسبه مالیات ساختار لایه‌ای زیر را پیاده‌سازی خواهد کرد:

Image

ما عناصر نسخه ۳ بخش «لینک» را برداشته و آن‌ها را برای سازگاری با معماری جدید برنامه تغییر می‌دهیم. این کار گاهی «بازسازی» (refactoring) نامیده می‌شود. در اینجا فرض می‌کنیم داده‌های مورد نیاز برنامه در فایل‌های متنی ذخیره شده‌اند. لایه [Dao] تعامل با این فایل‌ها را مدیریت خواهد کرد.

11.1. ساختار درخت اسکریپت

Image

11.2. ابجکت‌های مبادله‌شده بین لایه‌ها

ما برخی از اشیاء نسخه ۳ را حفظ خواهیم کرد. آنها را در اینجا به عنوان یادآوری فهرست می‌کنیم.

استثنا [ExceptionImpotsاستثنایی است که لایه [Dao] زمانی که با مشکلی در دسترسی به داده‌ها یا ماهیت داده‌ها (داده‌های نادرست) مواجه می‌شود، پرتاب خواهد کرد.


<?php

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

class ExceptionImpots extends \RuntimeException {

  public function __construct(string $message, int $code=0) {
    parent::__construct($message, $code);
  }
}

کلاس [Utilitaires] شامل متدهای مفیدی برای مدیریت فایل‌های متنی است (در این مورد، یک متد واحد):


<?php

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

// یک کلاس از توابع ابزار
abstract class Utilitaires {

  public static function cutNewLinechar(string $ligne): string {
    // علامت پایان خط از $ligne در صورت وجود حذف می‌شود
    $longueur = strlen($ligne);  // طول خط
    while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
      $ligne = substr($ligne, 0, $longueur - 1);
      $longueur--;
    }
    // end – خط را بازگردانید
    return($ligne);
  }
}

کلاس [TaxAdminData] داده‌های سازمان مالیاتی را در بر می‌گیرد:


<?php

namespace Application;

class TaxAdminData {
  // کروشه‌های مالیاتی
  private $limites;
  private $coeffR;
  private $coeffN;
  //ثابت‌های محاسبه مالیات
  private $plafondQfDemiPart;
  private $plafondRevenusCelibatairePourReduction;
  private $plafondRevenusCouplePourReduction;
  private $valeurReducDemiPart;
  private $plafondDecoteCelibataire;
  private $plafondDecoteCouple;
  private $plafondImpotCouplePourDecote;
  private $plafondImpotCelibatairePourDecote;
  private $abattementDixPourcentMax;
  private $abattementDixPourcentMin;

  // ابتدایی‌سازی
  public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
    //بازیابی محتویات فایل داده‌های مالیاتی
    $fileContents = \file_get_contents($taxAdminDataFilename);

    // شیء را بازمی‌گرداند
    return $this;
  }

  private function check($value): \stdClass {

    return $result;
  }

    //toString
  public function __toString() {
    // رشته JSON شیء
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

  // گیرنده و تنظیم‌کننده
  public function getLimites() {
    return $this->limites;
  }



  public function setLimites($limites) {
    $this->limites = $limites;
    return $this;
  }


}

ما یک کلاس جدید، [TaxPayerDataاضافه می‌کنیم که داده‌های نوشته شده در فایل نتایج را در بر می‌گیرد:


<?php

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

// کلاس داده
class TaxPayerData {
  // داده‌های مورد نیاز برای محاسبه مالیات مودی
  private $marié;
  private $enfants;
  private $salaire;
  // نتایج محاسبه مالیات
  private $montant;
  private $surcôte;
  private $décôte;
  private $réduction;
  private $taux;

  // مقداردهی‌کننده
  public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel) : TaxPayerData{
    // داده‌های مؤدی مورد نیاز برای محاسبه مالیات
    $this->marié = $marié;
    $this->enfants = $nbEnfants;
    $this->salaire = $salaireAnnuel;
    // شیء راه‌اندازی‌شده را بازمی‌گرداند
    return $this;
  }

  // گیرنده و تنظیم‌کننده
  public function getMarié() {
    return $this->marié;
  }



  public function setMarié($marié) {
    $this->marié = $marié;
    return $this;
  }



    //toString
  public function __toString() {
    // رشته JSON شیء
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

}

توجه: از تولید خودکار کد برای ساخت کانستراکتور، گترها و سترها استفاده کنید (به بخش مرتبط مراجعه کنید). توجه داشته باشید که سترها «fluent» هستند.

11.3. لایه [dao]

در اینجا ما در حال بررسی لایه [1] برنامه خود هستیم:

Image

11.3.1. رابط [InterfaceDao]

رابط لایه [dao] به شرح زیر خواهد بود: [InterfaceDao.php]:


<?php

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

interface InterfaceDao {

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

  // خواندن داده‌های مرجع مالیاتی (باندهای مالیاتی)
  public function getTaxAdminData(): TaxAdminData;

  // ذخیره نتایج
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

نظرات

  • مشخصات به شرح زیر است:
    • داده‌های مودیان در یک فایل متنی نگهداری می‌شود؛
    • نتایج محاسبه مالیات در یک فایل متنی ذخیره می‌شوند؛
    • هرگونه خطا در یک فایل متنی ذخیره می‌شود؛
    • مشخص نیست که داده‌های سازمان مالیاتی در چه قالبی در دسترس است. برای هر قالب جدید، رابط [InterfaceDao] باید توسط یک کلاس جدید پیاده‌سازی شود؛
    • متدهای رابط که هنگام دسترسی به داده‌ها با خطای مرگبار مواجه می‌شوند، باید یک استثنا از نوع [ExceptionImpots] پرتاب کنند؛
  • خط ۹: متدی که برای بازیابی داده‌های مالیات‌دهنده استفاده می‌شود، [statut marital, nombre denfants, salaire annuel];
    • پارامتر اول نام فایل متنی حاوی این داده‌ها است؛
    • پارامتر دوم نام فایل متنی است که در آن هرگونه خطاهای رخ‌داده ثبت می‌شوند؛
  • خط ۱۲: متدی که برای بازیابی داده‌ها از مراجع مالیاتی استفاده می‌شود. در اینجا هیچ پارامتری به آن ارسال نمی‌شود، زیرا نمی‌دانیم داده‌ها چگونه ذخیره شده‌اند؛
  • خط ۱۵: متدی که برای ذخیره نتایج محاسبه مالیات در یک فایل متنی استفاده می‌شود، که نام آن به‌عنوان پارامتر ارسال می‌شود؛

هنگام نوشتن رابط [InterfaceDao]، می‌دانیم که بسته به نحوه ذخیره‌سازی داده‌های سازمان مالیاتی، راه‌های مختلفی برای پیاده‌سازی متد [getTaxAdminData] وجود خواهد داشت. بنابراین، رابط [InterfaceDao] توسط کلاس‌های مختلفی پیاده‌سازی خواهد شد که هر یک یک فرمت ذخیره‌سازی خاص برای این داده‌ها (آرایه‌ها، فایل‌های متنی، پایگاه‌های داده، سرویس‌های وب) را مدیریت می‌کند. با این حال، این کلاس‌های مشتق، کد مشترک، یعنی پیاده‌سازی متدهای [getTaxPayersData, saveResults] را به اشتراک خواهند گذاشت. ما می‌دانیم که این مورد استفاده می‌تواند به دو روش پیاده‌سازی شود (به بخش «لینک» مراجعه کنید):

  1. یک کلاس انتزاعی C برای گروه‌بندی کد مشترک در میان کلاس‌های مشتق ایجاد می‌شود. کلاس C رابط I را پیاده‌سازی می‌کند، اما برخی متدها که باید در کلاس‌های مشتق اعلام شوند، در داخل کلاس C به صورت انتزاعی اعلام شده‌اند؛ در نتیجه، خود کلاس C نیز انتزاعی است. سپس کلاس‌های C1 و C2 را که از C مشتق شده‌اند، ایجاد می‌کنیم که هر یک روش‌های تعریف‌نشده (انتزاعی) کلاس والد خود C را به شیوهٔ خود پیاده‌سازی می‌کنند؛
  2. ما یک تریت T ایجاد می‌کنیم که تقریباً با کلاس انتزاعی C از راه‌حل قبلی یکسان است. این تریت رابط I را پیاده‌سازی نمی‌کند زیرا از نظر نحوی، نمی‌تواند این کار را انجام دهد. سپس کلاس‌های C1 و C2 را ایجاد می‌کنیم که رابط I را پیاده‌سازی می‌کنند و از تریت T استفاده می‌کنند. تنها کاری که برای این کلاس‌ها باقی می‌ماند این است که متدهای رابط I را که توسط تریت T که آن را وارد (import) کرده‌اند پیاده‌سازی نشده‌اند، پیاده‌سازی کنند؛

برای این مثال، در اینجا از یک تریت [TraitDao] استفاده خواهیم کرد.

11.3.2. ترایت [TraitDao]

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


<?php

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

trait TraitDao {

  // خواندن داده‌های مالیات‌دهنده
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
    // جدول داده‌های مودی مالیاتی
    $taxPayersData = [];
    // جدول خطا
    $errors = [];
    // هنگام مدیریت فایل‌ها خطاهای زیادی ممکن است رخ دهد
    try {
      //در حال خواندن داده‌های کاربر
      //هر سطر در قالب: وضعیت تأهل، تعداد فرزندان، حقوق سالانه
      $taxPayersFile = fopen($taxPayersFilename, "r");
      if (!$taxPayersFile) {
        throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
      }
      // ردیف فعلی فایل داده‌های کاربر پردازش می‌شود
      //که در قالب زیر است: وضعیت تأهل، تعداد فرزندان، حقوق سالانه
      $num = 1;         //شماره سطر فعلی
      $nbErreurs = 0;   // تعداد خطاهای رخ‌داده
      while ($ligne = fgets($taxPayersFile, 100)) {
        // رده‌های خالی نادیده گرفته می‌شوند
        $ligne = trim($ligne);
        if (strlen($ligne) == 0) {
          // خط بعدی
          $num++;
          // دوباره حلقه
          continue;
        }
        // حذف هر کاراکتر پایان خط
        $ligne = Utilitaires::cutNewLineChar($ligne);
        // استخراج سه فیلد 'married:children:salary' که $ligne را تشکیل می‌دهند
        list($marié, $enfants, $salaire) = explode(",", $ligne);
        // آنها را بررسی کنید
        //وضعیت تأهل باید 'بله' یا 'خیر' باشد
        $marié = trim(strtolower($marié));
        $erreur = ($marié !== "oui" and $marié !== "non");
        if (!$erreur) {
          //تعداد فرزندان باید یک عدد صحیح باشد
          $enfants = trim($enfants);
          if (!preg_match("/^\d+$/", $enfants)) {
            $erreur = TRUE;
          } else {
            $enfants = (int) $enfants;
          }
        }
        if (!$erreur) {
          // حقوق یک عدد صحیح است و شامل سنت یورو نمی‌شود
          $salaire = trim($salaire);
          if (!preg_match("/^\d+$/", $salaire)) {
            $erreur = TRUE;
          } else {
            $salaire = (int) $salaire;
          }
        }
        // خطا؟
        if ($erreur) {
          $errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
          $nbErreurs++;
        } else {
          // اطلاعات ذخیره شد
          $taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
        }
        // خط بعدی
        $num++;
      }
      //آیا به انتهای فایل رسیده‌ایم؟
      if (!feof($taxPayersFile)) {
        // به دلیل خطای خواندن از حلقه خارج شد
        throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
      } else {
        // حلقه در نشانگر پایان فایل خروجی شد
        //خطاها در یک فایل متنی ذخیره می‌شوند
        $this->saveString($errorsFilename, implode("\n", $errors));
        // نتیجه تابع
        return $taxPayersData;
      }
    } finally {
      // اگر فایل باز باشد، بسته می‌شود
      if ($taxPayersFile) {
        fclose($taxPayersFile);
      }
    }
  }

  //ذخیره نتایج
  public function saveResults(string $resultsFilename, array $taxPayersData): void {
    //ذخیرهٔ آرایهٔ [$taxPayersData] در فایل متنی [$resultsFileName]
    //اگر فایل متنی [$resultsFileName] وجود نداشته باشد، ایجاد می‌شود
    $this->saveString($resultsFilename, implode("\n", $taxPayersData));
  }

  //ذخیره نتایج یک جدول در یک فایل متنی
  private function saveString(string $fileName, string $data): void {
    //جدول [$data] را در فایل متنی [$fileName] ذخیره کنید
    // اگر فایل متنی [$fileName] وجود نداشته باشد، ایجاد می‌شود
    if (file_put_contents($fileName, $data) === FALSE) {
      throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
    }
  }

}

توضیحات

  • خط ۶: در اینجا ما یک ویژگی (trait) تعریف می‌کنیم، نه یک کلاس؛
  • خطوط ۹–۸۹: متد [getTaxPayersData] متد با همین نام را در رابط [InterfaceDao] پیاده‌سازی می‌کند. این روش داده‌های مربوط به مالیات‌دهندگان [statut marital, nombre denfants, salaire annuel] را از یک فایل متنی به نام [$taxPayersFilename] بازیابی می‌کند. این داده‌ها را در قالب یک آرایه [$taxPayersData] از عناصر نوع [TaxPayerData] (خطوط 67، 81) بازمی‌گرداند؛
  • متد [getTaxPayersData] بسیار شبیه به متد [AbstractBaseImpots::executeBatchImpots] توصیف‌شده در بخش «لینک» است، با تفاوت‌های زیر:
    • روش [getTaxPayersData] صرفاً داده‌های مودی را بازیابی می‌کند. این روش هیچ محاسبه مالیاتی انجام نمی‌دهد. این وظیفه لایه [métier] است؛
    • مانند روش [executeBatchImpotsخطاها را گزارش می‌کند. در اینجا، خطاها ابتدا در یک آرایه [$errors] (خط ۱۳) ذخیره می‌شوند که در پایان پردازش (خط ۷۹) در یک فایل متنی ذخیره می‌شود. بسته به شرایط، این آرایه ممکن است خالی باشد یا نباشد؛
    • در صورت وقوع یک خطای مرگبار، یک استثنای [ExceptionImpots] پرتاب می‌شود (خطوط ۲۰ و ۷۵)؛
  • خط ۷۳: به پردازش انجام‌شده هنگام خروج از حلقه در خطوط ۲۶–۷۱ توجه کنید. در واقع، تابع [fgets] این عیب را دارد که متغیر بولی FALSE را هم زمانی که خواندن خطوط به نشانگر پایان فایل رسیده و هم زمانی که عملیات خواندن به دلیل خطا ناموفق بوده، روی true تنظیم می‌کند. برای تمایز قائل شدن بین این دو حالت، با استفاده از تابع [feof] بررسی می‌کنیم که آیا به انتهای فایل رسیده‌ایم یا خیر. اگر به انتهای فایل نرسیده باشیم، یعنی خطایی رخ داده است و در این صورت یک استثنا پرتاب می‌شود؛
  • خطوط ۸۳–۸۸: تابع [finally] صرف‌نظر از اینکه آیا در حین پردازش فایل استثنایی رخ داده است یا خیر، اجرا می‌شود؛
  • خط ۸۵: اگر فایل باز شده باشد، آنگاه «هندل» (handle) فایل، یعنی [$taxPayersFile]، مقدار بولی TRUE را دارد؛ در غیر این صورت، مقدار آن FALSE است؛
  • خطوط ۹۹–۱۰۵: متد خصوصی [saveString]، که در خط ۷۹ برای نوشتن آرایه خطا در یک فایل متنی استفاده می‌شود؛
  • خط ۹۹: متد [saveString] دو پارامتر می‌گیرد:
    • [string $filenameکه نام فایل متنی مورد استفاده برای ذخیره داده‌ها است؛
    • [string $dataکه رشته‌ای است که باید در فایل متنی نوشته شود. این رشته از مجموعه‌ای از خطوط تشکیل شده که با کاراکتر پایان خط \n خاتمه می‌یابند؛
  • خط ۱۰۲: تابع PHP [file_puts_contents] یک رشته را به یک فایل متنی می‌نویسد. این تابع فایل را باز می‌کند، رشته را در آن می‌نویسد و فایل را می‌بندد. اگر خطایی رخ داده باشد، مقدار بولی FALSE را برمی‌گرداند؛
  • خط ۱۰۳: اگر خطایی رخ دهد، یک استثنا پرتاب می‌شود؛
  • خطوط ۹۲–۹۶: پیاده‌سازی متد [saveResults] از رابط [InterfaceDao]. متد خصوصی [saveString] دوباره استفاده می‌شود. در اینجا، پارامتر دوم تابع `[saveString]` یک رشته است که از آرایه `[$taxPayersData]` ساخته شده و عناصر آن از نوع `[TaxPayerData]` هستند. ممکن است این سؤال پیش بیاید که نتیجهٔ این عملیات چه خواهد بود:

implode("\n", $taxPayersData)

در کلاس [TaxPayerData] (بخش لینک)، متد زیر [__toString] را تعریف کرده‌ایم:


public function __toString() {
    //رشته JSON شیء
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

عملیات


implode("\n", $taxPayersData)

هر عنصر آرایه [$taxPayersData] را – که توسط متد [__toString] به یک رشته تبدیل شده است – با کاراکتر پایان خط \n الحاق می‌کند. این کار یک رشته به شکل زیر تولید می‌کند:

json1\njson2\n…

نتیجه‌گیری

ویژگی [TraitDao] دو تا از متدهای رابط [InterfaceDao] را پیاده‌سازی کرده است: [getTaxPayersData] و [saveResults]:


<?php

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

interface InterfaceDao {

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

  // خواندن داده‌های سازمان مالیاتی (باندهای مالیاتی)
  public function getTaxAdminData(): TaxAdminData;

  // ذخیره نتایج
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

ما هنوز باید متد [getTaxAdminData] را پیاده‌سازی کنیم که داده‌ها را از مقامات مالیاتی بازیابی می‌کند.

11.3.3. کلاس [ImpotsWithTaxAdminDataInJsonFile]

کلاس [ImpotsWithTaxAdminDataInJsonFile] رابط [InterfaceDao] را به شرح زیر پیاده‌سازی می‌کند:


<?php

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

// تعریف کلاس ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
  // استفاده از یک ویژگی
  use TraitDao;
  // شیء از نوع TaxAdminData حاوی داده‌های بازه مالیاتی
  private $taxAdminData;

  // سازنده
  public function __construct(string $taxAdminDataFilename) {
    // ما می‌خواهیم ویژگی [$this->taxAdminData] را مقداردهی اولیه کنیم
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
  }

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

نظرات

  • خط ۷: کلاس [ImpotsWithTaxAdminDataInJsonFile] رابط [InterfaceDao] را پیاده‌سازی می‌کند؛
  • خط ۹: کلاس [ImpotsWithTaxAdminDataInJsonFile] از تریت [traitDao] استفاده می‌کند که، همانطور که می‌دانیم، متدهای [getTaxPayersData] و [saveResults] از ... را پیاده‌سازی می‌کندرابط [InterfaceDao]. بنابراین کلاس [ImpotsWithTaxAdminDataInJsonFile] تنها باید متد [getTaxAdminData] را پیاده‌سازی کند، که داده‌ها را از مقامات مالیاتی بازیابی می‌کند؛
  • خط ۱۱: ویژگی از نوع [TaxAdminData] که توسط متد [getTaxAdminData] در خطوط ۲۰–۲۲ بازگردانده می‌شود. این ویژگی توسط سازنده در خطوط ۱۴–۱۷ مقداردهی اولیه می‌شود؛

اکنون کار با لایه [dao] برنامه خود را به پایان رسانده‌ایم: ما یک کلاس داریم که رابط [InterfaceDao] را که برای خودمان تعریف کرده‌ایم، به طور کامل پیاده‌سازی می‌کند. اکنون می‌توانیم به لایه [métier] برویم.

11.4. لایه [métier]

اکنون لایه [2] معماری خود را پیاده‌سازی خواهیم کرد:

Image

11.4.1. رابط [InterfaceMétier]

رابط لایه [métier] به شرح زیر خواهد بود:


<?php

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

interface InterfaceMetier {

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

  //محاسبه مالیات در حالت دسته‌ای
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}

نظرات

  • خط ۹: رابط [InterfaceMétier] می‌تواند بدهی مالیاتی یک فرد حقیقی را محاسبه کند، به شرطی که اطلاعات زیر در اختیار آن قرار گیرد: وضعیت تأهل، تعداد فرزندان، حقوق سالانه. متد [calculerImpot] از لایه [dao] استفاده نمی‌کند و بنابراین هیچ استثنایی ایجاد نمی‌کند؛
  • خط ۹: رابط [InterfaceMétier] همچنین می‌تواند بدهی مالیاتی گروهی از مودیان را که داده‌هایشان در فایل متنی به نام [$taxPayersFileName] موجود است، محاسبه کند. این رابط نتایج را در یک فایل متنی به نام [$resultsFileName] می‌نویسد. روش [executeBatchImpots] باید از لایه [dao] فراخوانی کند که مسئول دسترسی به سیستم فایل است. سپس استثناءها می‌توانند از لایه [dao] منتقل شوند، که متد [executeBatchImpots] آن‌ها را نمی‌گیرد: این متد اجازه می‌دهد تا به اسکریپت اصلی منتقل شوند. خطاهای غیرکشنده در فایل متنی به نام [$errorsFileName] ثبت می‌شوند؛
  • خط ۹: متد [calculerImpot] یک متد کاملاً [métier] است. این متد به منشأ داده‌هایی که استفاده می‌کند کاری ندارد؛
  • خط ۱۲: متد [executeBatchImpots] با لایه [dao] تعامل خواهد داشت تا از فایل‌های متنی بخواند و در آن‌ها بنویسد. این متد بارها متد کسب‌وکار [calculerImpot] را فراخوانی خواهد کرد؛

11.4.2. کلاس [Metier]

کلاس [Metier] رابط [InterfaceMetier] را به شرح زیر پیاده‌سازی می‌کند:


<?php

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

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

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

  public function __construct(InterfaceDao $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);
  }

  //محاسبه مالیات‌ها در حالت دسته‌ای
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {

    //ذخیره نتایج
    $this->dao->saveResults($resultsFileName, $results);
  }

}

نظرات

  • خط ۶: کلاس [Metier] رابط [InterfaceMetier] را پیاده‌سازی می‌کند، یعنی متدهای [calculerImpot] (خطوط ۳۰–۳۴) و [executeBatchImpots] (خطوط ۶۶–۷۰)؛
  • خط ۸: مرجعی به لایه [dao]. این امر ضروری است تا لایه [métier] بداند هنگام نیاز به داده‌های خارجی با چه کسی تماس بگیرد. این ویژگی از طریق متد setter در خطوط 14–17 یا از طریق متد constructor در خطوط 19–26 مقداردهی اولیه خواهد شد؛
  • خط ۱۰: شیئی از نوع [TaxAdminData] که داده‌های مرجع مالیاتی را در بر می‌گیرد. این داده‌ها توسط متد کسب‌وکار [calculerImpot] مورد نیاز است. این ویژگی از طریق سازنده در خطوط ۱۹–۲۶ مقداردهی اولیه می‌شود؛
  • خطوط ۱۹–۲۶: سازنده دو ویژگی کلاس را مقداردهی اولیه می‌کند:
    • ویژگی [$dao] با مرجعی که به‌عنوان پارامتر به سازنده ارسال شده است، مقداردهی اولیه می‌شود. توجه باید داشت که نوع این پارامتر از نوع رابط [InterfaceDao] است، و بدین ترتیب به کلاس [Metier] اجازه می‌دهد تا توسط هر کلاسی که این رابط را پیاده‌سازی می‌کند، مقداردهی اولیه شود؛
    • ویژگی [$taxAdminData] با فراخوانی متد [getTaxAdminData] از لایه [dao] مقداردهی اولیه می‌شود؛

از این رو وقتی روش‌های [calculerImpots] و [executeBatchImpots] اجرا می‌شوند، دو ویژگی [$dao] و [$taxAdminData] مقداردهی اولیه می‌شوند.

روش [calculerImpots] به شرح زیر است:


public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié: بله، نه
    // $enfants: تعداد فرزندان
    //$salaire: حقوق سالانه
    // $this->taxAdminData: داده‌های سازمان مالیاتی
    //
    // ما بررسی می‌کنیم که داده‌های سازمان مالیاتی را در اختیار داریم
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    //محاسبه مالیات با فرزندان
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    //محاسبه مالیات بدون احتساب فرزندان
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // اعمال سقف ضریب خانوادگی
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        //$PLAFOND_QF_DEMI_PART یورو برای دو فرزند اول
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        //$PLAFOND_QF_DEMI_PART یورو برای دو فرزند اول، دو برابر آن برای فرزندان بعدی
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // بالاترین نرخ مالیات اعمال می‌شود
    if ($impot1 > $impot2) {
      $impot = $impot1;
      $taux = $result1["taux"];
      $surcôte = $result1["surcôte"];
    } else {
      $surcôte = $impot2 - $impot1 + $result2["surcôte"];
      $impot = $impot2;
      $taux = $result2["taux"];
    }
    // محاسبه هرگونه معافیت مالیاتی
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    //محاسبه هرگونه معافیت مالیاتی
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    //نتیجه
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

توضیحات

  • این کد با متد [AbstractBaseImpots::calculerImpot] در نسخه ۳ مطابقت دارد، همانطور که در بخش مرتبط توضیح داده شده است. همین امر در مورد متدهای خصوصی [calculerImpot2, getDecôte, getRéduction, getRevenuImposable] نیز صدق می‌کند؛

روش [Metier::executeBatchImpots] به شرح زیر است:


public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    //اجازه می‌دهد استثناهای ناشی از لایه [dao] منتقل شوند
    //بازیابی داده‌های مالیات‌دهنده
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    //جدول نتایج
    $results = [];
    // ما آنها را تحلیل می‌کنیم
    foreach ($taxPayersData as $taxPayerData) {
      // مالیات محاسبه می‌شود
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // تکمیل [$taxPayerData]
      $taxPayerData->setMontant($result["impôt"]);
      $taxPayerData->setDécôte($result["décôte"]);
      $taxPayerData->setSurCôte($result["surcôte"]);
      $taxPayerData->setTaux($result["taux"]);
      $taxPayerData->setRéduction($result["réduction"]);
      // نتیجه را وارد جدول نتایج کنید
      $results [] = $taxPayerData;
    }
    //ذخیره نتایج
    $this->dao->saveResults($resultsFileName, $results);
  }

توضیحات

  • خط ۱: این متد باید برای هر مؤدی که در فایل متنی به نام [$taxPayersFileName] یافت می‌شود، متد [calculerImpot] را به طور مکرر فراخوانی کند. این متد باید نتایج را در فایل متنی به نام [$resultsFileName] بنویسد. هرگونه خطای غیرکشنده (non-fatal) ثبت‌شده، در فایل متنی به نام [$errorsFileName] ثبت می‌شود. این متد خود هیچ استثنایی (exception) پرتاب نمی‌کند، اما اجازه می‌دهد تا استثناهای پرتاب‌شده توسط لایه [dao] منتقل (propagate) شوند؛
  • خط ۴: داده‌های مالیات‌دهنده از لایه [dao] درخواست می‌شود. این لایه آرایه‌ای از عناصر از نوع [TaxPayerData] را بازمی‌گرداند که یک کلاس ویژگی [marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux] است (به بخش «لینک» مراجعه کنید). اگر در اینجا یک استثنا رخ دهد، از آنجا که توسط catch گرفته نمی‌شود، به طور خودکار به کد فراخوانی‌کننده بازگردانده می‌شود. این بدان معناست که در صورت وقوع استثنا، خط ۶ اجرا نمی‌شود؛
  • خط ۶: آرایه نتایج از نوع [TaxPayerData]؛
  • خطوط ۸–۲۲: مالیات برای هر عنصر از آرایه مالیات‌دهندگان [$taxPayersData] محاسبه می‌شود. برای این کار، متد داخلی [calculerImpot] فراخوانی می‌شود (خط ۱۰);
  • خطوط ۱۵–۱۹: نتیجهٔ به‌دست‌آمده برای مقداردهی اولیهٔ ویژگی‌های [TaxPayerData] که هنوز مقداردهی نشده بودند، استفاده می‌شود؛
  • خط ۲۱: نتیجهٔ به‌دست‌آمده در جدول نتایج [$results] انباشته می‌شود؛
  • خط 24: پس از محاسبه مالیات برای همه مالیات‌دهندگان، نتایج در یک فایل متنی ذخیره می‌شوند. این کار توسط لایه [dao] انجام می‌شود؛

نتیجه‌گیری

به‌طور کلی، نوشتن لایه [métier] نسبتاً ساده است، زیرا با لایه [dao] ارتباط برقرار می‌کند که به نوبه خود دسترسی به داده‌ها و مدیریت خطاهای مرتبط را بر عهده دارد.

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

اکنون اسکریپت لایه [3] معماری خود را می‌نویسیم:

Image

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


<?php

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

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

// مدیریت خطا توسط PHP
//ini_set("display_errors", "0");

// شامل کردن رابط‌ها و کلاس‌ها
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// تعریف ثابت‌ها
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // ایجاد لایه [dao]
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // ایجاد لایه [métier]
  $métier = new Metier($dao);
  //محاسبه مالیات در حالت دسته‌ای
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // خطای نمایش داده شده
  print $ex->getMessage() . "\n";
}
// پایان
print "Terminé\n";
exit;


توضیحات

  • خط ۲۴: نام فایل داده‌های مودی؛
  • خط ۲۵: نام فایل نتایج؛
  • خط ۲۶: نام فایل خطا؛
  • خط ۲۷: نام فایل jSON حاوی داده‌های مرجع مالیاتی؛
  • خط ۳۱: ایجاد لایه [dao]؛
  • خط ۳۳: ایجاد لایه [métier] بر اساس این لایه [dao];
  • خط ۳۵: اجرای متد [executeBatchImpots] از لایه [métier]؛
  • خطوط ۳۶–۳۹: همانطور که دیدیم، لایه [métier] می‌تواند استثناها را پرتاب کند. این استثناها در اینجا رهگیری می‌شوند؛

11.6. آزمون‌های بینایی

11.6.1. آزمون شمارهٔ ۱

با استفاده از پرونده مودی مالیاتی زیر [taxpayersdata.txt]:


oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000

فایل خطای زیر [errors.txt] تولید می‌شود:


la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée

و فایل نتایج زیر [resultats.txt]:

1
2
3
4
5
6
7
8
9
{"marié":"oui","enfants":2,"salaire":55555,"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}
{"marié":"oui","enfants":2,"salaire":50000,"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}
{"marié":"oui","enfants":3,"salaire":50000,"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}
{"marié":"non","enfants":2,"salaire":100000,"impôt":19884,"surcôte":4480,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":3,"salaire":100000,"impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"marié":"non","enfants":0,"salaire":100000,"impôt":22986,"surcôte":0,"décôte":0,"réduction":0,"taux":0.41}
{"marié":"oui","enfants":2,"salaire":30000,"impôt":0,"surcôte":0,"décôte":0,"réduction":0,"taux":0}
{"marié":"non","enfants":0,"salaire":200000,"impôt":64210,"surcôte":7498,"décôte":0,"réduction":0,"taux":0.45}
{"marié":"oui","enfants":3,"salaire":200000,"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}

11.6.2. آزمون شمارهٔ ۲

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

const TAXPAYERS_DATA_FILENAME = "taxpayersdata2.txt";

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


Warning: fopen(taxpayersdata2.txt): failed to open stream: No such file or directory in C:\Data\st-2019\dev\php7\poly\scripts-console\impots\version-04\TraitDao.php on line 18
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
Done.
  • خط ۱: هشدارها از تفسیرگر PHP؛
  • خط ۲: پیام خطا برای استثنایی که توسط لایه [dao] پرتاب شده است؛

امکان مخفی کردن پیام‌های خطا از تفسیرگر PHP وجود دارد:

Image

خط ۲۱ کد بالا به سیستم دستور می‌دهد که خطاهای PHP را نمایش ندهد. در فاز توسعه، این خطاها باید نمایش داده شوند. در حالت تولید، باید پنهان شوند.

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


Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé

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

تست‌های بصری بسیار ناکافی هستند:

  • ما معمولاً خود را به تنها چند آزمون محدود می‌کنیم؛
  • توجه به جزئیات در طول این بررسی بصری متغیر است و ممکن است جزئیاتی نادیده گرفته شوند؛

در دنیای توسعه حرفه‌ای، تست‌ها توسط کارکنان متخصصی نوشته می‌شوند که نقش اصلی آن‌ها دقیقاً همین کار است. بنابراین، آن‌ها تلاش می‌کنند تا تست‌ها را تا حد امکان جامع طراحی کنند. برای دستیابی به این هدف، از فریم‌ورک‌های تست‌گیری استفاده می‌کنند.

در اینجا، از فریم‌ورک Codeception [https://codeception.com/] استفاده خواهیم کرد، زیرا می‌توان آن را در NetBeans ادغام کرد. این فریم‌ورک امکانات گسترده‌ای را ارائه می‌دهد. ما فقط از چند مورد از آن‌ها استفاده خواهیم کرد. ایده این است که پس از هر نسخه جدید از تمرین برنامه، راهی سریع برای تأیید صحت عملکرد آن داشته باشیم. وجود تست‌های موفق به توسعه‌دهنده اطمینان خاطر می‌دهد که کدی که نوشته است، درست کار می‌کند. این یک عامل مهم است.

11.7.1. نصب فریم‌ورک [Codeception]

مانند بسیاری از کتابخانه‌های PHP، فریم‌ورک [Codeception] در کنار [Composer] نصب می‌شود. بنابراین یک ترمینال Laragon را باز می‌کنیم (به بخش «لینک» مراجعه کنید).

ابتدا باید فریم‌ورک‌های تست PHPUnit و [https://phpunit.de/] را نصب کنیم. این به این دلیل است که Codeception از فریم‌ورک PHPUnit در پس‌زمینه استفاده می‌کند:

Image

سپس، فریم‌ورک Codeception را نصب می‌کنیم:

Image

تمام. حالا بیایید به ادغام [Codeception] در NetBeans نگاهی بیندازیم.

11.7.2. ادغام [CodeCeption] در NetBeans

Image

  • در [1-2]، به ویژگی‌های پروژه دسترسی پیدا می‌کنیم؛
  • در [3-4]، [Codeception] را به‌عنوان یکی از فریم‌ورک‌های تست پروژه تنظیم کنید؛

Image

Image

  • در [5-8]، چارچوب [Codeception] برای پروژه راه‌اندازی می‌شود؛

Image

  • در [9]، یک پوشه به نام [tests] ایجاد شد، به همراه یک فایل پیکربندی به نام [codeception.yml] در [10-11]. فایل [11] با فایل [10] یکسان است. Codeception به سادگی یک پوشه به نام [Important Files] ایجاد کرد تا به فایل [10] معنای مشخصی بدهد؛
  • در [12-13]، به ویژگی‌های پروژه بازمی‌گردیم؛

Image

  • در [14-16]، پوشه [tests] [16] به عنوان پوشه تست پروژه تعیین شده است؛
  • در [16]، پوشه [tests] سپس با نام جدید [Test Files] ظاهر می‌شود. وجود این پوشه در یک پروژه PHP نشان می‌دهد که این پروژه شامل یک چارچوب برای تست خودکار است؛
  • ما تست‌های خود را در پوشه [unit] [17] ایجاد خواهیم کرد؛

11.7.3. تست‌ها برای لایه [dao]

Image

  • ما تمام تست‌های خود را در پوشه [unit] [1] ایجاد خواهیم کرد؛
  • نام کلاس‌های تست [Codeception] باید با کلمه کلیدی [Test] پایان یابد؛ در غیر این صورت، این کلاس‌ها به عنوان کلاس‌های تست شناخته نخواهند شد؛

کلاس‌های تست ما [Codeception] شکل زیر را خواهند داشت: [https://codeception.com/docs/05-UnitTests]:


<?php

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

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

// بارگذاری محیط تست


class DaoTest extends \Codeception\Test\Unit {
  // ویژگی‌های تست
  private $attribut1;

  public function __construct() {
    parent::__construct();
    // ابتدایی‌سازی محیط تست

  }

  // آزمون‌ها
  public function testTaxAdminData() {
    // آزمایش‌ها
    $this->assertEquals($expected, $actual);
    $this->assertEqualsWithDelta($expected, $actual, $delta);
    $this->assertTrue($actual);
    $this->assertFalse($actual);
    $this->assertNull($actual);
    $this->assertEmpty($actual);
    $this→assertSame($expected, $actual);

  }

}

توضیحات

  • خط ۷: کلاس‌های تست در همان فضای نام (namespace) برنامه تحت تست قرار خواهند داشت؛
  • خطوط ۹–۱۰: در اینجا عملیات [require] برای بارگذاری کلاس‌ها و رابط‌های تحت آزمون آمده است؛
  • خط ۱۲: نام کلاس تست باید با کلمه کلیدی [Test] پایان یابد. این کلاس باید از کلاس [\Codeception\Test\Unit] ارث بری کند؛
  • خطوط 16–20: کَنستراکتور به ما امکان می‌دهد محیط تست را راه‌اندازی کنیم؛
  • خط ۲۳: نام متدهای تست باید با کلمه کلیدی [test] شروع شود؛
  • خطوط 25–31: می‌توان از روش‌های تست مختلفی استفاده کرد؛

کلاس تست [DaoTest] به شرح زیر خواهد بود:


<?php

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

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

// ثوابت
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// شامل کردن رابط‌ها و کلاس‌ها
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// آزمایش -----------------------------------------------------
// تعریف ثابت‌ها
const TAXADMINDATA_FILENAME = "taxadmindata.json";

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

  public function __construct() {
    parent::__construct();
    // ایجاد لایه [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

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

  }
}

توضیحات

برای ایجاد تست‌ها برای یک نسخه از تمرین برنامه، از محیطی یکسان با آنچه در اسکریپت اصلی آن نسخه استفاده می‌شود، بهره خواهیم برد. برای نسخه 04، این اسکریپت زیر است: [main.php]:


<?php

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

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

// مدیریت خطا توسط PHP
ini_set("display_errors", "0");

// شامل کردن رابط‌ها و کلاس‌ها
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// تعریف ثابت‌ها
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // ایجاد لایه [dao]
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // ایجاد لایه [métier]
  $métier = new Metier($dao);
  //محاسبه مالیات در حالت دسته‌ای
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // نمایش خطا
  print $ex->getMessage() . "\n";
}
// پایان
print "Terminé\n";
exit;

برای تست لایه [dao]، در کلاس تست:

  • ما از محیط موجود در خطوط ۱۳ تا ۲۷ از [main.php] استفاده می‌کنیم؛
  • در سازنده کلاس تست، لایه [dao] را همان‌طور که در خط ۳۱ نشان داده شده است، می‌سازیم؛
  • ما متدهای تست را می‌نویسیم؛

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

بیایید به کد کامل کلاس تست بازگردیم:


<?php

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

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

// ثوابت
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// شامل کردن رابط‌ها و کلاس‌ها
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// تعریف ثابت‌ها
const TAXADMINDATA_FILENAME = "taxadmindata.json";

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

  public function __construct() {
    parent::__construct();
    // ایجاد لایه [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // آزمایش‌ها
  public function testTaxAdminData() {
    // ثوابت محاسباتی
    $this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
    $this->assertEquals(21037, $this->taxAdminData->getPlafondRevenusCelibatairePourReduction());
    $this->assertEquals(42074, $this->taxAdminData->getPlafondRevenusCouplePourReduction());
    $this->assertEquals(3797, $this->taxAdminData->getValeurReducDemiPart());
    $this->assertEquals(1196, $this->taxAdminData->getPlafondDecoteCelibataire());
    $this->assertEquals(1970, $this->taxAdminData->getPlafondDecoteCouple());
    $this->assertEquals(1595, $this->taxAdminData->getPlafondImpotCelibatairePourDecote());
    $this->assertEquals(2627, $this->taxAdminData->getPlafondImpotCouplePourDecote());
    $this->assertEquals(12502, $this->taxAdminData->getAbattementDixPourcentMax());
    $this->assertEquals(437, $this->taxAdminData->getAbattementDixPourcentMin());
    // محدوده‌های مالیاتی
    $this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $this->taxAdminData->getLimites());
    $this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $this->taxAdminData->getCoeffR());
    $this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $this->taxAdminData->getCoeffN());
  }

}

توضیحات

  • خطوط ۱۰–۲۵: بارگذاری محیط مورد نیاز برای آزمایش و تعریف ثابت‌ها؛
  • خطوط ۳۱–۳۶: ساخت لایه [dao] (خط ۳۴)، و به دنبال آن مقداردهی اولیه ویژگی [$taxAdminData] (خط ۲۹). این ویژگی حاوی داده‌هایی از مقامات مالیاتی است؛
  • خطوط ۳۹–۵۵: تنها روش آزمون. این شامل تأیید این است که محتوای ویژگی [$taxAdminData] با آنچه انتظار می‌رود مطابقت دارد؛
  • خطوط ۴۱–۵۰: بررسی ثابت‌های مورد استفاده در محاسبه مالیات؛
  • خطوط ۵۲–۵۵: بررسی محدوده‌های مالیاتی. متد [assertSame] تأیید می‌کند که دو موجودیت PHP، در این مورد جداول، یکسان هستند؛

برای اجرای این کلاس تست، مراحل زیر را دنبال کنید:

Image

  • در [1-2]، تست را اجرا کنید؛
  • [3]: پنجره نتایج آزمون؛
  • [4]: کلاس تست اجرا شده؛
  • [5]: نتایج. در اینجا، یک روش آزمون با موفقیت اجرا شده است؛
  • [6]: زمانی که تست شکست می‌خورد، یا رایج‌تر اینکه هیچ تستی اجرا نشده باشد، باید پنجره [6] را بررسی کنید. در اکثر موارد، بارگذاری محیط تست با شکست مواجه شده است، به این معنی که هیچ تستی نمی‌تواند اجرا شود. خطاهای نمایش‌داده‌شده در [6]، خطاهایی هستند که هنگام اجرای یک اسکریپت استاندارد PHP رخ می‌دهند؛

بیایید نگاهی به مثالی از یک تست معیوب بیندازیم:

در کلاس تست، ما خطایی را در تعریف یک ثابت وارد می‌کنیم:


// ثوابت
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");

سپس تست را اجرا می‌کنیم. نتیجهٔ به‌دست‌آمده به شرح زیر است:

Image

در پنجره [4]:

Image

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

کلاس تست [MetierTest] از همان قواعد ساختاری کلاس [DaoTest] پیروی می‌کند، اما متدهای تست بیشتری دارد:


<?php

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

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

// ثوابت
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// شامل کردن رابط‌ها و کلاس‌ها
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// آزمایش -----------------------------------------------------
// تعریف ثابت‌ها
const TAXADMINDATA_FILENAME = "taxadmindata.json";

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

  public function __construct() {
    parent::__construct();
    // ایجاد لایه [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    // ایجاد لایه [métier]
    $this->métier = new Metier($dao);
  }

  // آزمایش
  public function test1() {
    $result = $this->métier->calculerImpot("oui", 2, 55555);
    $this->assertEqualsWithDelta(2815, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test2() {
    $result = $this->métier->calculerImpot("oui", 2, 50000);
    $this->assertEqualsWithDelta(1385, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(384, $result["décôte"], 1);
    $this->assertEqualsWithDelta(347, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test3() {
    $result = $this->métier->calculerImpot("oui", 3, 50000);
    $this->assertEqualsWithDelta(0, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(720, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test4() {
    $result = $this->métier->calculerImpot("non", 2, 100000);
    $this->assertEqualsWithDelta(19884, $result["impôt"], 1);
    $this->assertEqualsWithDelta(4480, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test5() {
    $result = $this->métier->calculerImpot("non", 3, 100000);
    $this->assertEqualsWithDelta(16782, $result["impôt"], 1);
    $this->assertEqualsWithDelta(7176, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test6() {
    $result = $this->métier->calculerImpot("oui", 3, 100000);
    $this->assertEqualsWithDelta(9200, $result["impôt"], 1);
    $this->assertEqualsWithDelta(2180, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.3, $result["taux"]);
  }

  public function test7() {
    $result = $this->métier->calculerImpot("oui", 5, 100000);
    $this->assertEqualsWithDelta(4230, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test8() {
    $result = $this->métier->calculerImpot("non", 0, 100000);
    $this->assertEqualsWithDelta(22986, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test9() {
    $result = $this->métier->calculerImpot("oui", 2, 30000);
    $this->assertEqualsWithDelta(0, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0, $result["taux"]);
  }

  public function test10() {
    $result = $this->métier->calculerImpot("non", 0, 200000);
    $this->assertEqualsWithDelta(64210, $result["impôt"], 1);
    $this->assertEqualsWithDelta(7498, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.45, $result["taux"]);
  }

  public function test11() {
    $result = $this->métier->calculerImpot("oui", 3, 200000);
    $this->assertEqualsWithDelta(42842, $result["impôt"], 1);
    $this->assertEqualsWithDelta(17283, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }
}

توضیحات

  • خطوط ۱۰–۲۵: بارگذاری فایل‌هایی که محیط تست را تعریف می‌کنند. این مشابه لایه [dao] است؛
  • خطوط ۳۱–۳۷: نمونه‌سازی لایه‌های [dao] و [métier]؛
  • خطوط ۴۰–۴۷: آزمون محاسبه مالیات؛
  • خط ۴۱: یک محاسبه مالیات خاص با استفاده از لایه [métier] انجام می‌شود؛
  • خطوط ۴۲–۴۶: یک بررسی انجام می‌شود تا اطمینان حاصل شود که نتایج حاصل‌شده با نتایج شبیه‌ساز سازمان مالیاتی [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm] مطابقت دارد؛
  • خطوط ۲۳–۲۶: آزمون‌های برابری تا نزدیک‌ترین یورو انجام می‌شوند. در واقع، ما دیده‌ایم که مشکلات گرد کردن باعث شده بود که الگوریتم در سند نتایج مورد انتظار را تا نزدیک‌ترین یورو تولید کند؛
  • خط ۲۷: نرخ مالیات بدون هیچ حاشیه خطایی محاسبه می‌شود؛
  • خطوط ۴۹–۱۳۷: این نوع آزمون ۱۰ بار تکرار می‌شود، هر بار با پیکربندی متفاوت مالیات‌دهنده؛

آزمایش‌ها نتایج زیر را نشان می‌دهند:

Image

11.7.5. آزمایش‌ها برای نسخه‌های آینده

در ادامه، آزمون‌های لایه‌های [dao] و [métier] با آزمون‌های نسخه 04 یکسان خواهند بود. تنها محیط آزمون تغییر خواهد کرد. بنابراین، ما فقط این محیط و نتایج آزمون را ارائه خواهیم داد.