Skip to content

18. 练习题 – 第 8 版

我们将重新审视示例应用程序——第 5 版(链接段落),并将其改造成客户端/服务器应用程序。

18.1. 简介

第 5 版的架构如下:

Image

  • 名为 [dao](数据访问对象)的层负责处理与 MySQL 数据库及本地文件系统的交互;
  • 名为 [business] 的层负责执行税费计算;
  • 主脚本充当协调者:它实例化 [DAO] [业务逻辑] 层,然后与 [业务逻辑] 层进行通信以执行必要的任务;

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

Image

  • [2]中,我们将复用第5版中的[DAO]层,并移除用于访问本地文件系统的方法。这些方法将迁移至客户端的[DAO][6, 7]
  • [3]中,[业务]层将与第5版保持一致,但去除了其中的[executeBatchImpôts, saveResults]方法,这些方法将迁移至客户端的[DAO][7]
  • [4]中,必须编写服务器脚本:它需要:
    • 创建 [业务] [DAO][3, 2]
    • 与客户端脚本进行通信 [5, 7]
  • [7]中,必须编写客户端的[DAO]层:
    • 它将作为服务器脚本的 HTTP 客户端 [4, 5]
    • 它将复用第 5 版 [DAO] 层中用于访问本地文件系统的方法;
  • [8] 中,客户端的 [业务] 层将遵循第 5 版中的 [BusinessInterface] 接口。但其实现方式将有所不同。 在第 5 版中,[business] 层负责执行税费计算。在此,该计算由服务器的 [business] 层执行。因此,[business] 层将调用 [DAO][7] 与服务器通信,并请求其计算税费;
  • [9] 中,控制台脚本需要实例化客户端的 [DAO、业务] 层并启动其执行;

18.2. 服务器

我们目前专注于应用程序的服务器端。

Image

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

Image

18.2.1. 层间交换的实体

Image

层间交换的实体是链接部分中所述的第5版中的实体。

18.2.2. [dao] 层

Image

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


<?php
 
// namespace
namespace Application;
 
interface InterfaceServerDao {
 
  // reading tax administration data
  public function getTaxAdminData(): TaxAdminData;
}
  • 第 9 行:[getTaxAdminData] 方法从数据库中检索税务管理数据;

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


<?php
 
// namespace
namespace Application;
 
// definition of a ImpotsWithDataInDatabase class
class ServerDao implements InterfaceServerDao {
  // the TaxAdminData object containing tax bracket data
  private $taxAdminData;
  // the [Database] type object containing the characteristics of the BD
  private $database;
 
  // manufacturer
  public function __construct(string $databaseFilename) {
    // store the JSON configuration of the bd
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // we prepare the attribute
    $this->taxAdminData = new TaxAdminData();
    try {
      // open the database connection
      $connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
      // we want every SGBD error to trigger an exception
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // start a transaction
      $connexion->beginTransaction();
      // fill in the tax bracket table
      $this->getTranches($connexion);
      // fill in the constants table
      $this->getConstantes($connexion);
      // the transaction is completed successfully
      $connexion->commit();
    } catch (\PDOException $ex) {
      // is there a transaction in progress?
      if (isset($connexion) && $connexion->inTransaction()) {
        // transaction ends in failure
        $connexion->rollBack();
      }
      // trace the exception back to the calling code
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // close the connection
      $connexion = NULL;
    }
  }
 
  // reading data from the database
  private function getTranches($connexion): void {

  }
 
  // reading the constants table
  private function getConstantes($connexion): void {

  }
 
  // returns data for tax calculation
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }
 
}

该代码在链接的章节中已展示。

18.2.3. [业务]层

Image

Image

[业务]层实现了以下[InterfaceServerMetier]接口:


<?php
 
// namespace
namespace Application;
 
interface InterfaceServerMetier {
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}

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


<?php
 
// namespace
namespace Application;
 
class ServerMetier implements InterfaceServerMetier {
  // dao layer
  private $dao;
  // tax administration data
  private $taxAdminData;
 
  //---------------------------------------------
  // setter couche [dao]
  public function setDao(InterfaceServerDao $dao) {
    $this->dao = $dao;
    return $this;
  }
 
