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条命令与服务器进行交互。 其中一个命令 [command count] [2],会显示这个数字 [3]

我们仅介绍在PHP应用程序中需要用到的命令。我们将Redis用于唯一一个目的:在Redis内存中存储一个数组[‘attribut’=>’valeur’]。 这可以通过 Redis 命令 [set attribut valeur] [4] 实现。随后可通过命令 [get attribut] [5] 检索该值。这就是我们所需的一切。

可能需要清空Redis的内存。这可以通过命令 [flushdb] [6] 来实现。 随后若查询属性 [titre] [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 行:除了脚本 [impots-server.php] 之外,版本 10 没有带来任何新内容。它使用了版本 08 和 09 中的元素;
  • 第 19 行:对刚刚安装的 [predis] 库的必要依赖;

[impots-server.php] 服务器代码的演变如下:


<?php

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

// 命名空间
namespace Application;

// PHP 的错误处理
ini_set("display_errors", "0");
//
// 配置文件路径
define("CONFIG_FILENAME", "Data/config-server.json");
// 类别名
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

// 会话
$session = new Session();
$session->start();


// 第一个日志
$logger->write("\n---nouvelle requête\n");

// 获取当前请求
$request = Request::createFromGlobals();
// 仅首次认证
if (!$session->has("user")) {

} else {
  // 日志
  $logger->write("Authentification prise en session…\n");
}

// 用户有效 - 验证接收到的参数
$erreurs = [];
// 必须有三个参数 GET
$method = strtolower($request->getMethod());


// 有错误吗?
if ($erreurs) {
// 向客户端发送 400 错误代码 HTTP_BAD_REQUEST
  sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
  // 完成
  exit;
} else {
  // 日志
  $logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}

// 已具备所有必要条件
// Redis
\Predis\Autoloader::register();
try {
  // 客户端 [predis]
  $redis = new \Predis\Client();
  // 连接服务器以检查其是否在线
  $redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
  // 内部服务器错误
  doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
  // 完成
  exit;
}

// 创建图层 [dao]
if (!$redis->get("taxAdminData")) {
  // 从数据库中提取税务数据
  $logger->write("données fiscales prises en base de données\n");
  try {
    // 构建图层 [dao]
    $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
    // 将税务数据放入作用域内存 [application]
    // 将隐式调用方法 [TaxAdminData]->__toString
    $redis->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // 记录错误
    doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
    // 完成
    exit;
  }
} else {
  // 从作用域内存中获取税务数据 [application]
  $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
  $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
  // 实例化层 [dao]
  $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
  // 日志
  $logger->write("données fiscales prises dans redis\n");
}
// 创建层 [métier]
$métier = new ServerMetier($dao);
// 计算税款
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// 返回结果
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// 结束
exit;

function doInternalServerError(string $message, Response $response, array $infos,
  Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $message:错误信息
  // $response:响应 HTTP
  // $infos:发送邮件的信息表
  // $result:结果表
  // $logger:应用程序日志记录器
  // $predisClient:一个客户端 [predis]
  //
  // 向管理员发送一封邮件
  // SendAdminMail 拦截所有异常并自行记录
  $infos['message'] = $message;
  $sendAdminMail = new SendAdminMail($infos, $logger);
  $sendAdminMail->send();
  // 向客户端发送 500 错误代码
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}

// 向客户端发送响应 HTTP 的函数
function sendResponse(Response $response, array $result, int $statusCode,
  array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
  // $response:响应 HTTP
  // $result:结果表
  // $statusCode:响应状态 HTTP
  // $headers:应放入响应中的 HTTP 头部
  // $logger:应用程序日志记录器
  // $predisClient:一个客户端 [predis]
  //
  // 状态 HTTTP
  $response->setStatusCode($statusCode);
  // 正文
  $body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
  $response->setContent($body);
  // 头部
  $response->headers->add($headers);
  // 发送
  $response->send();
  // 日志
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
  // 关闭连接[redis]
  if ($predisClient != NULL) {
    $predisClient->disconnect();
  }
}

注释

  • 第 15 行:为类 [\Application\ServerDaoWithSession] 赋予别名 [ServerDaoWithRedis],以反映服务器脚本实现的变更;
  • 第 18-19 行:会话被保留。这里有两点信息需要记住:
    • 用户已成功认证。该信息属于 [session] 作用域:它与特定用户相关联,对其他用户无效;
    • 税务管理数据。该信息作用域为 [application]:它不与特定用户相关联,但对所有用户均有效;
  • 第54-64行:创建客户端[redis],该客户端将与服务器[redis]进行通信。 该客户端将通过服务器的默认端口进行通信。如果服务器未使用默认端口,或者未位于 [localhost] 机器上,则需将这些信息传递给 [\Predis\Client] 类的构造函数;
  • 第 59 行:立即将客户端连接到服务器以检测其响应;
  • 第 60-65 行:若连接 Redis 服务器失败,则向客户端发送错误响应,并向应用程序管理员发送一封邮件;
  • 第67行:向[redis]服务器查询键值[taxAdminData]。若未找到,则从数据库中获取税务数据(第72行);
  • 第75行: 键 [taxAdminData] 被放置在与变量 [$taxAdminData] 中的字符串 jSON 关联的内存 [redis] 中,该变量是一个类型为 QZXW2HTMLP005326Z 的对象QX。方法 [$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

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

// 命名空间
namespace Application;

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



}
  • 第 10 行:测试环境为 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] 中,URL 可访问密钥 [taxAdminData][8] 的信息;
  • [9] 中,密钥的状态;
  • [10] 中,其值:可识别出 jSON 字符串,该字符串属于 [TaxAdminData] 类型的对象;
  • [11] 中,可以删除该键;
  • [12] 中,可以添加另一个;