Skip to content

8. 应用练习 – 版本 3

我们重新回顾之前已学习的练习(第4.3和4.4节),并使用PHP代码(采用类)来解决该问题。

8.1. 脚本树结构

Image

8.2. 异常 [ExceptionImpots]

在第 03 版中,当类构造函数或方法遇到错误时,将抛出如下 [ExceptionImpots] 类型的异常:

<?php

// 命名空间
namespace Application;

class ExceptionImpots extends \RuntimeException {

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

}

注释

  • 第 4 行:类 [ExceptionImpots] 位于命名空间 [Application] 中;
  • 第 6 行:类 [ExceptionImpots] 继承了 PHP 中预定义的类 [RuntimeException]
  • 第 8 行:构造函数需要两个参数:
    • $message:是与该异常相关的错误消息;
    • $code:是与该异常关联的错误代码。如果未提供,则使用代码 0;

8.3. [TaxAdminData]

在 02 版本中,税务管理数据已整合:

  • 首先存储在文件 jSON 中;
  • 随后从该文件 jSON 转换为关联数组;

在版本 03 中,税务部门的数据仍位于文件 [taxadmindata.json] 中,但属性名称有所不同:


{
    "limites": [
        9964,
        27519,
        73779,
        156244,
        0
    ],
    "coeffR": [
        0,
        0.14,
        0.3,
        0.41,
        0.45
    ],
    "coeffN": [
        0,
        1394.96,
        5798,
        13913.69,
        20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
}

在 02 版本中,该文件用于初始化一个关联数组。在 03 版本中,该文件将初始化以下类:


<?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);
    $erreur = FALSE;
    // 错误?
    if (!$fileContents) {
      // 记录错误
      $erreur = TRUE;
      $message = "Le fichier des données [$taxAdminDataFilename] n'existe pas";
    }
    if (!$erreur) {
      // 从配置文件中将代码 jSON 读取到关联数组中
      $arrayTaxAdminData = \json_decode($fileContents, true);
      // 错误?
      if ($arrayTaxAdminData === FALSE) {
        // 记录错误
        $erreur = TRUE;
        $message = "Le fichier de données jSON [$taxAdminDataFilename] n'a pu être exploité correctement";
      }
    }
    // 错误?
    if ($erreur) {
      // 抛出异常
      throw new ExceptionImpots($message);
    }
    // 初始化类属性
    foreach ($arrayTaxAdminData as $key => $value) {
      $this->$key = $value;
    }
    // 验证所有键是否已初始化
    $arrayOfAttributes = \get_object_vars($this);
    foreach ($arrayOfAttributes as $key => $value) {
      if (!isset($this->$key)) {
        throw new ExceptionImpots("L'attribut [$key] de [TaxAdminData] n'a pas été initialisé");
      }
    }
    // 验证是否仅包含实数值
    foreach ($this as $key => $value) {
      // $value 必须是 >=0 的实数或 >=0 的实数数组
      $result = $this->check($value);
      // 错误?
      if ($result->erreur) {
        // 抛出异常
        throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
      } else {
        // 记录该值
        $this->$key = $result->value;
      }
    }
    // 返回对象
    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 getCoeffR() {
    return $this->coeffR;
  }


  }

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

  public function setCoeffR($coeffR) {
    $this->coeffR = $coeffR;
    return $this;
  }



}

注释

  • 第 6-20 行:这些属性将容纳文件 jSON 和 [taxadmindata.json] 中同名的属性。 这是一个重要点:[TaxAdminData]类的属性与jSON和[taxadmindata.json]文件中的属性完全相同。这一特性极大简化了代码编写;
  • [TaxAdminData] 没有构造函数。在 PHP 中,无法拥有多个构造函数。因此,一旦定义了一个构造函数,就无法通过其他方式初始化该对象。 接下来,我们的类将不包含构造函数,而是提供多个 [setFromQqChose] 类型的方法,以便通过不同方式进行初始化。因此,[TaxAdminData] 类型的对象可通过以下表达式构建:
(new TaxAdminData())→setFromQqChose(…)
  • 第 23 行:[setFromJsonFile] 方法将类的属性初始化为 [$jsonFilename] 文件中同名属性的值;
  • 第24-42行:利用文件jSON构建关联数组[$arrayTaxAdminData]。我们在02版本的脚本[main.php]中已经见过这段代码;
  • 第44-47行:如果在处理文件jSON时遇到错误,则抛出异常。该异常将回传至主脚本[main.php]
  • 第 48-51 行:初始化类的属性。此处利用了关联数组 [$arrayTaxAdminData] 和类 [TaxAdminData] 的属性名称与来自文件 jSON 的值名称一致这一事实;
  • 第53-57行:验证[TaxAdminData]的所有属性是否已初始化;
  • 第 53 行:表达式 [get_object_vars($this)] 返回一个关联数组,其属性即为对象 [$this] 的属性,也就是类 [TaxAdminData] 的属性。 这里需要注意的是,第48至51行的初始化操作可能已向对象[$this]添加了属性。因此,如果编写:
    $this->x = "1000";

那么属性 [x] 就会被添加到对象 [$this] 上,即使该属性并未在类 [TaxAdminData] 中声明。 可以肯定的是,第 6-20 行中的属性确实属于 [$this] 对象,但它们可能未被初始化。这是一个很容易犯的错误,只需在 [taxadmindata.json] 文件中属性名称写错即可;

  • 第54-57行:遍历[$this]的所有属性,若其中任何一个未被初始化,则抛出异常;
  • 某个属性可能被初始化为错误的值。在 PHP 中,无法为属性指定类型。因此,以下操作:
$this→plafondQfDemiPart=’abcd’

是可行的,尽管属性 [$plafondQfDemiPart] 应为实数;

  • 第 59-71 行:验证类中的每个属性是否具有正实数或零值。第 76 行的函数 [check] 负责此项工作。其参数 [$value] 可以是单个值,也可以是值数组;
  • 第62行:函数[check]返回一个类型为[\stdClass]的对象,该对象具有两个属性:
    • [erreur]:若发生错误则为 TRUE,否则为 FALSE;
    • [value]:与作为参数传递的 [$value] 对应的实际数值,第 62 行;
  • 第 64 行:检查验证是否成功;
  • 第66行:如果某个属性不是正实数或零,则抛出异常;
  • 第 69 行:否则记录其数值;
  • 第 73 行:将对象 [$this] 作为结果返回;

函数 [check] 如下:


private function check($value): \stdClass {
    // $value 是一个元素数组或单个元素
    // 创建数组
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // 将类型未知的元素数组转换为实数数组
    $newTableau = [];
    $result = new \stdClass();
    // 数组中的元素必须是正数或零
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // 将浮点数放入 newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // 记录错误
        $result->erreur = TRUE;
        // 退出
        return $result;
      }
    }
    // 返回结果
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // 单个值
      $result->value = $newTableau[0];
    } else {
      // 返回值列表
      $result->value = $newTableau;
    }
    return $result;
  }

注释

  • 第 1 行:参数 [$value] 可以是数组,也可以是单个元素。 此外,其类型未知。该值来自文件 [taxadmindata.json]。根据该文件中记录的值,读取到的值可以是整数、实数、字符串或布尔值。例如:

"plafondQfDemiPart": 1551,
"plafondQfDemiPart": 1551.78,
"plafondQfDemiPart": "1551",
"plafondQfDemiPart": "xx",

在情况 1 中,该值类型为 [entier];在情况 2 中,类型为 [réel]; 在情况3中,类型为[string],可转换为数字;在情况4中,类型为[string],无法转换为数字;

  • 第4-8行:根据第1行接收的参数[$value]创建一个数组;
  • 第10行:该数组将被填充为实数;
  • 第11行:结果将是一个类型为[\stdClass]的对象;
  • 第13行:判断是否为正实数或零的条件表达式;
  • 第14-24行:验证数组[$tableau]的所有元素是否均为正实数或零,并将这些已转换为类型[float](第17行)的元素填入数组[$newTableau]
  • 第18-23行:一旦检测到某个元素不是正实数或零,便将错误记录在结果中并返回该结果;
  • 第25-34行:当数组[$tableau]的所有元素均被判定为正确时;
  • 第 32 行:返回值 [$result→value] 是一个实数数组 [float] 或一个单个实数;

