Skip to content

18. 应用练习 – 第 8 版

我们将重新使用示例应用程序 – 版本 5(链接段落),并将其改造成一个客户端/服务器应用程序。

18.1. 简介

第 5 版的架构如下:

Image

  • 名为 [dao] 的层(数据访问对象)负责与数据库 MySQL 及本地文件系统进行交互;
  • 名为 [métier] 的层负责计算税款;
  • 主脚本是总指挥:它实例化 [dao] [métier] 层,然后与 [métier] 层进行交互以完成所需任务;

我们将把该架构迁移至以下客户端/服务器架构:

Image

  • [2] 中,我们将沿用第 5 版的 [dao] 层,并移除其中访问本地文件系统的方法。 这些方法将迁移至客户端 [6, 7][dao] 层;
  • [3] 中, [métier]层将保留第5版的功能,但其[executeBatchImpôts, saveResults]方法将迁移至客户端的[dao]和[7]层;
  • [4] 中,需编写服务器脚本:其需:
    • 创建 [métier][dao][3, 2] 层;
    • 与客户端脚本 [5, 7] 进行交互;
  • [7] 中,需编写客户端层 [dao]
    • 它将成为服务器脚本 [4, 5] 的客户端 HTTP
    • 它将沿用第5版中[dao]层访问本地文件系统的方法;
  • [8] 中,客户端的 [métier] 层将遵循第 5 版的 [InterfaceMetier] 接口。但其实现方式将有所不同。 在第 5 版中,[métier] 层负责计算税款。在此,由服务器的 [métier] 层进行该计算。 因此,[métier]层将调用[dao]和[7]层,与服务器进行交互并请求其计算税款;
  • [9] 中,控制台脚本需要实例化客户端的 [dao, métier] 层并启动其执行;

18.2. 服务器

我们将重点关注应用程序的服务器端。

Image

该架构将通过以下脚本实现:

Image

18.2.1. 各层之间交换的实体

Image

各层之间交换的实体即为“链接”段落中描述的第5版实体。

18.2.2. [dao]

Image

[dao] 层实现了以下 [InterfaceServerDao] 接口:


<?php

// 命名空间
namespace Application;

interface InterfaceServerDao {

  // 读取税务管理数据
  public function getTaxAdminData(): TaxAdminData;
}
  • 第 9 行:方法 [getTaxAdminData] 将从数据库中检索税务管理部门的数据;

接口 [InterfaceServerDao] 由以下类 [ServerDao] 实现:


<?php

// 命名空间
namespace Application;

// 定义类 ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
  // 类型为 TaxAdminData 的对象,其中包含税率区间数据
  private $taxAdminData;
  // 类型为 [Database] 的对象,其中包含 BD 的特征
  private $database;

  // 生成器
  public function __construct(string $databaseFilename) {
    // 将数据库配置 JSON 存储起来
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // 准备属性
    $this->taxAdminData = new TaxAdminData();
    try {
      // 建立数据库连接
      $connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
      // 要求每次 SGBD 发生错误时,均抛出异常
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // 启动事务
      $connexion->beginTransaction();
      // 填充税率区间表
      $this->getTranches($connexion);
      // 填充常量表
      $this->getConstantes($connexion);
      // 交易成功结束
      $connexion->commit();
    } catch (\PDOException $ex) {
      // 是否有正在进行的交易?
      if (isset($connexion) && $connexion->inTransaction()) {
        // 因失败结束事务
        $connexion->rollBack();
      }
      // 将异常上报给调用方
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // 关闭连接
      $connexion = NULL;
    }
  }

  // 读取数据库数据
  private function getTranches($connexion): void {

  }

  // 读取常量表
  private function getConstantes($connexion): void {

  }

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

}

该代码已在“链接”一节中介绍。

18.2.3. [métier]

Image

Image

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


<?php

// 命名空间
namespace Application;

interface InterfaceServerMetier {

  // 计算纳税人的税款
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}

接口 [InterfaceServerMetier] 由以下类 [ServerMetier] 实现:


<?php

// 命名空间
namespace Application;

class ServerMetier implements InterfaceServerMetier {
  // Dao 层
  private $dao;
  // 税务管理数据
  private $taxAdminData;

  //---------------------------------------------
  // [dao] 层的设置器
  public function setDao(InterfaceServerDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceServerDao $dao) {
    // 在 [dao] 层中存储一个引用
    $this->dao = $dao;
    // 检索用于计算税款的数据
    // 方法 [getTaxAdminData] 可能抛出异常 ExceptionImpots
    // 随后将其回传至调用代码
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// 计算税额
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // 结果
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

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

    // 结果
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=年薪-免税额
  // 免税额有最低和最高限额
  private function getRevenuImposable(float $salaire): float {

    // 结果
    return floor($revenuImposable);
  }

// 计算可能的折减
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // 结果
    return ceil($décôte);
  }

// 计算可能的减免
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    ..
    // 结果
    return ceil($réduction);
  }
}

该代码已在第 1 版的“链接”段落中出现并进行了说明。其基于数据库的对象版本已在“链接”段落中介绍。

18.2.4. 服务器脚本

Image

Image

服务器脚本实现了 [web] [4] 层。脚本 [impots-server] 由以下文件 jSON [config-server.json] 进行配置:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
    "databaseFilename": "Data/database.json",
    "taxAdminDataFileName": "Data/taxadmindata.json",
    "relativeDependencies": [
        "/Entities/BaseEntity.php",
        "/Entities/ExceptionImpots.php",
        "/Entities/TaxAdminData.php",
        "/Entities/Database.php",
        "/Dao/InterfaceServerDao.php",
        "/Dao/ServerDao.php",
        "/Métier/InterfaceServerMetier.php",
        "/Métier/ServerMetier.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ]
}
  • 第 1 行:作为文件路径基准的根目录;
  • 第 2 行:jSON 数据库配置文件;
  • 第 3 行:税务管理数据文件 jSON;
  • 第 5-14 行:应用程序文件;
  • 第 15 行:第三方库的必要依赖项,此处为 Symfony;
  • 第16-20行:允许使用该应用程序的用户列表;

文件 jSON 和 [database.json, taxadmindata.json] 属于第 5 版,具体说明见“链接”一节。

脚本 [impots-server] 以如下方式实现 [web] 层:


<?php

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

// 命名空间
namespace Application;

// 通过 PHP 进行错误处理
//ini_set("display_errors", "0");
//
// 配置文件路径
define("CONFIG_FILENAME", "Data/config-server.json");

// 读取配置
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// 引入脚本所需的依赖项
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// 绝对依赖项(第三方库)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// 常量定义
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// Symfony 依赖项
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;

// 准备服务器的响应 JSON
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// 获取当前请求
$request = Request::createFromGlobals();
// 身份验证
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// 用户是否存在?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
  $trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
  $i++;
}
// 设置响应状态码
if (!$trouvé) {
  // 未找到 - 状态码 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
  // 错误消息
  $response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
  $response->send();
  // 结束
  exit;
}
// 用户有效 - 验证接收的参数
$erreurs = [];
// 必须有三个参数 GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// 错误?
if ($erreur) {
  $erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}

// 获取婚姻状况
if (!$request->query->has("marié")) {
  $erreurs[] = "paramètre marié manquant";
} else {
  $marié = trim(strtolower($request->query->get("marié")));
  $erreur = $marié !== "oui" && $marié !== "non";
  // 错误?
  if ($erreur) {
    $erreurs[] = "paramètre marié [$marié] invalide";
  }
}

// 获取子女数量
if (!$request->query->has("enfants")) {
  $erreurs[] = "paramètre enfants manquant";
} else {
  $enfants = trim($request->query->get("enfants"));
  // 子女数量必须为 >=0 的整数
  $erreur = !preg_match("/^\d+$/", $enfants);
  // 错误?
  if ($erreur) {
    $erreurs[] = "paramètre enfants [$enfants] invalide";
  }
}

// 获取年薪
if (!$request->query->has("salaire")) {
  $erreurs[] = "paramètre salaire manquant";
} else {
  // 薪资必须为大于等于0的整数
  $salaire = trim($request->query->get("salaire"));
  $erreur = !preg_match("/^\d+$/", $salaire);
  // 错误?
  if ($erreur) {
    $erreurs[] = "paramètre salaire [$salaire] invalide";
  }
}

// 查询中还有其他参数?
foreach (\array_keys($request->query->all()) as $key) {
  // 参数有效吗?
  if (!\in_array($key, ["marié", "enfants", "salaire"])) {
    $erreurs[] = "paramètre [$key] invalide";}
}

// 错误?
if ($erreurs) {
  // 向客户端发送 400 错误代码
  $response->setStatusCode(Response::HTTP_BAD_REQUEST);
  $response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// 已具备运行所需的一切条件
// 创建服务器架构
$msgErreur = "";
try {
  // 创建 [dao] 层
  $dao = new ServerDao($config["databaseFilename"]);
  // 创建 [métier] 层
  $métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// 记录错误
  $msgErreur = utf8_encode($ex->getMessage());
}
// 错误?
if ($msgErreur) {
  // 向客户端发送 500 错误代码
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  $response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// 计算税款
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// 返回响应
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();

注释

  • 第16行:加载配置文件;
  • 第18-26行:加载所有依赖项;
  • 第 29 行:文件名 [database.json]
  • 第32-33行:声明将要使用的第三方库类;
  • 第 36-38 行:准备响应 jSON;
  • 第40-52行:验证发起请求的用户是否属于授权用户;
  • 第54-63行:若非如此,则返回代码HTTP 401,表示拒绝访问。 收到此状态码及 HTTP [WWW-Authenticate => Basic realm=] 请求头后,大多数浏览器会弹出身份验证窗口,提示用户进行身份验证;
  • 第59行:服务器的响应jSON解释了错误原因。所有服务器响应均为数组[‘réponse’=>’qq chose’]中的字符串jSON;
  • 第64-117行:验证请求的有效性:
    • 请求必须为 GET,且包含恰好三个参数;
    • 参数 [marié] 的值必须为“是”或“否”;
    • 参数 [enfants],其值必须为大于等于 0 的整数;
    • 参数 [salaire] 的值必须为大于等于 0 的整数;
  • 第 65 行:每次检测到错误时,都会向数组 [$erreurs] 中添加一条错误消息;
  • 第120-126行:若发生错误,则向客户端发送代码HTTP [400 Bad Request](第122行);
  • 第 123 行:服务器的响应 jSON 说明了错误原因;
  • 从第132行开始,所有内容均已验证。此时可以实例化[dao, métier]层。此实例化操作会产生开销,因此仅在确信请求有效时才应执行;
  • 第130-138行:构建服务器架构。[dao]层的构建可能抛出[ExceptionImpots]类型的异常。若发生此异常,则记录该错误;
  • 第135-138行:若发生异常,则向客户端返回状态码HTTP 500。该代码表示服务器发生故障;
  • 第 143 行:响应中说明了错误原因;
  • 第148行 :税额计算委托给 [métier] 层;
  • 第 150-151 行:发送响应;

让我们用浏览器测试这个脚本。请求安全版本的 URL(即 [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]):

Image

  • [1] 中,请求的加密 URL;
  • 转换为 [2],即三个参数 [marié, enfants, salaire]
  • [3] 中,Laragon 的 Apache 服务器发送了一个自签名的证书 SSL。浏览器检测到该证书并显示安全警告:它认为该服务器的网站不可信;
  • [4] 处,继续;

Image

  • [6],继续;

Image

  • [7] 处,浏览器会弹出窗口供用户进行身份验证;
  • [9,10] 中,输入 [admin] [admin]

Image

  • [13] 中,服务器返回 jSON;

进行一些错误测试:

请求 URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]

得到以下结果:

Image

截断 SGBD MySQL,并请求 URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]

Image

18.2.5. 测试 [Codeception]

每次构建服务器的新版本时,我们将测试 [métier][dao] 层,这与自 04 版以来的做法一致(参见链接和链接中的段落)。

