Skip to content

20. 应用程序练习 – 第 10 版

上一版本已说明,应将所有应用用户共用的税费数据存储在 [Application] 作用域的缓存中。我们将使用 Redis 服务器 [https://redis.io] 来实现这一点。

20.1. Redis

[Application] 作用域的内存将由 Redis 服务器实现。需要此应用程序内存的 PHP 脚本将成为该服务器的客户端:

Image

20.2. 安装 Redis

Laragon 自带的 Redis 服务器默认处于禁用状态。因此,您必须首先将其启用:

Image

  • [3] 中,启用 [Redis] 服务器;
  • [4] 中,将端口 [6379] 保留为 Redis 客户端使用的默认端口;

启用 Redis 后,Laragon 服务将自动重启:

Image

20.3. 命令模式下的 Redis 客户端

可在命令模式下查询 Redis 服务器。打开 Laragon 终端(参见链接部分):

Image

  • [1] 中,[redis-cli] 命令将启动 Redis 服务器的命令行客户端;

截至2019年7月,Redis客户端支持172条用于与服务器交互的命令 [https://redis.io/commands#list]。其中一条命令 [command count] [2] 会显示该数字 [3]

本文仅介绍我们PHP应用程序所需的命令。我们将 Redis 用于单一目的:在 Redis 内存中存储一个 [‘属性’=>’值’] 数组。这通过 Redis 命令 [set 属性 值] [4] 实现。随后可使用 [get 属性] 命令 [5] 检索该值。这就是我们所需的全部内容。

可能需要清空 Redis 的内存。这可以通过 [flushdb] 命令 [6] 实现。随后,如果查询 [title] 属性的值 [7],我们会得到一个 [nil] 引用 [8],表明未找到该属性。我们还可以使用 [exists] 命令 [9-10] 来检查属性是否存在。

要退出 Redis 客户端,请输入 [quit] 命令 [11]

20.4. 为 PHP 安装 Redis 客户端

现在我们需要为 PHP 安装一个 Redis 客户端:

Image

有多种库实现了 Redis 客户端。我们将使用 [Predis][https://github.com/nrk/predis](2019 年 7 月)。与之前的库一样,该库可通过 [composer] 在 Laragon 终端中安装:

Image

20.5. 服务器端代码

Image

配置文件 [config-server.json] 修改如下:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-10",
    "databaseFilename": "Data/database.json",
    "relativeDependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Entities/TaxAdminData.php",
        "/../version-08/Entities/Database.php",
        "/../version-08/Dao/InterfaceServerDao.php",
        "/../version-08/Dao/ServerDao.php",
        "/../version-09/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/../version-09/Utilities/Logger.php",
        "/../version-09/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php",
        "C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
    ],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Data/logs.txt"
}

注释

  • 第 5–15 行:第 10 版除了 [impots-server.php] 脚本外,并未引入任何新内容。它采用了第 08 版和第 09 版的元素;
  • 第 19 行:这是我们刚刚安装的 [predis] 库所需的依赖项;

服务器代码 [impots-server.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-server.json");
// class alias
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
 
// session
$session = new Session();
$session->start();


// 1st log
$logger->write("\n---nouvelle requête\n");
 
// retrieve the current query
$request = Request::createFromGlobals();
// authentication only the 1st time
if (!$session->has("user")) {

} else {
  // log
  $logger->write("Authentification prise en session…\n");
}
 
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());

 
// mistakes?
if ($erreurs) {
// an error code 400 HTTP_BAD_REQUEST is sent to the customer
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // completed
  exit;
} else {
  // logs
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
 
// we've got everything you need to work
// Redis
\Predis\Autoloader::register();
try {
  // customer [predis]
  $redis = new \Predis\Client();
  // connect to the server to see if it's there
  $redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
  // internal server error
  doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
  // completed
  exit;
}
 
// creation of the [dao] layer
if (!$redis->get("taxAdminData")) {
  // tax data is taken from the database
  $logger->write("données fiscales prises en base de données\n");
  try {
    // construction of the [dao] layer
    $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
    // put tax data in scope memory [application]
    // method [TaxAdminData]->__toString will be called implicitly
    $redis->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // we note the error
    doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
    // completed
    exit;
  }
} else {
  // tax data are taken from the [application] scope memory
  $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
  $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
  // istanciation of the [dao] layer
  $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
  // logs
  $logger->write("données fiscales prises dans redis\n");
}
// creation of the [business] layer
$métier = new ServerMetier($dao);
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// end
exit;
 
function doInternalServerError(string $message, Response $response, array $infos,
  Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $message: error message
  // $response : answer HTTP
  // $infos: information table for sending mail
  // $result: results table
  // $logger: application logger
  // $predisClient: a customer [predis]
  //
  // send an e-mail to the administrator
  // SendAdminMail intercepts all exceptions and logs them itself
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // an error code 500 is sent to the customer
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}

// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode,
  array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $response : answer HTTP
  // $result: results table
  // $statusCode: HTTP response status
  // $headers: HTTP headers to be included in the response
  // $logger: application logger
  // $predisClient: a customer [predis]
  //
  // status HTTTP
  $response->setStatusCode($statusCode);
  // body
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // headers
  $response->headers->add($headers);
  // shipping
  $response->send();
  // log
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
  // close connection [redis]
  if ($predisClient != NULL) {
    $predisClient->disconnect();
  }
}

注释

  • 第 15 行:我们将别名 [ServerDaoWithRedis] 赋予类 [\Application\ServerDaoWithSession],以反映服务器脚本实现中的变更;
  • 第 18–19 行:会话被维护。在此,我们需要牢记两点信息:
    • 用户已成功认证。该信息具有 [session] 作用域:它与特定用户绑定,对其他用户无效;
    • 税务管理数据。该信息具有 [application] 作用域:它不与特定用户相关联,而是适用于所有用户;
  • 第 54–64 行:创建将与 [redis] 服务器通信的 [redis] 客户端。该客户端将通过服务器的默认端口进行通信。如果服务器未使用默认端口,或者未位于 [localhost] 机器上,则需要将此信息传递给 [\Predis\Client] 类的构造函数;
  • 第 59 行:客户端立即连接到服务器以检查其是否响应;
  • 第 60–65 行:若连接 Redis 服务器失败,则向客户端发送错误响应,并向应用程序管理员发送电子邮件;
  • 第 67 行:向 [redis] 服务器查询键 [taxAdminData]。若未找到,则从数据库中检索税务数据(第 72 行);
  • 第 75 行:将键 [taxAdminData] 及其对应的 JSON 字符串(变量 [$taxAdminData],其类型为 [TaxAdminData] 对象)存储在 [redis] 内存中。 [$redis→set] 方法期望键的值为字符串。因此,它将尝试将 [TaxAdminData] 对象转换为 [string] 类型。这会隐式调用 [TaxAdminData->__toString] 方法,该方法会生成 [TaxAdminData] 对象的 JSON 字符串;
  • 第 84 行:键 [taxAdminData] 已存在于 [redis] 内存中,因此我们检索其值。我们知道这是 [TaxAdminData] 对象的 JSON 字符串。随后我们解析该字符串以获取一个属性数组;
  • 第 85 行:基于该数组,实例化一个新的 [TaxAdminData] 对象;
  • 第 87 行:实例化 [dao] 层;

20.6. 客户端代码

Image

客户端版本 10 与版本 9 完全相同。唯一的改动在于配置文件 [config-client.json]


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10",
    "taxPayersDataFileName": "Data/taxpayersdata.json",
    "resultsFileName": "Data/results.json",
    "errorsFileName": "Data/errors.json",
    "dependencies": [
        "/../version-08/Entities/BaseEntity.php",
        "/../version-08/Entities/TaxPayerData.php",
        "/../version-08/Entities/ExceptionImpots.php",
        "/../version-08/Utilities/Utilitaires.php",
        "/../version-08/Dao/InterfaceClientDao.php",
        "/../version-08/Dao/TraitDao.php",
        "/../version-09/Dao/ClientDao.php",
        "/../version-08/Métier/InterfaceClientMetier.php",
        "/../version-08/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-10/impots-server.php"
}

唯一的改动是第24行的服务器URL。

结果与版本 09 相同。让我们测试一个新的错误情况:

Image

控制台中的结果如下:


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

20.7. [Codeception] 客户端测试

Image

版本 10 中的 [ClientMetierTest] 测试类与版本 09 中的完全相同,只有一个例外:


<?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-10");


 
}
  • 第 10 行:测试环境为 Redis 10 版本客户端;