第 82-85 行中的函数 [__toString] 返回对象 [$this] 的属性和值的字符串 jSON。

第 87-110 行:类的 getter 和 setter 方法;

:有时编写一个类的所有 get/set 方法可能会有些麻烦,尤其是当属性较多时。NetBeans 可以自动生成这些方法以及构造函数。要实现这一点,只需将属性 [1]

Image

  • [2] 中,右键单击您想要插入代码的位置,然后选择 [Insert Code] 选项;

Image

  • 为 [4],表示您希望生成构造函数;
  • [5] 中,勾选所有属性:这意味着您希望构造函数为每个属性都提供一个参数;
  • [6] 中,采用 Java 构造函数的样式;
  • [7] 中,指定您希望在构造函数前显式添加关键字 [public]
  • [8] 中,点击“确定”;

Image

  • [9] 中,NetBeans 已生成构造函数。但它无法为参数添加类型,因为它不知道这些参数的类型。请手动添加类型 [10]

要生成 getter 和 setter 方法,请重新执行步骤 2-4,并在步骤 4 中选择 [Getter and Setter]

Image

  • [5] 中,指定您希望为每个属性生成 getter 和 setter;
  • [6] 中,指定希望获取器和设置器采用 Java 风格:setAttribut、getAttribut;
  • [7] 中,指定这些 getter 和 setter 应为 public;
  • [8] 中,点击确认;

Image

  • [9] 中,由 NetBeans 生成的 getter 和 setter;

删除这些 getter 和 setter,然后重新执行步骤 2-7。

  • [8] 中,勾选之前未勾选的 [Fluent Setter] 选项;

最终结果如下:

Image

每个 setter 都以 [return $this] 操作结尾。这使得能够按以下方式初始化属性:

$data→setLimites($limites)→setCoeffR($coeffR)→setCoeffN($coeffN) ;

实际上,[$data→setLimites($limites)](代码第32行)的值为[$this],因此此处的值为[$data]。 因此,我们可以调用该对象的 [setCoeffR($coeffR)] 方法,以此类推,因为该方法又会返回 [$this](代码第 37 行)。 这种类方法的编写方式使得本应不返回任何内容的方法最终返回对象 [$this],这种写法被称为流式编写。它简化了这些方法的使用。

8.4. 接口 [InterfaceImpots]

现在,我们定义以下 [InterfaceImpots] 接口:


<?php

// 命名空间
namespace Application;

interface InterfaceImpots {

  // 获取用于计算税款的税率区间数据
  // 可触发异常 ExceptionImpots
  public function getTaxAdminData(): TaxAdminData;

  // 该接口能够计算税额
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // 该接口能够处理文本文件中的数据
  // $usersFilename:用户数据文件,包含婚姻状况、子女数量、年薪等信息
  // $resultsFilename:结果文件,包含婚姻状况、子女数量、年薪、应缴税额
  // $errorsFilename:遇到的错误文件
  // 可能引发异常 ExceptionImpots
  public function executeBatchImpots(string $usersFileName, string $resultsFileName, string $errorsFileName): void;
}

注释

  • 第4行:该接口位于命名空间 [Application] 中;
  • 第6行:用于计算税款的接口;
  • 第10行:方法[getTaxAdminData]将用于从税务部门获取数据,并将其存储在我们刚刚介绍的[TaxAdminData]类型的对象中。 由于这些数据可能存储在文件、数据库甚至网络中,方法 [getTaxAdminData] 可能无法成功获取数据。在此情况下,它将抛出类型为 [ExceptionImpots] 的异常。这是面向对象编程中用于报告方法或构造函数中遇到错误的标准方法;
  • 第 13 行:方法 [calculerImpot] 用于计算某用户的税额;
  • 第 20 行:方法 [executeBatchImpots] 用于计算多名纳税人的税款:
    • [$usersFileName] 是包含纳税人数据的文本文件名;
    • [$resultsFileName] 是包含这些纳税人应纳税额的文本文件名;
    • [$errorsFileName] 是包含处理这些文件时遇到的错误的文本文件名;

