22. 应用练习 – 第 11 版
Web 服务通常仍会以 XML 格式而非 JSON 格式发送响应:
- JSON 数据流更轻量,但需要用户指南才能理解;
- XML 数据流虽然冗长,但具有自文档特性,可以立即理解;
我们修改了客户端/服务器版本 11,使服务器现在向客户端发送 XML 数据流作为响应:

22.1. 服务器

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

22.1.1. [Utilities] 类
我们将复用自 03 版以来使用的 [Utilities] 类(参见相关段落):
<?php
// namespace
namespace Application;
// a class of utility functions
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
…
}
// from https://stackoverflow.com/questions/1397036/how-to-convert-array-to-simplexml
public static function getXmlForArrayOfAttributes(array $arrayOfAttributes,
\SimpleXmlElement &$node): void {
// scan array attributes
foreach ($arrayOfAttributes as $attribute => $value) {
// is the attribute numeric?
if (is_numeric($attribute)) {
// table index case (but also other cases)
$attribute = 'i' . $attribute;
}
// is $value an array?
if (is_array($value)) {
// we'll explore the [$value] array in turn
// we add a node to the XML graph
$subnode = $node->addChild($attribute);
// recursive call to explore the [$value] array
Utilitaires::getXmlForArrayOfAttributes($value, $subnode);
} else {
// we add the node to the XML graph
$node->addChild("$attribute", htmlspecialchars("$value"));
}
}
}
}
评论
- 第 14–36 行:我们引入静态方法 [getXmlForArrayOfAttributes],该方法返回作为参数传递的数组 [arrayOfAttributes] 的 XML 字符串。第二个参数是类型为 [SimpleXmlElement] 的 XML 图节点的引用。执行后,该节点包含数组 [arrayOfAttributes] 的 XML 树;
我们编写以下测试 [testXml.php]:

<?php
// dependency
require __DIR__ . "/Utilitaires.php";
// associative table
$array = ["nom" => "amédée", "prénom" => "sylvain", "âge" => 40,
"enfants" => [["nom" => "amédée", "prénom" => "béatrice", "âge" => 6],
["nom" => "amédée", "prénom" => "bertrand", "âge" => 4]]];
// xml
header("Content-Type: application/xml");
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><root></root>");
\Application\Utilitaires::getXmlForArrayOfAttributes($array, $node);
print $node->asXML();
运行此脚本 [2] 时,在 Chrome 浏览器中会显示以下内容:

22.1.2. 服务器脚本
必须修改服务器脚本 [impots-server.php] 及其配置文件 [config-server.json]:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-11",
"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",
"/Utilities/Utilitaires.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"
}
注释
- 项目根目录现为版本 11 文件夹;
- 第 16 行:已引入新的 [Utilities] 类;
服务器脚本的更改如下:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
…
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/xml");
$response->setCharset("utf-8");
…
// 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,
…
}
// 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 XML
$node = new \SimpleXMLElement("<?xml version='1.0' encoding='UTF-8'?><réponse></réponse>");
Utilitaires::getXmlForArrayOfAttributes($result, $node);
$response->setContent($node->asXML());
// headers
$response->headers->add($headers);
// shipping
$response->send();
// log
if ($logger != NULL) {
// log in jSON
$log = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$logger->write("$log\n");
$logger->close();
}
// close connection [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
评论
- 第 12 行:指定响应类型为 [application/xml];
- 第 29–59 行:服务器响应现在是 XML 格式;
- 第 41 行:创建 XML 树的根节点 [<response></response>];
- 第 42 行:将 [$result] 数组中的 XML 树填充到该树中,以便发送给客户端;
- 第 43 行:将 XML 树转换为 XML 字符串以发送给客户端;
测试
直接在 Chrome 浏览器中输入 URL [http://localhost/php7/scripts-web/impots/version-11/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=60000]。Chrome 浏览器中将显示以下结果 [1]:

22.2. 客户端
接下来我们将重点关注应用程序的客户端部分。

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

在新版本中,唯一的变化是:
- 配置文件 [config-client.json];
- 客户端的 [dao] 层;
配置文件 [config-client.json] 变为如下内容:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-11",
"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",
"/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-11/impots-server.php"
}
22.2.1. [DAO] 层
客户端 [ClientDao.php](上文第 13 行)已进行修改以适应新的响应格式。我们使用 [simpleXML] 来处理它:
<?php
namespace Application;
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// using a Trait
use TraitDao;
// attributes
private $urlServer;
private $user;
private $sessionCookie;
// 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 {
…
// we retrieve the XML response
$réponse = $response->getContent(false);
$xml = new \SimpleXMLElement($réponse);
// logs
// print "$réponse";
// retrieve response status
$statusCode = $response->getStatusCode();
// mistake?
if ($statusCode !== 200) {
// we have an error - we throw an exception
$message = \json_encode(["statut HTTP" => $statusCode, "réponse" => $xml], JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
if (!$this->sessionCookie) {
// retrieve the session cookie
$headers = $response->getHeaders();
if (isset($headers["set-cookie"])) {
// session cookie ?
foreach ($headers["set-cookie"] as $cookie) {
$match = [];
$match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
if ($match) {
$this->sessionCookie = "PHPSESSID=" . $champs[1];
}
}
}
}
// the answer is given in the form of a table
return \json_decode(\json_encode($xml, JSON_UNESCAPED_UNICODE), true);
}
}
评论
- 第 26-27 行:读取服务器的响应。这是一个 XML 文档 [<response>…</response>]。根据接收到的 XML 文档构建一个 [SimpleXMLElement] 对象;
- 第 33-37 行:若发生错误,异常消息将采用服务器响应的 JSON 字符串而非 XML 字符串。这是因为 JSON 字符串更为简洁;
- 第 53 行:结果数组分两步返回:
- 将类型为 [\SimpleXMLElement] 的 [$xml] 对象转换为 JSON;
- 我们将生成的 JSON 字符串转换为关联数组。这就是待返回的结果;
测试
若在适当的环境(数据库、身份验证、日志)中运行客户端,将获得常规结果(请查看文件 [taxpayersdata.json, results.txt, errors.json])。在服务器端,日志如下:
06/07/19 07:41:32:877 :
---nouvelle requête
06/07/19 07:41:32:882 : Autentification en cours…
06/07/19 07:41:32:883 : Authentification réussie [admin, admin]
06/07/19 07:41:32:883 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
06/07/19 07:41:32:908 : données fiscales prises en base de données
06/07/19 07:41:32:959 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
06/07/19 07:41:33:070 :
---nouvelle requête
06/07/19 07:41:33:077 : Authentification prise en session…
06/07/19 07:41:33:077 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
06/07/19 07:41:33:099 : données fiscales prises dans redis
06/07/19 07:41:33:100 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
06/07/19 07:41:33:189 :
---nouvelle requête
06/07/19 07:41:33:202 : Authentification prise en session…
06/07/19 07:41:33:202 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
06/07/19 07:41:33:233 : données fiscales prises dans redis
06/07/19 07:41:33:233 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
06/07/19 07:41:33:318 :
…
22.2.2. 测试 [Codeception]

[ClientMetierTest] 测试如下:
<?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-11");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
…
// test class
class ClientMetierTest extends 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
…
}
测试结果如下:
