Skip to content

13. 应用练习 – 第 5 版

Image

我们已经编写了该练习的多个版本。最新版本采用了分层架构:

Image

[dao] 层实现了 [InterfaceDao] 接口。我们构建了一个实现该接口的类:

  • [DaoImpotsWithTaxAdminDataInJsonFile],该类从文件 jSON 中获取税务数据;

我们将通过一个新类 [DaoImpotsWithTaxAdminDataInDatabase] 来实现 [InterfaceDao] 接口,该类将从数据库 MySQL 中获取税务部门的数据。

13.1. 创建数据库 [dbimpots-2019]

参照上文链接中的示例,我们创建一个名为 [dbimpots-2019] 的数据库,其所有者为 [admimpots],密码为 [mdpimpots]

Image

  • 在上文的 [1-4] 中,我们可以看到数据库 [dbimpots-2019],目前该数据库尚无表;

Image

  • 在上方的 [1-5] 中,我们可以看到用户 [admimpots] 对数据库 [dbimpots-2019] 拥有所有权限。 这里未显示的是,该用户的密码为 [admimpots]

现在我们创建表 [tbtranches],该表将存储税率区间:

Image

  • [1-7] 中,我们创建了一个名为 [tbtranches] 的表,该表包含 4 列;

Image

  • [3-6] 中,我们定义了一列名为 [id] (3),类型为整数 [int] (4),该列将作为表的主键 [6],并由 [5] 通过 SGBD 进行自增。 这意味着 MySQL 将在插入操作时自行管理主键的值。它会将值 1 赋予首次插入的主键,然后将值 2 赋予下一次插入的主键,依此类推……;
  • [7] 步骤中,向导提供了其他主键配置选项。此处我们只需确认 [7] 中的默认值即可;

Image

  • [8-16] 步骤中,定义表中的另外三列:
    • [limites] (8) 类型为十进制数 (9),共 10 位(含 2 位小数 (10)),将存储税级第 17 列的元素;
    • [coeffR] (11) 类型为十进制数 (12),共 6 位,其中包含 2 位小数 (13),将包含税率区间第 18 列的元素;
    • [coeffN] (14) 类型为十进制数 (15),共 10 位,其中包含 2 位小数 (16),将包含税率区间第 19 列的元素;

验证此结构后,我们得到以下结果:

Image

  • [5] 中,主键图标表明 [id] 列为主键。我们还可以看到,该主键的取值为整数(6),且由 MySQL 进行管理(自动递增);

与创建表 [tbtranches] 相同,我们构建表 [tbconstantes],该表将包含税额计算的常量:

Image

可以将数据库结构导出为文本文件,格式为一组命令 SQL:

Image

此处的选项 [5] 仅导出数据库结构,而不导出其内容。在本例中,数据库尚无内容。

Image

Image

Image

选项 [11] 生成以下文件:SQL [dbimpots-2019.sql]


-- phpMyAdmin SQL 转储
-- 版本 4.8.5
-- https://www.phpmyadmin.net/
--
-- 主机:localhost:3306
-- 生成时间:2019年6月30日 01:10PM
-- 服务器版本:5.7.24
-- PHP 版本:7.2.11

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;

--
-- 数据库:`dbimpots-2019`
--
CREATE DATABASE IF NOT EXISTS `dbimpots-2019` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `dbimpots-2019`;

-- --------------------------------------------------------

--
-- 表 `tbconstantes` 的表结构
--

DROP TABLE IF EXISTS `tbconstantes`;
CREATE TABLE `tbconstantes` (
  `id` int(11) NOT NULL,
  `plafondQfDemiPart` decimal(10,2) NOT NULL,
  `plafondRevenusCelibatairePourReduction` decimal(10,2) NOT NULL,
  `plafondRevenusCouplePourReduction` decimal(10,2) NOT NULL,
  `valeurReducDemiPart` decimal(10,2) NOT NULL,
  `plafondDecoteCelibataire` decimal(10,2) NOT NULL,
  `plafondDecoteCouple` decimal(10,2) NOT NULL,
  `plafondImpotCelibatairePourDecote` decimal(10,2) NOT NULL,
  `plafondImpotCouplePourDecote` decimal(10,2) NOT NULL,
  `abattementDixPourcentMax` decimal(10,2) NOT NULL,
  `abattementDixPourcentMin` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表 `tbtranches` 的表结构
--

DROP TABLE IF EXISTS `tbtranches`;
CREATE TABLE `tbtranches` (
  `id` int(11) NOT NULL,
  `limites` decimal(10,2) NOT NULL,
  `coeffR` decimal(10,2) NOT NULL,
  `coeffN` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- 导出表的索引
--

--
-- 表 `tbconstantes` 的索引
--
ALTER TABLE `tbconstantes`
  ADD PRIMARY KEY (`id`);

--
-- 表 `tbtranches` 的索引
--
ALTER TABLE `tbtranches`
  ADD PRIMARY KEY (`id`);

--
-- 用于已导出表的 AUTO_INCREMENT
--

--
-- 表 `tbconstantes` 的 AUTO_INCREMENT
--
ALTER TABLE `tbconstantes`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

--
-- AUTO_INCREMENT 用于表 `tbtranches`
--
ALTER TABLE `tbtranches`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

如果数据库 [dbimpots-2019] 已损坏或被篡改,您可以使用此文件 SQL 来重建该数据库。 在此无需在重新生成数据库前将其删除,因为脚本 SQL 会自动处理此操作:

Image

Image

13.2. 代码组织

为了更好地展示我们编写的各个 PHP 脚本的作用,我们将代码按文件夹进行组织:

Image

  • [1],版本 05 的概览;
  • [2],应用程序实体,即各层之间交换的实体;
  • [3],应用程序的实用工具;
  • [4] 中,应用程序使用或生成的数据。在此我们决定仅使用 jSON 格式存储文本文件。该格式具有以下优势:
    • 它们被许多工具所识别;
    • 这些工具支持语法高亮。此外,jSON 格式遵循特定规则。若未遵守这些规则,工具会发出提示。 例如,在基础文本文件中难以察觉的一个错误是使用大写/小写字母 O 代替数字 0。若发生此类错误,系统将予以提示。实际上,在代码 jSON 中:

"plafondRevenusCouplePourReduction": 42O74

其中在 [42074] 中无意间将零写成了大写字母 O,NetBeans 会报告此错误:

Image

实际上,NetBeans 识别出了大写字母 O,这使得 [49O74] 被视为字符串。因此它推断语法应为 [4-5]:字符串 [47O74] 应被引号包围。 因此,开发者的注意力会被该错误吸引,并可进行修正:要么添加引号,要么将字母 O 替换为数字 0;

05 版本的其他元素如下:

Image

  • [6] 中,[Dao] 层的接口和类;
  • [7] 中,[métier] 层的接口和类;
  • [8] 中,包含 05 版本的主要脚本;

版本 05 有两个明确的目标:

  • 将文件 jSON [Data/txadmindata.json] 的内容填入数据库 MySQL [dbimpots-2019]
  • 利用来自数据库 MySQL [dbimpots-2019] 的税务数据实现税款计算;

我们将分别处理这两个目标。

13.3. 填充数据库 [dbimpots-2019]

13.3.1. 目标

文本文件 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
}

我们的目标是将这些数据导入先前创建的 MySQL [dbimpots-2019] 数据库中。

13.3.2. 实体

Image

实体 [Database] 将用于封装以下 jSON [database.json] 文件的数据:


{
    "dsn": "mysql:host=localhost;dbname=dbimpots-2019",
    "id": "admimpots",
    "pwd": "mdpimpots",
    "tableTranches": "tbtranches",
    "colLimites": "limites",
    "colCoeffR": "coeffr",
    "colCoeffN": "coeffn",
    "tableConstantes": "tbconstantes",
    "colPlafondQfDemiPart": "plafondQfDemiPart",
    "colPlafondRevenusCelibatairePourReduction": "plafondRevenusCelibatairePourReduction",
    "colPlafondRevenusCouplePourReduction": "plafondRevenusCouplePourReduction",
    "colValeurReducDemiPart": "valeurReducDemiPart",
    "colPlafondDecoteCelibataire": "plafondDecoteCelibataire",
    "colPlafondDecoteCouple": "plafondDecoteCouple",
    "colPlafondImpotCelibatairePourDecote": "plafondImpotCelibatairePourDecote",
    "colPlafondImpotCouplePourDecote": "plafondImpotCouplePourDecote",
    "colAbattementDixPourcentMax": "abattementDixPourcentMax",
    "colAbattementDixPourcentMin": "abattementDixPourcentMin"
}

实体 [TaxAdminData] 将用于封装以下 jSON 和 [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
}

实体 [TaxPayerData] 将用于封装以下 jSON 和 [taxpayerdata.json] 文件的数据:


[
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 55555
    },
    {
        "marié": "ouix",
        "enfants": "2x",
        "salaire": "55555x"
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": 50000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 50000
    },
    {
        "marié": "non",
        "enfants": 2,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 5,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 30000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 200000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 20000
    }
]

13.3.2.1. 基类 [BaseEntity]

为简化实体代码,我们将采用以下规则:实体的属性名称应与该实体需封装的 jSON 文件中的属性名称保持一致。 根据此规则,[Database, TaxAdminData, TaxPayerData] 类具有可归纳到父类中的共同点。该父类即为以下 [BaseEntity] 类:


<?php

namespace Application;

class BaseEntity {
  // 属性
  protected $arrayOfAttributes;

  // 从文件初始化jSON
  public function setFromJsonFile(string $jsonFilename) {
    // 从税务数据文件中读取内容
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // 错误?
    if (!$fileContents) {
      // 记录错误
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // 从配置文件中获取代码 jSON 并将其存入关联数组
      $this->arrayOfAttributes = \json_decode($fileContents, true);
      // 错误?
      if ($this->arrayOfAttributes === FALSE) {
        // 记录错误
        $erreur = TRUE;
        $message = "Le fichier de données jSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // 错误?
    if ($erreur) {
      // 抛出异常
      throw new ExceptionImpots($message);
    }
    // 初始化类属性
    foreach ($this->arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // 返回对象
    return $this;
  }

  public function checkForAllAttributes() {
    // 验证所有键是否已初始化
    foreach (\array_keys($this->arrayOfAttributes) as $key) {
      if ($key !== "arrayOfAttributes" && !isset($this->$key)) {
        throw new ExceptionImpots("L'attribut [$key] de la classe "
          . get_class($this) . " n'a pas été initialisé");
      }
    }
  }

  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // 初始化类的某些属性
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // 返回对象
    return $this;
  }

  // toString
  public function __toString() {
    // 对象的属性
    $arrayOfAttributes = \get_object_vars($this);
    // 移除父类的属性
    unset($arrayOfAttributes["arrayOfAttributes"]);
    // 对象的 JSON 字符串
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }

  // getter
  public function getArrayOfAttributes() {
    return $this->arrayOfAttributes;
  }

}

注释

  • 第 5 行:类 [BaseEntity] 旨在被类 [Database, TaxAdminData, TaxPayerData] 继承;
  • 第 7 行:属性 [$arrayOfAttributes] 是一个数组,包含所有继承自 [BaseEntity] 的子类的属性及其值;
  • 第 9-41 行:属性 [$arrayOfAttributes] 根据作为参数传递的文件 jSON [$jsonFilename] 进行初始化。 如果无法读取文件 jSON,或者该文件不是有效的 jSON 文件,则会抛出类型为 [ExceptionImpot] 的异常;
  • 第 36-38 行:此处为特殊代码,仅在由子类执行时生效。 在此情况下,[$this] 代表子类 [Database, TaxAdminData, TaxPayerData] 的实例,此时第 36-38 行将初始化该子类的属性,前提是这些属性具有 protected(或 public)的可见性(参见链接段落)。 实际上,我们曾提到 [Database, TaxAdminData, TaxPayerData] 实体的属性与它们所封装的 jSON 文件中的属性是相同的。 最后,[setFromJsonFile]方法允许子类基于jSON文件进行初始化;
  • 第40行:如果子类调用了方法[setFromJsonFile],则返回对象[$this],即子类的实例;
  • 第43-51行:方法[checkForAllAttributes]允许子类检查其所有属性是否已初始化。若未初始化,则抛出异常[ExceptionImpots]。 该方法允许子类验证其 jSON 文件是否遗漏了某些属性;
  • 第 53-60 行:方法 [setFromArrayOfAttributes] 允许子类根据一个关联数组初始化其全部或部分属性,该数组的键名与待初始化的子类属性名称相同;
  • 第 63-70 行:方法 [__toString] 允许获取子类的 jSON 表示形式;

13.3.2.2. 实体 [Database]

实体 [Database] 如下所示:


<?php

namespace Application;

class Database extends BaseEntity {
  // 属性
  protected $dsn;
  protected $id;
  protected $pwd;
  protected $tableTranches;
  protected $colLimites;
  protected $colCoeffR;
  protected $colCoeffN;
  protected $tableConstantes;
  protected $colPlafondQfDemiPart;
  protected $colPlafondRevenusCelibatairePourReduction;
  protected $colPlafondRevenusCouplePourReduction;
  protected $colValeurReducDemiPart;
  protected $colPlafondDecoteCelibataire;
  protected $colPlafondDecoteCouple;
  protected $colPlafondImpotCelibatairePourDecote;
  protected $colPlafondImpotCouplePourDecote;
  protected $colAbattementDixPourcentMax;
  protected $colAbattementDixPourcentMin;



}

[Database] 用于封装以下 jSON 和 [database.json] 文件的数据:


{
    "dsn": "mysql:host=localhost;dbname=dbimpots-2019",
    "id": "admimpots",
    "pwd": "mdpimpots",
    "tableTranches": "tbtranches",
    "colLimites": "limites",
    "colCoeffR": "coeffr",
    "colCoeffN": "coeffn",
    "tableConstantes": "tbconstantes",
    "colPlafondQfDemiPart": "plafondQfDemiPart",
    "colPlafondRevenusCelibatairePourReduction": "plafondRevenusCelibatairePourReduction",
    "colPlafondRevenusCouplePourReduction": "plafondRevenusCouplePourReduction",
    "colValeurReducDemiPart": "valeurReducDemiPart",
    "colPlafondDecoteCelibataire": "plafondDecoteCelibataire",
    "colPlafondDecoteCouple": "plafondDecoteCouple",
    "colPlafondImpotCelibatairePourDecote": "plafondImpotCelibatairePourDecote",
    "colPlafondImpotCouplePourDecote": "plafondImpotCouplePourDecote",
    "colAbattementDixPourcentMax": "abattementDixPourcentMax",
    "colAbattementDixPourcentMin": "abattementDixPourcentMin"
}

该类与文件 jSON 具有相同的属性。这些属性描述了数据库 MySQL [dbimpots-2019] 的特征:

dsn
数据库名称 DSN
id
数据库所有者
pwd
其密码
tableTranches
包含税率区间的表名
colLimites
colCoeffR
colCoeffN
表的列名[tableTranches]
tableConstantes
包含税额计算常数的表名
colPlafondQfDemiPart
colPlafondRevenusCelibatairePourReduction
colPlafondRevenusCouplePourReduction
colValeurReducDemiPart
colPlafondDecoteCelibataire
colPlafondDecoteCouple
colPlafondImpotCelibatairePourDecote
colPlafondImpotCouplePourDecote
colAbattementDixPourcentMax
colAbattementDixPourcentMin
包含税额计算常数的表 [tableConstantes] 的列名

既然已经知道表和列的名称,且这些名称不会改变,为何还要为它们命名? 在 SGBD 和 MySQL 之后,我们将使用 SGBD 和 PostgreSQL 来存储税务管理数据。 然而,Postgres 中的列名和表名并不遵循与 MySQL 相同的命名规则。 因此我们不得不使用其他名称。其他 SGBD 也是如此。如果希望代码在 SGBD 之间具有可移植性,那么最好使用参数,而不是硬编码的表名和列名。

让我们回到 [Database] 类的代码:


<?php

namespace Application;

class Database extends BaseEntity {
  // 属性
  protected $dsn;
  protected $id;
  protected $pwd;
  protected $tableTranches;
  protected $colLimites;
  protected $colCoeffR;
  protected $colCoeffN;
  protected $tableConstantes;
  protected $colPlafondQfDemiPart;
  protected $colPlafondRevenusCelibatairePourReduction;
  protected $colPlafondRevenusCouplePourReduction;
  protected $colValeurReducDemiPart;
  protected $colPlafondDecoteCelibataire;
  protected $colPlafondDecoteCouple;
  protected $colPlafondImpotCelibatairePourDecote;
  protected $colPlafondImpotCouplePourDecote;
  protected $colAbattementDixPourcentMax;
  protected $colAbattementDixPourcentMin;

  // setter
  // 初始化
  public function setFromJsonFile(string $jsonFilename) {
    // 父类
    parent::setFromJsonFile($jsonFilename);
    // 检查所有属性是否已初始化
    parent::checkForAllAttributes();
    // 返回对象
    return $this;
  }

  // 获取器和设置器
  public function getDsn() {
    return $this->dsn;
  }



  public function setDsn($dsn) {
    $this->dsn = $dsn;
    return $this;
  }



}

注释

  • 第 7-24 行:该类的所有属性均具有 [protected] 的可见性。这是使其能够从父类 [BaseEntity] 进行修改的必要条件(参见链接段落);
  • 第28-35行:方法[setFromJsonFile]允许根据作为参数传递的文件jSON的内容初始化类[Database]的属性。 文件 jSON 的属性与类 [Database] 的属性必须完全一致。若文件 jSON 无法使用,则会抛出异常;
  • 第 30 行:由父类执行初始化;
  • 第 32 行:要求父类验证 [Database] 类的所有属性是否已初始化。若未初始化,则抛出异常;
  • 第 34 行:返回刚刚初始化的 [Database] 实例;
  • 第 37 行及之后:类的属性 getter 和 setter 方法;

13.3.2.3. 实体 [TaxAdminData]

实体 [TaxAdminData] 如下所示:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // 税率区间
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // 税额计算常量
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  
}

[TaxAdminData] 用于封装以下 jSON 和 [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
}

该类与文件 jSON 具有相同的属性。这些属性代表税务管理数据。类 [TaxAdminData] 的其余代码如下:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // 税率区间
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // 税额计算常数
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // 初始化
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // 父级
    parent::setFromJsonFile($taxAdminDataFilename);
    // 检查所有属性是否已初始化
    parent::checkForAllAttributes();
    // 验证属性值是否为大于等于0的实数
    foreach ($this as $key => $value) {
      if ($key !== "arrayOfAttributes") {
        // $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;
  }

  protected 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;
  }

  // 获取器和设置器

}

注释

  • 第 23 行:方法 [setFromJsonFile] 用于根据作为参数传递的文件 jSON 初始化类 [TaxAdminData] 的属性。 文件 jSON 中的属性必须在类中以相同名称存在;
  • 第 25 行:由父类执行此操作;
  • 第27行:要求父类验证子类的所有属性是否已初始化;
  • 第29-42行:在本地验证所有属性是否已赋值为正实数或零。此验证已在版本03的“链接”章节中讨论过;

13.3.3. [dao]

现在我们可以编写代码,将文本文件 [taxadmindata.json] 中的数据导入数据库 MySQL 中的表 [tbtranches, tbconstantes][dbimpots-2019]。我们将采用以下架构:

Image

Image

[dao]层将实现以下[InterfaceDao4TransferAdminDataFromFile2Database]接口:


<?php

// 命名空间
namespace Application;

interface InterfaceDao4TransferAdminData2Database {

  public function transferAdminData2Database(): void;
}

注释

  • 第 8 行:方法 [transferAdminData2Database] 的作用是将税务管理数据存储在数据库中;

接口 [InterfaceDao4TransferAdminData2Database] 将由以下类 [DaoTransferAdminDataFromJsonFile2Database] 实现:


<?php

// 命名空间
namespace Application;