在开始测试之前,让我们使用 [redis-cli] 客户端从 [redis] 服务器的内存中删除 [taxAdminData] 键:

Image

现在,让我们运行测试:

Image

现在让我们查看服务器日志 [logs.txt]


05/07/19 08:52:16:396 :
---nouvelle requête
05/07/19 08:52:16:403 : Autentification en cours…
05/07/19 08:52:16:403 : Authentification réussie [admin, admin]
05/07/19 08:52:16:403 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
05/07/19 08:52:16:407 : données fiscales prises en base de données
05/07/19 08:52:16:420 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:546 :
---nouvelle requête
05/07/19 08:52:16:555 : Autentification en cours…
05/07/19 08:52:16:555 : Authentification réussie [admin, admin]
05/07/19 08:52:16:556 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
05/07/19 08:52:16:559 : données fiscales prises dans redis
05/07/19 08:52:16:559 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
05/07/19 08:52:16:668 :
---nouvelle requête
05/07/19 08:52:16:675 : Autentification en cours…
05/07/19 08:52:16:675 : Authentification réussie [admin, admin]
05/07/19 08:52:16:675 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
05/07/19 08:52:16:678 : données fiscales prises dans redis
05/07/19 08:52:16:678 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
05/07/19 08:52:16:776 :
---nouvelle requête

我们之前已经提到,对于每个测试,测试类的构造函数都会被重新执行,这意味着被测试的 [ClientDao] 类在每次测试中都会使用一个不存在的会话 Cookie 进行实例化。因此,一切都仿佛这 11 个测试代表了 11 个不同的用户,拥有 11 个不同的会话。

  • 第 6 行:从数据库中检索税务数据;
  • 第 13、20 行:从 [Redis] 内存中检索税务数据。因此,我们有一个由应用程序所有用户共享的 [application] 作用域内存;

20.8. [Redis] 服务器 Web 界面

我们已经看到,[Redis] 服务器可以通过命令行模式进行管理。它也可以通过 Web 界面进行管理:

Image

  • [4]中,管理URL;
  • [5] 中,显示服务器存储的键;
  • [6] 中,当前服务器状态;

点击 [5],即可查看有关 [taxAdminData] 密钥的信息:

Image

  • [7] 中,可访问 [taxAdminData][8] 相关信息的 URL;
  • [9] 中,显示该键的状态;
  • [10] 中,其值:您可以识别出 [TaxAdminData] 对象的 JSON 字符串;
  • [11] 中,您可以删除该键;
  • [12] 中,您可以添加另一个键;