  public function __construct(InterfaceServerDao $dao) {
    // a reference is stored on the [dao] layer
    $this->dao = $dao;
    // recover data for tax calculation
    // method [getTaxAdminData] may throw a ExceptionImpots exception
    // we then let it go back to the calling code
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }
 
// tAX CALCULATION
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {

    // result
    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 {

    // result
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }
 
  // revenuImposable=annualwage-discount
  // the allowance has a minimum and a maximum
  private function getRevenuImposable(float $salaire): float {

    // result
    return floor($revenuImposable);
  }
 
// calculates any discount
  private function getDecôte(string $marié, float $salaire, float $impots): float {

    // result
    return ceil($décôte);
  }
 
// calculates any reduction
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    ..
    // result
    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 行:MySQL 数据库的 JSON 配置文件;
  • 第 3 行:包含税务管理数据的 JSON 文件;
  • 第 5–14 行:应用程序文件;
  • 第 15 行:对第三方库的依赖,本例中为 Symfony;
  • 第 16–20 行:获准使用该应用程序的用户数组;

JSON 文件 [database.json, taxadmindata.json] 来自第 5 版,具体说明请参见链接部分。

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


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// definition of constants
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
 
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
 
// retrieve the current query
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// does the user exist?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
  $trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
  $i++;
}
// set the response status code
if (!$trouvé) {
  // not found - code 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
  // error msg
  $response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
  $response->send();
  // end
  exit;
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// mistake?
if ($erreur) {
  $erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}

// marital status is restored
if (!$request->query->has("marié")) {
  $erreurs[] = "paramètre marié manquant";
} else {
  $marié = trim(strtolower($request->query->get("marié")));
  $erreur = $marié !== "oui" && $marié !== "non";
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre marié [$marié] invalide";
  }
}
 
// the number of children
if (!$request->query->has("enfants")) {
  $erreurs[] = "paramètre enfants manquant";
} else {
  $enfants = trim($request->query->get("enfants"));
  // number of children must be an integer >=0
  $erreur = !preg_match("/^\d+$/", $enfants);
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre enfants [$enfants] invalide";
  }
}
 
// we recover the annual salary
if (!$request->query->has("salaire")) {
  $erreurs[] = "paramètre salaire manquant";
} else {
  // salary must be an integer >=0
  $salaire = trim($request->query->get("salaire"));
  $erreur = !preg_match("/^\d+$/", $salaire);
  // mistake?
  if ($erreur) {
    $erreurs[] = "paramètre salaire [$salaire] invalide";
  }
}
 
// other parameters in the query?
foreach (\array_keys($request->query->all()) as $key) {
  // valid parameter?
  if (!\in_array($key, ["marié", "enfants", "salaire"])) {
    $erreurs[] = "paramètre [$key] invalide";}
}
 
// mistakes?
if ($erreurs) {
  // an error code 400 is sent to the customer
  $response->setStatusCode(Response::HTTP_BAD_REQUEST);
  $response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// we have everything you need to work
// server architecture creation
$msgErreur = "";
try {
  // creation of the [dao] layer
  $dao = new ServerDao($config["databaseFilename"]);
  // creation of the [business] layer
  $métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// we note the error
  $msgErreur = utf8_encode($ex->getMessage());
}
// mistake?
if ($msgErreur) {
  // an error code 500 is sent to the customer
  $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
  $response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
  $response->send();
  exit;
}
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
$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 响应说明了错误原因。所有服务器响应都将是一个数组形式的 JSON 字符串 [‘response’=>’something’]
  • 第 64–117 行:我们验证请求的有效性:
    • 一个包含恰好三个参数的 GET 请求;
    • 一个 [married] 参数,其值必须为 ‘yes’ 或 ‘no’;
    • [children] 参数,其值必须为大于等于 0 的整数;
    • [salary] 参数,其值必须为大于等于 0 的整数;
  • 第 65 行:每次检测到错误时,都会向数组 [$errors] 中添加一条错误信息;
  • 第 120–126 行:如果发生错误,将向客户端发送 HTTP 状态码 [400 Bad Request](第 122 行);
  • 第 123 行:服务器的 JSON 响应说明了错误的原因;
  • 从第 132 行开始,所有内容均已验证通过。我们可以实例化 [DAO、业务] 层。此实例化操作会消耗资源,因此仅在确认请求有效时才应执行;
  • 第 130–138 行:构建服务器架构。构建 [DAO] 层可能会抛出 [ExceptionImpots] 异常。若发生此异常,则记录错误;
  • 第 135–138 行:若发生异常,则向客户端发送 HTTP 状态码 500。该代码表示服务器已崩溃;
  • 第 143 行:响应中说明了错误的原因;
  • 第 148 行 :将税费计算委托给 [business] 层;
  • 第 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] 中,三个参数 [married, children, salary]
  • [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

我们关闭 MySQL 数据库管理系统,并请求 URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]