// 类定义TransferAdminDataFromFile2DatabaseDao
class DaoTransferAdminDataFromJsonFile2Database implements InterfaceDao4TransferAdminData2Database {
  // 目标数据库属性
  private $database;
  // 税务管理数据
  private $taxAdminData;

  // 制造商
  public function __construct(string $databaseFilename, string $taxAdminDataFilename) {
    // 存储数据库配置
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // 存储税务数据
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
  }

  // 从文本文件中导入税率区间数据
  // 至数据库
  public function transferAdminData2Database(): void {
    // 在数据库中进行操作
    $database = $this->database;
    try {
      // 建立数据库连接
      $connexion = new \PDO($database->getDsn(), $database->getId(), $database->getPwd());
      // 希望每次 SGBD 出现错误时,都抛出一个异常
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // 启动事务
      $connexion->beginTransaction();
      // 填充税率区间表
      $this->fillTableTranches($connexion);
      // 填充常量表
      $this->fillTableConstantes($connexion);
      // 交易成功结束
      $connexion->commit();
    } catch (\PDOException $ex) {
      // 是否有正在进行的交易?
      if (isset($connexion) && $connexion->inTransaction()) {
        // 因失败结束事务
        $connexion->rollBack();
      }
      // 将异常上报给调用方
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // 关闭连接
      $connexion = NULL;
    }
  }


  // 填充税率区间表
  private function fillTableTranches($connexion): void {

  }

  // 填充常量表
  private function fillTableConstantes($connexion): void {

  }

}

注释

此处运用了我们在关于 MySQL 的章节中所学的内容。

  • 第 7 行:类 [DaoTransferAdminDataFromJsonFile2Database] 实现了接口 [InterfaceDao4TransferAdminData2Database]
  • 第 9 行:属性 [$database] 是类型为 [Database] 的对象,该对象封装了文件 [database.json] 中的数据;
  • 第 11 行:属性 [$taxAdminData] 是类型为 [TaxAdminData] 的对象,该对象封装了文件 [taxadmindata.json] 的数据;
  • 第 14-19 行:构造函数接收 [database.json, taxadmindata.json] 文件名的参数;
  • 第 16 行:初始化属性 [$database]
  • 第 18 行:初始化属性 [$taxAdminData]
  • 第 23 行:实现接口的唯一方法 [InterfaceDao4TransferAdminData2Database]
  • 第26-38行:分两步填充表[tbtranches, tbconstantes]
    • 第 34 行:首先填充表 [tbtranches]。此操作在事务内进行(第 32、38 行)。 方法 [fillTableTranches](第 55 行)一旦出现异常即会抛出异常。此时,执行流程将转至第 39-50 行的 catch / finally 块;
    • 第36行:通过方法[fillTableConstantes](第60行)以相同方式填充表[tbconstantes]
  • 第39-47行:处理代码抛出异常的情况;
  • 第41-44行:若存在事务,则将其回滚;
  • 第46行:抛出类型为[ExceptionImpots]的异常,并携带原始异常的消息(该原始异常的类型不限);
  • 第47-50行:在[finally]语句中,关闭连接;

方法 [fillTableTranches] 的代码如下:


private function fillTableTranches($connexion): void {
    // 数据库快捷方式
    $database = $this->database;
    // 需插入数据库的数据
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // 清空表(以防其中已有数据)
    $statement = $connexion->prepare("delete from " . $database->getTableTranches());
    $statement->execute();
    // 准备插入操作
    $sqlInsert = "insert into {$database->getTableTranches()} "
      . "({$database->getColLimites()}, {$database->getColCoeffR()},"
      . " {$database->getColCoeffN()}) values (:limites, :coeffR, :coeffN)";
    $statement = $connexion->prepare($sqlInsert);
    // 使用各税档的数值执行已准备好的命令
    for ($i = 0; $i < count($limites); $i++) {
      $statement->execute([
        "limites" => $limites[$i],
        "coeffR" => $coeffR[$i],
        "coeffN" => $coeffN[$i]]);
    }
  }

注释

  • 第 1 行:方法 [fillTableTranches] 接收一个已打开的连接作为参数。此外,我们知道该连接中已启动了一项事务;
  • 第 5-7 行:要插入表中的值由属性 [$taxAdminData] 提供;
  • 第 9-10 行:清空表 [tbtranches] 的当前内容;
  • 第12-15行:准备向表中插入行。此处使用属性[$database]提供的列名;
  • 第17-22行:根据需要多次执行第12-15行准备好的插入语句;

方法 [fillTableConstantes] 的代码如下:


private function fillTableConstantes($connexion): void {
    // 快捷方式
    $database = $this->database;
    // 清空表,以防其中有数据
    $statement = $connexion->prepare("delete from {$database->getTableConstantes()}");
    $statement->execute();
    // 准备插入操作
    $taxAdminData = $this->taxAdminData;
    $sqlInsert = "insert into {$database->getTableConstantes()}"
      . " ({$database->getColPlafondQfDemiPart()},"
      . " {$database->getColPlafondRevenusCelibatairePourReduction()},"
      . " {$database->getColPlafondRevenusCouplePourReduction()},"
      . " {$database->getColValeurReducDemiPart()},"
      . " {$database->getColPlafondDecoteCelibataire()},"
      . " {$database->getColPlafondDecoteCouple()},"
      . " {$database->getColPlafondImpotCelibatairePourDecote()},"
      . " {$database->getColPlafondImpotCouplePourDecote()},"
      . " {$database->getColAbattementDixPourcentMax()},"
      . " {$database->getColAbattementDixPourcentMin()})"
      . " values ("
      . ":plafondQfDemiPart,"
      . ":plafondRevenusCelibatairePourReduction,"
      . ":plafondRevenusCouplePourReduction,"
      . ":valeurReducDemiPart,"
      . ":plafondDecoteCelibataire,"
      . ":plafondDecoteCouple,"
      . ":plafondImpotCelibatairePourDecote,"
      . ":plafondImpotCouplePourDecote,"
      . ":abattementDixPourcentMax,"
      . ":abattementDixPourcentMin)";
    $statement = $connexion->prepare($sqlInsert);
    // 执行预编译语句
    $statement->execute([
      "plafondQfDemiPart" => $taxAdminData->getPlafondQfDemiPart(),
      "plafondRevenusCelibatairePourReduction" => $taxAdminData->getPlafondRevenusCelibatairePourReduction(),
      "plafondRevenusCouplePourReduction" => $taxAdminData->getPlafondRevenusCouplePourReduction(),
      "valeurReducDemiPart" => $taxAdminData->getValeurReducDemiPart(),
      "plafondDecoteCelibataire" => $taxAdminData->getPlafondDecoteCelibataire(),
      "plafondDecoteCouple" => $taxAdminData->getPlafondDecoteCouple(),
      "plafondImpotCelibatairePourDecote" => $taxAdminData->getPlafondImpotCelibatairePourDecote(),
      "plafondImpotCouplePourDecote" => $taxAdminData->getPlafondImpotCouplePourDecote(),
      "abattementDixPourcentMax" => $taxAdminData->getAbattementDixPourcentMax(),
      "abattementDixPourcentMin" => $taxAdminData->getAbattementDixPourcentMin()
    ]);
  }