首先,我们将项目 [scripts-web] 与测试 [Codeception] 关联。为此,请按照链接段落中针对项目 [scripts-console] 所采用的相同步骤操作。 我们得到一个名为 [scripts-web] 的项目,其中包含一个名为 [Test Files] 的文件夹:

Image

我们将为 [dao] 层创建一个测试,并为 [métier] 层创建一个测试。

18.2.5.1. [dao] 层的测试

Image

[ServerDaoTest] 的测试如下:


<?php

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

// 命名空间
namespace Application;

// 常量定义
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// 配置文件路径
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");

// 获取配置
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// 引入脚本所需的依赖项
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// 绝对依赖项(第三方库)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// 测试 -----------------------------------------------------

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

  public function __construct() {
    // 父级
    parent::__construct();
    // 获取配置
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // 创建层[dao]
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // 测试
  public function testTaxAdminData() {

  }

}

注释

  • 第 9-24 行:构建与 [impots-server.php] 服务器相同的工作环境。这通过第 9-12 行中定义环境所依赖的两个常量来实现;
  • 第32-40行:构建待测试的[dao]层实例,操作方式与服务器脚本[impots-server.php]中一致;
  • 从现在起,环境条件与服务器脚本 [impots-server.php] 相同:可以开始测试;
  • 第43-45行:方法[testTaxAdminData]即链接段落中所述的方法;

测试结果如下:

Image

18.2.5.2. [métier] 层测试

Image

[ServerMetierTest] 测试将如下所示:


<?php

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

// 命名空间
namespace Application;

// 常量定义
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// 配置文件路径
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// 获取配置
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// 引入脚本所需的依赖项
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// 绝对依赖项(第三方库)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// 测试类
class ServerMetierTest extends \Codeception\Test\Unit {
  // 业务层
  private $métier;

  public function __construct() {
    parent::__construct();
    // 获取配置
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // 创建 [dao] 层
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    // 创建层[métier]
    $this->métier = new ServerMetier($dao);
  }

  // 测试
  public function test1() {

  }

  public function test2() {

  }

  ..

  public function test11() {

  }

}

注释

  • 第 9-24 行:构建与 [impots-server.php] 服务器相同的工作环境。这通过第 9-12 行中定义环境所依赖的两个常量来实现;
  • 第30-38行:构建待测试的[métier]层实例,操作方式与服务器脚本[impots-server.php]中的做法一致;
  • 从现在起,环境条件与服务器脚本 [impots-server.php] 相同:可以开始测试;
  • 第40-53行:[test1, test2…, test11]的方法与“链接”段落中描述的一致;

测试结果如下:

Image

18.3. 客户端

我们关注应用程序的客户端部分。

Image

该架构将通过以下脚本实现:

Image

18.3.1. 在层之间交换的实体

Image

上述实体均已描述并被使用过:

18.3.2. [dao]图层

Image

[dao] 层实现了以下 [InterfaceClientDao] 接口:


<?php

// 命名空间
namespace Application;

interface InterfaceClientDao {

  // 读取纳税人数据
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // 计算纳税人的税款
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // 结果记录
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
  • 第 9 行:函数 [getTaxPayersData] 将文件 [$taxPayersFilename] 中的纳税人数据加载到内存中。如有错误,将记录在文件 [$errorsFilename] 中;
  • 第12行:函数[calculerImpots]计算纳税人的税额;
  • 第15行:函数[saveResults]将表[$taxPayersData]中的数据(代表多项税款计算结果)保存到文件[$resultsFilename]中

接口 [InterfaceClientDao] 由以下类 [ClientDao] 实现:


<?php

namespace Application;

// 关联
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // 使用一个 Trait
  use TraitDao;
  // 属性
  private $urlServer;
  private $user;

  // 构造函数
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }

  // 税额计算
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // 创建客户HTTP
    $httpClient = HttpClient::create([
        'auth_basic' => [$this->user["login"], $this->user["passwd"]],
        "verify_peer" => false
    ]);
    // 向服务器发送请求
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
    ]]);
    // 获取响应
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // 日志
    // 打印 "$json=json\n";
    // 获取响应状态
    $statusCode = $response->getStatusCode();
    // 错误?
    if ($statusCode !== 200) {
      // 出现错误 - 抛出异常
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    // 返回响应
    return $réponse;
  }

}

注释

  • 第 10 行:插入 [TraitDao](参见链接段落),该类实现了方法 [getTaxPayersData] [saveResults]。 因此,仅剩 [calculerImpots] 方法需要实现。该方法在第 22-49 行中实现;
  • 第16-19行:[ClientDao]类的构造函数接收两个参数:
    • 来自税费计算服务器的 URL [$urlServer]
    • 包含“login”和“passwd”键的数组 [$user],该数组定义了发起请求的用户;
  • 第 22 行:方法 [calculerImpots] 接收这三个参数,并将其发送至税务计算服务器;
  • 第24-27行:创建一个HTTP客户端,其中包含:
    • 第25行:发起请求的用户凭证;
    • 第 26 行:指定 HTTP 客户端不验证服务器发送的 SSL 证书有效性的选项;
  • 第29-34行:使用服务器预期的三个参数向其发送请求;
  • 第36行:获取来自服务器的响应jSON。 如果未将参数 [false] 传递给方法 [Response::getContent],那么当服务器响应状态处于 [3xx-5xx] 区间内(错误情况)时, 当尝试获取响应内容 [Response::getContent] 或其头部信息 HTTP、[Response::getHeaders] 时,对象 [Response] 会立即抛出异常。 在此,无论响应的状态(HTTP)如何,我们都希望能够访问其内容,哪怕只是为了将其记录到日志中(第40行);
  • 第 37-38 行:服务器的响应是数组 [‘réponse’=>qqChose] 中的字符串 jSON。我们从中提取 [qqChose]
  • 第 40 行:在开发模式下记录响应 jSON;
  • 第 42 行:获取响应的状态码;
  • 第44-49行:如果状态码HTTP不为200,则说明我们的服务器遇到了问题。 此时抛出类型为 [ExceptionImpots] 的异常,其消息内容为服务器响应 jSON 加上响应状态码 HTTP;
  • 第51行:返回一个关联数组,其键为[impôt, surcôte, décôte, réduction, taux]

18.3.3. [métier]

Image

Image

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


<?php

// 命名空间
namespace Application;

interface InterfaceClientMetier {

  // 计算纳税人的税款
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // 批处理模式下的税款计算
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • 第 9 行:函数 [calculerImpots] 计算税额;
  • 第 12 行: 函数 [executeBatchImpots] 计算数据存储在文件 [$taxPayersFileName] 中的纳税人的税额,将计算结果写入文件 [$resultsFileName],并将遇到的错误写入文件 [$errorsFileName]

接口 [InterfaceClientMetier] 由以下类 [ClientMetier] 实现:


<?php

// 命名空间
namespace Application;

class ClientMetier implements InterfaceClientMetier {
  // 属性
  private $clientDao;

  // 构造函数
  public function __construct(InterfaceClientDao $clientDao) {
    // 将引用存储在 [dao] 层中
    $this->clientDao = $clientDao;
  }
  
  // 计算税款
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }

  // 批处理模式下的税款计算
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // 允许来自 [dao] 层的异常上报
    // 获取纳税人数据
    $taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // 结果表
    $results = [];
    // 对结果进行处理
    foreach ($taxPayersData as $taxPayerData) {
      // 计算税款
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // 填写[$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // 将结果填入结果表
      $results [] = $taxPayerData;
    }
    // 保存结果
    $this->clientDao->saveResults($resultsFileName, $results);
  }

}

注释

  • 第 11-14 行:类 [ClientMetier] 的构造函数接收一个指向层 [dao] 的引用作为参数;
  • 第17-19行:税额计算委托给[dao]层;
  • 第20-38行:函数[executeBatchImpots]已在“链接”部分中描述;

18.3.4. 主脚本

Image

Image