Image

18.2.5. [Codeception] 测试

每次构建服务器的新版本时,我们将按照自 04 版以来的惯例(参见段落链接链接),对 [业务][DAO] 层进行测试。

首先,我们将 [scripts-web] 项目与 [Codeception] 测试关联起来。具体操作方法与链接段落中针对 [scripts-console] 项目所采用的步骤相同。最终,我们将得到一个包含 [Test Files] 文件夹的 [scripts-web] 项目:

Image

我们将为 [dao] 层和 [business] 层各创建一个测试。

18.2.5.1. 针对 [dao] 层的测试

Image

[ServerDaoTest] 的内容如下:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
 
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// test -----------------------------------------------------
 
class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;
 
  public function __construct() {
    // parent
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  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. [business] 层的测试

Image

[ServerMetierTest] 测试将如下所示:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// test class
class ServerMetierTest extends \Codeception\Test\Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
    // creation of the [business] layer
    $this->métier = new ServerMetier($dao);
  }
 
  // tests
  public function test1() {

  }
 
  public function test2() {

  }
 
  ..
 
  public function test11() {

  }
 
}

评论

  • 第 9–24 行:我们设置了与 [impots-server.php] 服务器相同的工作环境。这是通过第 9–12 行定义环境所依赖的两个常量来实现的;
  • 第 30–38 行:我们创建了一个待测试的 [business] 层实例,这与服务器脚本 [impots-server.php] 中的做法一致;
  • 从这一刻起,我们与服务器脚本 [impots-server.php] 处于相同的环境中:我们可以开始测试;
  • 第 40–53 行:方法 [test1, test2…, test11]链接章节中所述的方法;

测试结果如下:

Image

18.3. 客户端

我们目前专注于应用程序的客户端部分。

Image

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

Image

18.3.1. 各层之间交换的实体

Image

上述实体均已描述完毕,且已投入使用:

  • 链接部分中的 [BaseEntity]
  • [ExceptionImpots] 位于链接部分;
  • [TaxPayerData] 位于链接部分;

18.3.2. [dao] 层

Image

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


<?php
 
// namespace
namespace Application;
 
interface InterfaceClientDao {
 
  // reading taxpayer data
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
 
  // recording results
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
  • 第 9 行:[getTaxPayersData] 函数将纳税人数据从 [$taxPayersFilename] 文件加载到内存中。如果出现任何错误,这些错误将被记录在 [$errorsFilename] 文件中;
  • 第 12 行:[calculateTaxes] 函数计算纳税人的税款;
  • 第 15 行:[saveResults] 函数将 [$taxPayersData] 数组中的数据(该数组包含多项税款计算的结果)保存到 [$resultsFilename] 文件中;

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


<?php
 
namespace Application;
 
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
 
class ClientDao implements InterfaceClientDao {
  // using a Trait
  use TraitDao;
  // attributes
  private $urlServer;
  private $user;
 
  // manufacturer
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }
 
  // tAX CALCULATION
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // create a HTTP customer
    $httpClient = HttpClient::create([
        'auth_basic' => [$this->user["login"], $this->user["passwd"]],
        "verify_peer" => false
    ]);
    // make a request to the server
    $response = $httpClient->request('GET', $this->urlServer,
      ["query" => [
          "marié" => $marié,
          "enfants" => $enfants,
          "salaire" => $salaire
    ]]);
    // the answer is retrieved
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // logs
    // print "$json=json\n";
    // retrieve response status
    $statusCode = $response->getStatusCode();
    // mistake?
    if ($statusCode !== 200) {
      // we have an error - we throw an exception
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    // we return the answer
    return $réponse;
  }
 
}