注释

  • 第 1 行:方法 [fillTableConstantes] 接收一个已打开的连接作为参数。此外,已知该连接中已启动一个事务;
  • 第 5-6 行:清空表 [tbconstantes]
  • 第 9-31 行:准备插入语句 SQL。该操作较为复杂,因为此插入操作中有 10 个字段需要初始化,且需从属性 [$database] 中获取字段名称;
  • 第33-44行:执行插入命令。仅需插入一行数据。此处代码同样因需从属性[$taxAdminData]中获取待插入值而变得复杂;

13.3.4. 主脚本

Image

Image

主脚本基于 [dao] 层进行数据传输:


<?php

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

// 命名空间
namespace Application;

// 通过 PHP 进行错误处理
// ini_set("display_errors", "0");
// 接口和类的引入
require_once __DIR__ . "/../Entities/BaseEntity.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao.php";
require_once __DIR__ . "/../Dao/TraitDao.php";
require_once __DIR__ . "/../Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../Dao/DaoTransferAdminDataFromJsonFile2Database.php";
//
// 常量定义
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";

//
try {
  // 创建 [dao] 层
  $dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
  // 将数据传输到数据库
  $dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
  // 显示错误
  print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// 结束
print "Terminé\n";
exit;

注释

  • 第 12-21 行:加载应用程序的类和接口;
  • 第24-24行:两个文件jSON;
  • 第 30 行:通过向构造函数传递两个文件 jSON 来实例化 [dao] 层;
  • 第 32 行:执行数据传输;

执行此代码后,数据库中将得到以下结果:

Image

[3] 列中,可以看到 MySQL 为主键 [id] 赋予的值。编号从 1 开始。上图是多次执行脚本后获得的屏幕截图。

Image

Image

13.4. 税款计算

Image

13.4.1. 架构

税款计算应用程序的 04 版采用分层架构:

Image

[dao]实现了[InterfaceDao]接口。我们构建了一个实现该接口的类:

  • [DaoImpotsWithTaxAdminDataInJsonFile],该类会从文件 jSON 中获取税务数据。这是第 04 版;

我们将通过一个新的类 [DaoImpotsWithTaxAdminDataInDatabase] 来实现接口 [InterfaceDao],该类将从数据库 MySQL 中获取税务部门的数据。 [dao] 层与之前一样,将把结果和错误写入文本文件,并从文本文件中读取纳税人数据。只是这次,这些文本文件将是 jSON 文件。 此外,我们知道,只要继续遵循 [InterfaceDao] 接口,[métier] 层就无需修改。

Image

13.4.2. 实体 [TaxPayerData]

Image

[TaxPayerData] 用于将以下 jSON 和 [taxpayersdata.json] 文件的数据封装到一个类中:


[
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 55555
    },
    {
        "marié": "ouix",
        "enfants": "2x",
        "salaire": "55555x"
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": 50000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 50000
    },
    {
        "marié": "non",
        "enfants": 2,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 5,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 30000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 200000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 20000
    }
]

[TaxPayerData] 类如下:


<?php

// 命名空间
namespace Application;

// 数据类
class TaxPayerData extends BaseEntity {
  // 计算纳税人税款所需的数据
  protected $marié;
  protected $enfants;
  protected $salaire;
  // 税款计算结果
  protected $impôt;
  protected $surcôte;
  protected $décôte;
  protected $réduction;
  protected $taux;

  // 获取器和设置器

}

注释

  • 第 7 行:类 [TaxPayerData] 继承自类 [BaseEntity]。由于其父类的方法已足够,因此类 [TaxPayerData] 本身未定义任何方法。 需要指出的是,类 [TaxPayerData] 的属性与文件 jSON [taxpayersdata.json] 中的属性完全一致;

13.4.3. [dao]

13.4.3.1. [TraitDao] 特性

特征 [TraitDao] 实现了接口 [InterfaceDao] 的一部分。让我们回顾一下该接口:


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

[TraitDao] 类实现了 [InterfaceDao] 接口中的 [getTaxPayersData, saveResults] 方法。 由于在 04 版和 05 版之间,[TaxPayerData] 实体的定义发生了变化,因此我们需要重新检查 [TraitDao] 的代码:


<?php

// 命名空间
namespace Application;

trait TraitDao {

  // 读取纳税人数据
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
    // 将纳税人数据读取到表中
    $baseEntity = new BaseEntity();
    $baseEntity->setFromJsonFile($taxPayersFilename);
    $arrayOfAttributes = $baseEntity->getArrayOfAttributes();
    // 纳税人数据表
    $taxPayersData = [];
    // 错误数组
    $errors = [];
    // 遍历 [TaxPayerData] 类型的元素属性数组
    $i = 0;
    foreach ($arrayOfAttributes as $attributesOfTaxPayerData) {
      // 验证
      $error = $this->check($attributesOfTaxPayerData);
      if (!$error) {
        // 一名纳税人
        $taxPayersData[] = (new TaxPayerData())->setFrOmArrayOfAttributes($attributesOfTaxPayerData);
      } else {
        // + 错误 - 记录无效数据的编号
        $error = ["numéro" => $i] + $error;
        $errors[] = $error;
      }
      // 下一条
      $i++;
    }
    // 将错误保存到 json 文件中
    $string = "";
    foreach ($errors as $error) {
      $string .= \json_encode($error, JSON_UNESCAPED_UNICODE) . "\n";
    }
    $this->saveString($errorsFilename, $string);
    // 函数结果
    return $taxPayersData;
  }

  private function check(array $attributesOfTaxPayerData): array {
    // 验证 [$taxPayerData] 中的数据
    // 错误属性的列表
    $attributes = [];
    // 婚姻状况必须为“是”或“否”
    $marié = trim(strtolower($attributesOfTaxPayerData["marié"]));
    $erreur = ($marié !== "oui" and $marié !== "non");
    if ($erreur) {
      // 记录该错误
      $attributes[] = ["marié" => $marié];
    }
    // 子节点数量必须为正整数或零
    $enfants = trim($attributesOfTaxPayerData["enfants"]);
    if (!preg_match("/^\d+$/", $enfants)) {
      // 记录错误
      $erreur = TRUE;
      $attributes[] = ["enfants" => $enfants];
    } else {
      $enfants = (int) $enfants;
    }

    // 工资必须为正整数或零(不包含欧元分)
    $salaire = trim($attributesOfTaxPayerData["salaire"]);
    if (!preg_match("/^\d+$/", $salaire)) {
      // 记录错误
      $erreur = TRUE;
      $attributes[] = ["salaire" => $salaire];
    } else {
      $salaire = (int) $salaire;
    }

    // 错误?
    if ($erreur) {
      // 返回错误
      return ["erreurs" => $attributes];
    } else {
      // 返回无错误
      return [];
    }
  }

  // 记录结果
  public function saveResults(string $resultsFilename, array $taxPayersData): void {
    // 将表格 [$taxPayersData] 保存到文本文件 [$resultsFileName] 中
    // 如果文本文件 [$resultsFileName] 不存在,则创建该文件
    // 构建结果字符串 jSON
    $string = "[" . implode(",
", $taxPayersData) . "]";
    // 将该字符串保存
    $this->saveString($resultsFilename, $string);
  }

  // 将数组结果写入文本文件
  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]");
    }
  }

}

注释

  • [TraitDao] 实现了接口 [InterfaceDao] 中的方法 [getTaxPayersData](第 9 行)和 [saveResults](第 86 行);
  • 第 9 行:方法 [getTaxPayersData] 接收的参数为:
    • [$taxPayersFilename]:纳税人数据文件 jSON 的名称;
    • [$errorsFilename]:错误文件的名称 jSON [errors.json]
  • 第 11-13 行:纳税人数据文件 jSON 的内容被传输到关联数组 [$arrayOfAttributes] 中。 如果文件 jSON 无法处理,则抛出异常 [ExceptionImpots]
  • 第 15 行:数组 [$taxPayersData] 将包含封装在 [TaxPayerData] 类型对象中的纳税人数据;
  • 第17行:将错误累积到数组[$errors]中;
  • 第99-33行:构建表[$taxPayersData]
  • 第22行:在将数据封装为[TaxPayerData]类型之前,会先对数据进行验证。[check]方法返回:
    • 如果数据不正确,则返回一个包含错误属性的数组 [‘erreurs’=>[…]]
    • 若数据正确,则返回一个空数组;
  • 第 25 行:数据有效的情况。构建一个新的 [TaxPayerData] 对象并将其添加到 [$taxPayersData] 数组中;
  • 第26-30行:数据无效的情况。 在错误记录中,标注出jSON文件中出错的[TaxPayerData]对象编号,以便用户定位,随后将该错误添加到[$errors]数组中;
  • 第35-39行:将遇到的错误记录到作为参数传递的jSON文件中([$errorsFilename],第9行);
  • 第 41 行:返回已构建的 [TaxPayerData] 对象数组:这是该方法的目标;
  • 第44-83行:私有方法[check]验证第44行作为参数传递的数组[$attributesOfTaxPayerData]中参数[marié, enfants, salaire]的有效性。 如果存在错误的属性,它会将其累积到数组 [$attributes] 中(第 47、53、60、70 行),形式为数组 [‘attribut erroné’=> valeur de l’attribut erroné]
  • 第 78 行:若存在错误,则返回数组 [‘erreurs’=>$attributes]
  • 第 81 行:若无错误,则返回一个空的错误数组;
  • 第86-93行:实现接口[InterfaceDao]中的方法[saveResults]
  • 第 90 行:构建字符串 jSON,将其写入第 86 行作为参数传递的文件 jSON [$resultsFilename]。必须从数组中构建字符串 jSON:
    • 数组中的每个元素与下一个元素之间以逗号和换行符分隔;
    • 整个数组用方括号 [] 包围;
  • 第 92 行:字符串 jSON 被写入文件 jSON [$resultsFilename]

13.4.3.2. 类 [DaoImpotsWithTaxAdminDataInDatabase]

[DaoImpotsWithTaxAdminDataInDatabase] 以如下方式实现了接口 [InterfaceDao]


<?php

// 命名空间
namespace Application;

// 定义类 ImpotsWithDataInDatabase
class DaoImpotsWithTaxAdminDataInDatabase implements InterfaceDao {
  // 使用一个特征
  use TraitDao;
  // 类型为 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;
  }

}

注释

  • 第 4 行:保留 [dao] 层其他实现已使用的命名空间;
  • 第 7 行:类 [DaoImpotsWithTaxAdminDataInDatabase] 实现了接口 [InterfaceDao]
  • 第 9 行:导入特征 [TraitDao]。已知该特征实现了接口的一部分。 唯一需要实现的方法是第62-64行中的[getTaxAdminData]方法。该方法仅将第11行的私有属性[taxAdminData]设为返回值。由此可推断,构造函数必须初始化该属性。这是它的唯一作用;
  • 第 16 : 构造函数接收的唯一参数是 [$databaseFilename],该参数是定义数据库的文件 jSON [database.json] 的名称 ;
  • 第 18 行:使用文件 jSON [$databaseFilename] 创建类型为 [Database] 的对象,该对象已构建并存储在第 13 行的属性 [$database] 中。 如果无法正确处理文件 jSON,则抛出异常 [ExceptionImpots]
  • 第 20 行:创建对象 [$this→taxAdminData],该对象需由构造函数进行初始化;
  • 第22-26行:建立与数据库的连接。请注意,[\PDO] 这一命名用于表示 [PDO] 类的 PHP 实例。 实际上,由于当前处于 [Application] 命名空间中如果仅写 [PDO],该相对名称将被当前命名空间作为前缀,从而指向不存在的类 [Application\PDO]
  • 第 28 行:发生错误时,SGBD 将调用 \PDOException(第 37 行);
  • 第 30 行:启动事务。此操作实际并无必要,因为仅会执行两个 SQL 命令,且这些命令不会修改数据库。但仍需执行此操作以隔离其他数据库用户;
  • 第 32 行:通过第 52 行中的私有方法 [getTranches] 读取税率区间表 [tbtranches]
  • 第34行:通过第57行的私有方法[getConstantes]读取计算常量表[tbconstantes]
  • 第36行:若执行到此行,说明一切正常。因此确认该事务;
  • 第37-42行:若执行到此处,则表示发生了异常。 因此,若当前有事务正在进行,则回滚该事务(第39-42行)。第44行,为了保持异常的一致性,我们将接收到的异常消息以[ExceptionImpots]类型的异常形式重新抛出;
  • 第45-48行:无论是否发生异常,均关闭连接;