客户端脚本 [MainImpotsClient.php] 实现了 [console] 和 [9] 层。它由以下文件 jSON 和 [conf-client.json] 进行配置:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "Entities/BaseEntity.php",
        "Entities/TaxPayerData.php",
        "Entities/ExceptionImpots.php",
        "Utilities/Utilitaires.php",
        "Dao/InterfaceClientDao.php",
        "Dao/TraitDao.php",
        "Dao/ClientDao.php",
        "Métier/InterfaceClientMetier.php",
        "Métier/ClientMetier.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php"
    ],
    "user": {
        "login": "admin",
        "passwd": "admin"
    },
    "urlServer": "https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php"
}
  • 第 1 行:客户的根目录;
  • 第 2 行:纳税人数据文件 jSON;
  • 第 3 行:结果文件 jSON;
  • 第4行:错误文件jSON;
  • 第6-19行:客户项目的各种依赖项;
  • 第20-23行:向税务计算服务器发送请求的用户;
  • 第24行:税务计算服务器的安全URL文件;

[MainImpotsClient.php]脚本代码如下:


<?php

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

// 命名空间
namespace Application;

// 通过 PHP 进行错误处理
//ini_set("display_errors", "0");
//
// 配置文件路径
define("CONFIG_FILENAME", "../Data/config-client.json");

// 读取配置
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// 引入脚本所需的依赖项
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// 绝对依赖项(第三方库)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// 常量定义
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// Symfony 依赖项
use Symfony\Component\HttpClient\HttpClient;

// 创建 [dao] 层
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// 创建 [métier] 层
$clientMetier = new ClientMetier($clientDao);

// 批处理模式下的税款计算
try {
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
  // 显示错误
  print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// 结束
print "Terminé\n";
exit;

注释

  • 第13行:配置文件的路径;
  • 第16行:解析配置文件;
  • 第 18-26 行:加载依赖项;
  • 第37行:创建[dao]图层。向图层构造函数传递其所需的两项信息:
    • 税务计算服务器的 URL;
    • 将执行查询的用户凭证;
  • 第39行:创建[métier]层。向该层的构造函数传递一个指向刚创建的[dao]层的引用;
  • 第43行:要求图层[métier]
    • 计算文件 $config["taxPayerDataFileName"] 中所有纳税人的税款
    • 将计算结果写入文件 $config["resultsFileName"]
    • 将错误记录到文件 $config["errorsFileName"] 中;
  • 第 43 行可能会引发异常;
  • 第 46 行:显示异常的错误消息;

客户端的运行结果与之前版本相同。请检查以下文件:

  • [Data/taxpayersdata.json]:用于计算税额的纳税人数据;
  • [Data/results.json]:文件[Data/taxpayersdata.json]中各纳税人的计算结果;
  • [Data/errors.json]:在处理文件 [Data/taxpayersdata.json] 时可能遇到的错误;

让我们来看一下可能出现的错误情况。首先,停止 Laragon 服务器。此时客户端控制台显示的结果如下:


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

现在仅启动 Apache 服务器,而不启动 SGBD MySQL:

Image

此时客户端控制台显示的结果如下:


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

现在,启动 MySQL,然后在 [config-client] 中修改登录用户:

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

此时客户端控制台显示的结果如下:


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

18.3.5. [Codeception] 测试

与之前版本的做法一样,我们将为 08 版本编写 [Codeception] 测试。

Image

18.3.5.1. [métier] 层测试

[ClientMetierTest.php] 测试如下:


<?php

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

// 命名空间
namespace Application;

// 常量定义
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");

// 配置文件路径
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");

// 读取配置
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// 引入脚本所需的依赖项
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// 绝对依赖项(第三方库)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// 测试类
class ClientMetierTest extends \Codeception\Test\Unit {
  // 业务层
  private $métier;

  public function __construct() {
    parent::__construct();
    // 获取配置
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // 创建 [dao] 层
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // 创建层[métier]
    $this->métier = new ClientMetier($clientDao);
  }

  // 测试
  public function test1() {

  }

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

  public function test11() {

  }

}

注释

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

测试结果如下:

Image