评论

  • 第 10 行:我们插入 [TraitDao]参见相关段落),该类实现了 [getTaxPayersData] [saveResults] 方法。至此,仅剩 [calculateTaxes] 方法需要实现。该方法在第 22–49 行中实现;
  • 第 16–19 行:[ClientDao] 类的构造函数接受两个参数:
    • 税务计算服务器的 URL [$urlServer]
    • 包含键值对“login”和“passwd”的数组 [$user],用于定义发起请求的用户;
  • 第 22 行:[calculateTaxes] 方法接收这三个参数,并将它们发送至税费计算服务器;
  • 第 24–27 行:创建一个 HTTP 客户端,其中:
    • 第 25 行:发送请求的用户凭据;
    • 第 26 行:设置选项,防止 HTTP 客户端验证服务器发送的 SSL 证书的有效性;
  • 第 29–34 行:使用服务器预期的三个参数向服务器发送请求;
  • 第 36 行:从服务器获取 JSON 响应。 如果未在 [Response::getContent] 方法上设置 [false] 参数,那么当服务器的响应状态处于 [3xx-5xx] 范围(错误情况)时,一旦尝试获取响应内容 [Response::getContent] 或其 HTTP 头部 [Response::getHeaders][Response] 对象就会抛出异常。 在此,无论响应的 HTTP 状态如何,我们都希望能够访问其内容,哪怕只是为了将其记录到日志中(第 40 行);
  • 第 37-38 行:服务器的响应是一个 JSON 字符串,格式为数组 [‘response’=>something]。我们提取其中的 [something]
  • 第 40 行:在开发模式下记录 JSON 响应;
  • 第 42 行:我们获取响应状态码;
  • 第 44-49 行:如果 HTTP 状态码不是 200,则说明服务器遇到了问题。此时,我们会抛出一个 [ExceptionImpots] 异常,其消息由服务器的 JSON 响应加上 HTTP 状态码组成;
  • 第 51 行:返回结果,这是一个关联数组,键为 [tax, surcharge, discount, reduction, rate]

18.3.3. [业务]层

Image

Image

[业务][8] 实现了以下 [BusinessClientInterface]


<?php
 
// namespace
namespace Application;
 
interface InterfaceClientMetier {
 
  // calculating a taxpayer's taxes
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • 第 9 行:[calculateTaxes] 函数计算税款;
  • 第 12 行:[executeBatchImports] 函数为 [$taxPayersFileName] 文件中的纳税人计算税款,将结果写入 [$resultsFileName] 文件,并将遇到的任何错误写入 [$errorsFileName] 文件;

[BusinessClientInterface] 接口由以下 [BusinessClient] 类实现:


<?php
 
// namespace
namespace Application;
 
class ClientMetier implements InterfaceClientMetier {
  // attribute
  private $clientDao;
 
  // manufacturer
  public function __construct(InterfaceClientDao $clientDao) {
    // the reference is stored on the [dao] layer
    $this->clientDao = $clientDao;
  }
  
  // tAX CALCULATION
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
  }
 
  // batch mode tax calculation
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // we let the exceptions coming from the [dao] layer flow upwards
    // retrieve taxpayer data
    $taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // results table
    $results = [];
    // we exploit them
    foreach ($taxPayersData as $taxPayerData) {
      // tax calculation
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // complete [$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // put the result in the results table
      $results [] = $taxPayerData;
    }
    // recording results
    $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
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "../Data/config-client.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
 
// definition of constants
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// symfony dependencies
use Symfony\Component\HttpClient\HttpClient;
 
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$clientMetier = new ClientMetier($clientDao);
 
// tax calculation in batch mode
try {
  $clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
  // error is displayed
  print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;

注释

  • 第 13 行:配置文件的路径;
  • 第 16 行:处理配置文件;
  • 第 18–26 行:加载依赖项;
  • 第 37 行:创建 [dao] 层。我们向该层的构造函数传递了它所期望的两项信息:
    • 税费计算服务器的 URL;
    • 将发起请求的用户凭据;
  • 第 39 行:创建 [business] 层。我们将刚刚创建的 [dao] 层的引用传递给该层的构造函数;
  • 第 43 行:我们要求 [业务] 层:
    • 计算文件 $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 服务器,而不启动 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. 测试 [业务] 层

[ClientMetierTest.php] 测试代码如下:


<?php

// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
 
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
 
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
 
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// test class
class ClientMetierTest extends \Codeception\Test\Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // we retrieve the configuration
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // creation of the [dao] layer
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // creation of the [business] layer
    $this->métier = new ClientMetier($clientDao);
  }
 
  // tests
  public function test1() {

  }
 
  -------------
 
  public function test11() {

  }
 
}

注释

  • 第10–26行:测试环境的定义。我们使用与链接部分中描述的主脚本[MainImpotsClient]相同的环境;
  • 第33–41行:构建[dao][business]层;
  • 第 40 行:属性 [$this→business] 引用 [business] 层;
  • 第 44–51 行:方法 [test1, test2…, test11]链接章节中所述的方法;

测试结果如下:

Image