方法 [getTranches] 如下:


private function getTranches($connexion): void {
    // 快捷方式
    $database = $this->database;
    $taxAdminData = $this->taxAdminData;
    // 准备查询SELECT
    $statement = $connexion->prepare(
      "select {$database->getColLimites()}," .
      " {$database->getColCoeffR()}," .
      " {$database->getColCoeffN()}" .
      " from {$database->getTableTranches()}");
    // 使用税率区间值执行已准备的命令
    $statement->execute();
    // 分析结果
    $limites = [];
    $coeffR = [];
    $coeffN = [];
    // 填充三个表格
    while ($tranche = $statement->fetch(\PDO::FETCH_OBJ)) {
      $limites[] = (float) $tranche->{$database->getColLimites()};
      $coeffR[] = (float) $tranche->{$database->getColCoeffR()};
      $coeffN[] = (float) $tranche->{$database->getColCoeffN()};
    }
    // 将数据存储在类中的属性 [$taxAdminData] 中
    $taxAdminData->setFromArrayOfAttributes([
      "limites" => $limites,
      "coeffR" => $coeffR,
      "coeffN" => $coeffN
    ]);
  }

注释

  • 第 1 行:该方法接收参数 [$connexion],这是一个已打开的连接,且其中正在进行事务;
  • 第 2-4 行:创建两个快捷方式,以避免在整个代码中重复编写 [$this->database] [$taxAdminData = $this->taxAdminData]。这里是对象引用的副本,而非对象本身的副本;
  • 第6-10行:准备 SELECT 命令,并在第12行执行;
  • 第13-22行:处理SELECT的结果。接收到的信息被累积到三个数组[limites, coeffR, coeffN]中
  • 第24-28行:使用这三个数组初始化类的[$this->taxAdminData]属性;

私有方法 [getConstantes] 如下:


private function getConstantes($connexion): void {
    // 快捷方式
    $database = $this->database;
    $taxAdminData = $this->taxAdminData;
    // 准备查询 SELECT
    $select = "select {$database->getColPlafondQfDemiPart()}," .
      "{$database->getColPlafondRevenusCelibatairePourReduction()}," .
      "{$database->getColPlafondRevenusCouplePourReduction()}," . "{$database->getColValeurReducDemiPart()}," .
      "{$database->getColPlafondDecoteCelibataire()}," . "{$database->getColPlafondDecoteCouple()}," .
      "{$database->getColPlafondImpotCelibatairePourDecote()}," . "{$database->getColPlafondImpotCouplePourDecote()}," .
      "{$database->getColAbattementDixPourcentMax()}," . "{$database->getColAbattementDixPourcentMin()}" .
      " from {$database->getTableConstantes()}";
    $statement = $connexion->prepare($select);
    // 执行已准备的命令
    $statement->execute();
    // 处理结果 - 此处仅有一行
    $row = $statement->fetch(\PDO::FETCH_OBJ);
    // 初始化属性 [$taxAdminData]
    $taxAdminData->setPlafondQfDemiPart($row->{$database->getColPlafondQfDemiPart()});
    $taxAdminData->setPlafondRevenusCelibatairePourReduction(
      $row->{$database->getColPlafondRevenusCelibatairePourReduction()});
    $taxAdminData->setPlafondRevenusCouplePourReduction($row->{$database->getColPlafondRevenusCouplePourReduction()});
    $taxAdminData->setValeurReducDemiPart($row->{$database->getColValeurReducDemiPart()});
    $taxAdminData->setPlafondDecoteCelibataire($row->{$database->getColPlafondDecoteCelibataire()});
    $taxAdminData->setPlafondDecoteCouple($row->{$database->getColPlafondDecoteCouple()});
    $taxAdminData->setPlafondImpotCelibatairePourDecote($row->{$database->getColPlafondImpotCelibatairePourDecote()});
    $taxAdminData->setPlafondImpotCouplePourDecote($row->{$database->getColPlafondImpotCouplePourDecote()});
    $taxAdminData->setAbattementDixPourcentMax($row->{$database->getColAbattementDixPourcentMax()});
    $taxAdminData->setAbattementDixPourcentMin($row->{$database->getColAbattementDixPourcentMin()});
  }

注释

  • 第 1 行:该方法接收参数 [$connexion],这是一个已打开的连接,且其中正在进行事务;
  • 第 2-4 行:创建两个快捷方式,以避免在整个代码中重复编写 [$this->database] [$taxAdminData = $this->taxAdminData]。这里是对象引用的副本,而非对象本身的副本;
  • 第 6-15 行:准备 SELECT 命令,并在第 15 行执行;
  • 第 17-29 行:处理 SELECT 的结果。获取的信息用于初始化类的 [$this->taxAdminData] 属性;

:需注意该类不依赖于 SGBD 和 MySQL。调用方代码通过数据库中的 DSN 来确定所使用的 SGBD。

13.4.4. [métier]

Image

  • 我们刚刚实现了 [dao] 层 (3);
  • 由于我们遵循了 [InterfaceDao] 接口,[métier] 层(2)理论上可以保持不变。然而,我们不仅修改了 [dao] 层。 我们还修改了所有层共用的实体;

[métier]层实现了以下[InterfaceMetier]接口:


<?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;
}
  • 第 12 行:方法 [executeBatchImpots] 现在使用文件 jSON [$taxPayersFileName],而在 04 版本中,它是一个基本的文本文件。;

在版本 04 中,方法 [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);
  }
  • 第 15 行现已出错。在类 [TaxPayerData] 的新定义中,方法 [setMontant] 已不存在;

在 05 版本中,方法 [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->setFromArrayOfAttributes($result);
      // 将结果填入结果表
      $results [] = $taxPayerData;
    }
    // 保存结果
    $this->dao->saveResults($resultsFileName, $results);
  }

注释

  • 第 15 行:不再使用类 [TaxPayerData] 的各个设置器,而是使用其全局设置器 [setFromArrayOfAttributes]
  • 其余代码无需修改;