文本文件 [$usersFileName] 的内容可能如下:


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

请注意,第 5 行和第 7 行包含错误项。

因此,文本文件 [$resultsFileName] 的内容如下:

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}

而文本文件 [$errorsFileName] 的内容如下:

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

8.5. [Utilitaires]

此外,我们在文件 [Utilitaires.php] 中定义了一个类 [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--;
    }
    // 结束 - 返回该行
    return($ligne);
  }
}

注释

  • 第 4 行:类 [Utilitaires] 也位于命名空间 [Exemples] 中;
  • 第 9 行:方法 [cutNewLinechar] 会移除作为参数传递的文本中可能存在的换行符。它返回由此形成的新行。需要注意的是,这是一个静态方法,即它将以 [Utilitaires::cutNewLineChar] 的形式被调用;

8.6. 抽象类 [AbstractBaseImpots]

接口 [InterfaceImpots] 将由以下抽象类 [AbstractBaseImpots] 实现:


<?php

// 命名空间
namespace Application;

// 定义一个抽象类 AbstractBaseImpots
abstract class AbstractBaseImpots implements InterfaceImpots {
  // 税务管理数据
  private $taxAdminData = NULL;

  // 计算税款所需的数据
  abstract function getTaxAdminData(): TaxAdminData;

// 税款计算
// --------------------------------------------------------------------------
  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];
  }

// --------------------------------------------------------------------------
  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 $usersFileName, string $resultsFileName, string $errorsFileName): void {

  }

}

注释

  • 第 4 行:类 [AbstractBaseImpots] 将位于命名空间 [Application] 中,与当前正在编写应用程序中的其他元素一样;
  • 第 7 行:类 [AbstractBaseImpots] 实现了接口 [InterfaceImpots]
  • 第 9 行:税务管理数据将存储在 [$taxAdminData] 属性中
  • 第12行:实现接口中的[getTaxAdminData]方法。 目前尚无法定义该方法:我们在前文的段落中看到过一个示例,其中税务数据是从 jSON 文件中获取的。我们将探讨另一种情况,即数据需从数据库中检索。[getTaxAdminData] 方法的具体实现将由派生类来定义。 前两个案例将分别生成两个派生类。因此,方法 [getTaxAdminData] 被声明为抽象方法,这会自动使该类本身成为抽象类(第 7 行);
  • 第15至64行:此前在“链接”和“链接”两节中已介绍过的税额计算函数;
  • 版本 02 将税务数据存放在关联数组 [$taxAdminData] 中。版本 03 则将其存放在属性 [$this→taxAdminData] 中。这两种方案之间的第一个区别在于税务数据的可见性:
    • 在版本 02 中,关联数组 [$taxAdminData] 没有全局可见性。因此,它被作为参数传递给所有税额计算函数;
    • 在 03 版本中,属性 [$this→taxAdminData] 对类中的所有方法具有全局可见性。因此,它不再作为参数传递给所有税额计算函数;
  • 第二个区别在于,版本 03 将函数替换为类方法。现在每次调用方法都使用表达式 [$this→getMéthode(…)](第 27、31、57、60 行);
  • 第三个区别在于,当方法 [calculerImpot] 开始工作时,它并不知道其所需的属性 [private $taxAdminData] 是否已被初始化。实际上,该类的构造函数并未对其进行初始化。 因此,[calculerImpot]方法需要借助第12行的[getTaxAdminData]方法来完成初始化。第23-25行正是为此而设计的;
  • 除上述差异外,税额计算方法与先前版本保持一致;

函数 [executeBatchImpots] 如下所示:


public function executeBatchImpots(string $usersFileName, string $resultsFileName, string $errorsFileName): void {
    // 一旦处理文件,可能会出现不少错误
    try {
      // 打开文件时出现错误
      $errors = fopen($errorsFileName, "w");
      if (!$errors) {
        throw new ExceptionImpots("Impossible de créer le fichier des erreurs [$errorsFileName]", 10);
      }
      // 打开结果文件
      $results = fopen($resultsFileName, "w");
      if (!$results) {
        throw new ExceptionImpots("Impossible de créer le fichier des résultats [$resultsFileName]", 11);
      }
      // 读取用户数据
      // 每行数据格式为:婚姻状况、子女数、年薪
      $data = fopen($usersFileName, "r");
      if (!$data) {
        throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$usersFileName]", 12);
      }
      // 处理用户数据文件的当前行
      // 该行格式为:婚姻状况、子女数、年薪
      $num = 1;         // 当前行号
      $nbErreurs = 0;   // 遇到的错误数量
      while ($ligne = fgets($data, 100)) {
        // 调试
        //  print "第 " . ($i + 1) . " 行: " . $ligne;
        // 移除可能存在的换行符
        $ligne = Utilitaires::cutNewLineChar($ligne);
        // 提取组成 $ligne 的 3 个字段:已婚:子女:工资
        list($marié, $enfants, $salaire) = explode(",", $ligne);
        // 对其进行验证
        // 婚姻状况必须为“是”或“否”
        $marié = trim(strtolower($marié));
        $erreur = ($marié !== "oui" and $marié !== "non");
        if (!$erreur) {
          // 子女数量必须为整数
          $enfants = trim($enfants);
          if (!preg_match("/^\s*\d+\s*$/", $enfants)) {
            $erreur = TRUE;
          } else {
            $enfants = (int) $enfants;
          }
        }
        if (!$erreur) {
          // 工资为整数,不包含欧元分
          $salaire = trim($salaire);
          if (!preg_match("/^\s*\d+\s*$/", $salaire)) {
            $erreur = TRUE;
          } else {
            $salaire = (int) $salaire;
          }
        }
        // 错误?
        if ($erreur) {
          fputs($errors, "la ligne [$num] du fichier [$usersFileName] est erronée\n");
          $nbErreurs++;
        } else {
          // 计算税额
          $result = $this->calculerImpot($marié, (int) $enfants, (int) $salaire);
          // 将结果写入结果文件
          $result = ["marié" => $marié, "enfants" => $enfants, "salaire" => $salaire] + $result;
          fputs($results, \json_encode($result, JSON_UNESCAPED_UNICODE) . "\n");
        }
        // 下一行
        $num++;
      }
      // 有错误吗?
      if ($nbErreurs > 0) {
        throw new ExceptionImpots("Il y a eu des erreurs", 15);
      }
    } catch (ExceptionImpots $ex) {
      // 重新抛出异常
      throw $ex;
    } finally {
      // 关闭所有文件
      fclose($data);
      fclose($results);
      fclose($errors);
    }
  }

代码注释

  • 第1行:该函数接收三个参数:
    • [$usersFileName]:包含纳税人数据的文本文件名。每行文本包含一名纳税人的数据,格式为:婚姻状况(是/否)、子女数量、年薪:
oui,2,55555
oui,2,50000
  • (续)
    • [$resultsFileName]:将包含结果的文本文件名。每行文本将采用以下格式:
{"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}
  • (续)
    • [$errorsFileName]:错误文本文件的名称:

la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
  • 第 3 行:由于某些操作可能会引发异常,因此使用 try / catch / finally 块包裹方法中的所有代码;
  • 第3-19行:打开这三个文件。一旦打开失败,就会抛出异常;
  • 第24行:逐行读取文件[$data]中的内容,每次读取不超过100个字符(所有行均少于100个字符);
  • 第28行:使用静态方法[Utilitaires::cutNewLineChar]移除可能存在的换行符;
  • 第 30 行:提取读取行的三个元素;
  • 第33-52行:验证这三个元素的有效性。此处若出现错误,不会抛出异常,而是将错误信息写入文本文件[$errors](第55行);
  • 第59行:若读取的行有效,则进行税额计算。计算结果以关联数组["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux]的形式返回;
  • 第 61 行:向所得结果中添加键 [marié, enfants, salaire]
  • 第61行:将结果以字符串jSON的形式写入文本文件[$results]
  • 第68-70行:在处理完文件[$data]后,检查遇到的错误行数。如果至少有一行错误,则抛出异常;
  • 第 71-74 行:捕获代码可能抛出的异常,并立即重新抛出(第 73 行)。 此设计的目的是为了在第74-79行实现[finally]子句:无论方法代码的执行如何结束,该代码可能打开的三个文件都会被关闭。关闭未曾打开的文件不会引发错误;

8.7. [ImpotsWithTaxAdminDataInJsonFile]

抽象类 [AbstractBaseImpots] 未实现接口 [InterfaceImpots] 中的方法 [getTaxAdminData]。因此,我们需要在派生类中定义该方法。 我们在以下派生类 [ImpotsWithTaxAdminDataInJsonFile] 中进行定义:


<?php

// 命名空间
namespace Application;

// 类定义 ImpotsWithDataInArrays
class ImpotsWithTaxAdminDataInJsonFile extends AbstractBaseImpots {
  // 一个 Data 类型的属性
  private $taxAdminData;

  // 构造函数
  public function __construct(string $jsonFileName) {
    // 根据文件 jSON 初始化 $this->taxAdminData
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($jsonFileName);
  }

  // 返回用于计算税款的数据
  public function getTaxAdminData(): TaxAdminData {
    // 返回属性 [$this->taxAdminData]
    return $this->taxAdminData;
  }

}

注释

  • 第7行:类[ImpotsWithTaxAdminDataInJsonFile]继承自抽象类[AbstractBaseImpots]。它需要定义其父类未定义的方法[getTaxAdminData]
  • 第 9 行:属性 [$taxAdminData] 将包含税务部门的数据;
  • 第12-15行:构造函数接收的唯一参数是包含税务数据的文件名jSON;
  • 第 14 行:创建并初始化一个 [TaxAdminData] 类型的对象。此操作可能抛出 [ExceptionImpots] 类型的异常。该异常将回溯至主脚本 [main.php]
  • 第 18-20 行:为方法 [getTaxAdminData] 提供实现,该方法在父类中未被定义。此处只需将构造函数初始化的属性 [$this->taxAdminData] 进行赋值即可;

8.8. 脚本 [main.php]

以下脚本 [main.php] 调用了这些类和接口:


<?php

// 严格遵守函数参数的声明类型
declare(strict_types = 1);

// 命名空间
namespace Application;

// 接口和类的包含
require_once __DIR__ . '/InterfaceImpots.php';
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . '/ExceptionImpots.php';
require_once __DIR__ . '/Utilitaires.php';
require_once __DIR__ . '/AbstractBaseImpots.php';
require_once __DIR__ . "/ImpotsWithTaxAdminDataInJsonFile.php";

// 测试 -----------------------------------------------------
// 常量定义
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // 创建对象 ImpotsWithTaxAdminDataInJsonFile
  $impots = new ImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // 执行税务批处理
  $impots->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // 显示错误
  print $ex->getMessage() . "\n";
}
// 结束
print "Terminé\n";
exit();


注释

  • 第 4 行:强制严格遵守函数参数的类型;
  • 第 7 行:脚本 [main.php] 也被放置在命名空间 [Application] 中;
  • 第10-15行:向PHP解释器指定脚本所用类和接口的位置。 需要注意的是,此处我们未使用 use 语句来声明脚本所用类的全名。这是因为脚本和类位于同一个命名空间 [Application] 中,因此无需此操作;
  • 第 18-22 行:脚本中使用的文本文件名称;
  • 第 24-29 行:创建一个 [ImpotsWithTaxAdminDataInJsonFile] 对象,并处理可能出现的异常;
  • 第 28 行:调用 [executeBatchImpots] 方法,该方法将对文件 [TAXPAYERSDATA_FILENAME] 中的所有纳税人进行税款计算。 结果将写入文件 [RESULTS_FILENAME],可能出现的错误将写入文件 [ERRORS_FILENAME]
  • 第29-32行:若发生无法恢复的错误,则显示错误信息;

结果

使用以下纳税人文件 [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}