13.4.5. 主脚本

Image

  • 我们刚刚实现了 [dao] (3) 和 [métier] (2) 层;
  • 接下来需要编写主脚本 (1);

主脚本与 04 版本类似:


<?php

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

// 命名空间
namespace Application;

// 通过 PHP 进行错误处理
//ini_set("display_errors", "0");
// 接口和类的引入
require_once __DIR__ . "/../Entities/BaseEntity.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao.php";
require_once __DIR__ . "/../Dao/TraitDao.php";
require_once __DIR__ . "/../Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once __DIR__ . "/../Métier/InterfaceMetier.php";
require_once __DIR__ . "/../Métier/Metier.php";
//
// 常量定义
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
const RESULTS_FILENAME = "../Data/resultats.json";
const ERRORS_FILENAME = "../Data/errors.json";
const TAXPAYERSDATA_FILENAME = "../Data/taxpayersdata.json";

try {
  // 创建 [dao] 层
  $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
  // 创建 [métier] 层
  $métier = new Metier($dao);
  // 批处理模式下的税款计算
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // 显示错误
  print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// 结束
print "Terminé\n";
exit;


注释

  • 第 12-22 行:加载 05 版本的所有文件;
  • 第25-29行:应用程序中各个jSON文件的名称;
  • 第 33 行:构建 [dao] 层;
  • 第35行:构建[métier]层;
  • 第 37 行:调用 [métier] 层中的 [executeBatchImpots] 方法;

结果

该应用程序生成两个 jSON 文件:

  • [resultats.json]:各项税费计算结果;
  • [errors.json]:报告在文件 jSON 和 [taxpayersdata.json] 中发现的错误;

文件 [errors.json] 内容如下:


{
    "numéro": 1,
    "erreurs": [
        {
            "marié": "ouix"
        },
        {
            "enfants": "2x"
        },
        {
            "salaire": "55555x"
        }
    ]
}

这意味着在 [taxpayersdata.json] 中,纳税人表的第 1 项有误。文件 [taxpayersdata.json] 内容如下:


[
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 55555
    },
    {
        "marié": "ouix",
        "enfants": "2x",
        "salaire": "55555x"
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": 50000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 50000
    },
    {
        "marié": "non",
        "enfants": 2,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 5,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 30000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 200000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 20000
    }
]

结果文件 [resultats.json] 如下所示:


[
    {
        "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é": "non",
        "enfants": 3,
        "salaire": 100000,
        "impôt": 16782,
        "surcôte": 7176,
        "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é": "oui",
        "enfants": 5,
        "salaire": 100000,
        "impôt": 4230,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.14
    },
    {
        "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": 20000,
        "impôt": 0,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0
    }
]

这些结果与版本 04 的结果一致。

13.5. [Codeception] 测试

正如在第 04 版链接段落中所做的那样,我们将为第 05 版编写测试 [Codeception]

Image

13.5.1. [dao] 层测试

[DaoTest.php] 测试如下:


<?php

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

// 命名空间
namespace Application;

// 根目录
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");

// 接口和类的包含
require_once ROOT . "/Entities/BaseEntity.php";
require_once ROOT . "/Entities/TaxAdminData.php";
require_once ROOT . "/Entities/TaxPayerData.php";
require_once ROOT . "/Entities/Database.php";
require_once ROOT . "/Entities/ExceptionImpots.php";
require_once ROOT . "/Utilities/Utilitaires.php";
require_once ROOT . "/Dao/InterfaceDao.php";
require_once ROOT . "/Dao/TraitDao.php";
require_once ROOT . "/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/Métier/InterfaceMetier.php";
require_once ROOT . "/Métier/Metier.php";
// 第三方库
require_once VENDOR . "/autoload.php";

// 常量定义
const DATABASE_CONFIG_FILENAME = ROOT ."/Data/database.json";
const TAXADMINDATA_FILENAME = ROOT ."/Data/taxadmindata.json";
const RESULTS_FILENAME = ROOT ."/Data/resultats.json";
const ERRORS_FILENAME = ROOT ."/Data/errors.json";
const TAXPAYERSDATA_FILENAME = ROOT ."/Data/taxpayersdata.json";

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

  public function __construct() {
    parent::__construct();
    // 创建 [dao] 层
    $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // 测试
  public function testTaxAdminData() {
    // 计算常量
    $this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());

  }

}

注释

  • 第 9-33 行:定义测试环境。我们使用与链接段落中描述的主脚本 [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] 相同的环境;
  • 第39-44行:构建[dao]层;
  • 第 43 行:[$this→taxAdminData] 属性包含待测试的数据;
  • 第47-51行:方法[testTaxAdminData]即“链接”段落中所述的方法;

测试结果如下:

Image

13.5.2. [métier] 层测试

[MetierTest.php] 测试如下:


<?php

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

// 命名空间
namespace Application;

// 根目录
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");

// 接口和类的包含
require_once ROOT . "/Entities/BaseEntity.php";
require_once ROOT . "/Entities/TaxAdminData.php";
require_once ROOT . "/Entities/TaxPayerData.php";
require_once ROOT . "/Entities/Database.php";
require_once ROOT . "/Entities/ExceptionImpots.php";
require_once ROOT . "/Utilities/Utilitaires.php";
require_once ROOT . "/Dao/InterfaceDao.php";
require_once ROOT . "/Dao/TraitDao.php";
require_once ROOT . "/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/Métier/InterfaceMetier.php";
require_once ROOT . "/Métier/Metier.php";
// 第三方库
require_once VENDOR . "/autoload.php";

// 常量定义
const DATABASE_CONFIG_FILENAME = ROOT ."/Data/database.json";
const TAXADMINDATA_FILENAME = ROOT ."/Data/taxadmindata.json";
const RESULTS_FILENAME = ROOT ."/Data/resultats.json";
const ERRORS_FILENAME = ROOT ."/Data/errors.json";
const TAXPAYERSDATA_FILENAME = ROOT ."/Data/taxpayersdata.json";

class MetierTest extends \Codeception\Test\Unit {
  // 业务层
  private $métier;

  public function __construct() {
    parent::__construct();
    // 创建 [dao] 层
    $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_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 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"]);
  }

}

注释

  • 第 9-33 行:定义测试环境。我们使用与链接段落中描述的主脚本 [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] 相同的环境;
  • 第39-45:构建[dao]和[métier]层;
  • 第 44 行:[$this→métier] 属性引用 [métier] 图层;
  • 第 47-64 行:[test1, test2…, test11] 方法即链接段落中所述的方法;

测试结果如下:

Image