Skip to content

19. 应用练习 – 第9版

在此版本中,我们将对服务器进行以下改进:

  • 目前,每次请求都会从数据库中检索税务管理数据。我们将使用会话:
    • 在用户首次请求时,从数据库中检索税务机关数据并存储在会话中;
    • 对于同一用户的后续请求,将从会话中获取税务机关数据。由于数据库查询会消耗大量资源,因此预计执行时间将略有提升;
  • 服务器将把重要事件记录到文本文件中:
    • 身份验证成功或失败;
    • 客户端发送的参数是否有效;
    • 税费计算结果;
    • 各种错误情况;
  • 若发生致命错误,将向应用程序管理员发送电子邮件;

客户端还必须进行修改,以处理将发送给它的会话 Cookie。

19.1. 服务器

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

Image

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

Image

19.1.1. 实用工具

Image

19.1.1.1. [Logger] 类

[Logger] 类将用于将日志写入文本文件:


<?php
 
namespace Application;
 
class Logger {
  // attribute
  private $resource;
 
  // manufacturer
  public function __construct(string $logsFilename) {
    // open file
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  // writing a message to the logs
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }
 
  // close log file
  public function close() {
    fclose($this->resource);
  }
 
}

评论

  • 第 7 行:日志文件资源;
  • 第 10 行:类构造函数将日志文件名作为参数接收;
  • 第 12 行:文本文件以追加模式 (a+) 打开:文件将被打开,其内容将保留。新数据将写在当前内容之后;
  • 第 13–15 行:如果无法打开文件,则抛出异常;
  • 第 19–21 行:[write] 方法将消息 [$message] 写入日志文件,并在其前添加日期和时间;
  • 第 24–16 行:[close] 方法关闭日志文件;

注意:服务器应用程序可能同时为多个客户端提供服务。然而,所有客户端共用一个日志文件。 因此,在向文件写入数据时存在并发访问的风险。必须对写入操作进行同步,以防止数据混淆。PHP为此提供了信号量 [https://www.php.net/manual/fr/book.sem.php]。本文中我们将忽略写入同步,但必须时刻注意这一问题。

19.1.1.2. [SendAdminMail] 类

[SendAdminMail] 类允许您在应用程序发生崩溃时向管理员发送电子邮件:


<?php
 
namespace Application;
 
class SendAdminMail {
  // attributes
  private $config;
  private $logger;
 
  // manufacturer
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }
 
  public function send() {
    // sends $this->config['message'] to smtp server $this->config['smtp-server'] on port $infos[smt-port]
    // if $this->config['tls'] is true, TLS support will be used
    // mail is sent from $this->config['from']
    // for recipient $this->config['to']
    // message has subject $this->config['subject']
    // attachments from $this->config['attachments'] are attached to the mail
    // the result of the method
    try {
      // message creation
      $message = (new \Swift_Message())
        // message subject
        ->setSubject($this->config["subject"])
        // sender
        ->setFrom($this->config["from"])
        // recipients with a dictionary (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // message text
        ->setBody($this->config["message"])
      ;
      // attachments
      foreach ($this->config["attachments"] as $attachment) {
        // path of attachment
        $fileName = __DIR__ . $attachment;
        // check that the file exists
        if (file_exists($fileName)) {
          // attach the document to the message
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // error
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // protocol TLS ?
      if ($this->config["tls"] === "TRUE") {
        // TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"], 'tls'))
          ->setUsername($this->config["user"])
          ->setPassword($this->config["password"]);
      } else {
        // no TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // the shipment manager
      $mailer = new \Swift_Mailer($transport);
      // sending the message
      $mailer->send($message);
      // end
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // error
      if ($this->logger !== NULL) {
        $this->logger->write("Erreur lors de l'envoi du message [{$this->config["message"]}] à {$this->config["to"]}\n");
      }
    }
  }
 
}

评论

  • 第 11 行:构造函数接受两个参数:
    • [$config]:一个关联数组,包含发送电子邮件所需的所有信息;
    • [$logger]:用于记录电子邮件发送过程中关键时刻的日志器;

该关联数组将采用以下形式:

1
2
3
4
5
6
7
8
9
{
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
}
  • 第 16–76 行:使用 [send] 方法发送电子邮件。该代码已在链接部分中展示并说明;

19.1.2. [dao] 层

Image

[ServeurDaoWithSession.php] 脚本内容如下:


<?php
 
// namespace
namespace Application;
 
// definition of a ImpotsWithDataInDatabase class
class ServerDaoWithSession extends ServerDao {
 
  // manufacturer
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // simplest case
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // hand over to parent class
      parent::__construct($databaseFilename);
    }
  }
 
}

评论

  • 第 7 行:09 版本中的 [ServerDaoWithSession] 类继承自 08 版本的 [ServerDao] 类。事实上,[ServerDao] 类已经掌握了数据库的使用方法。剩下的工作就是处理税务管理数据已被检索的情况:
  • 第 10 行:构造函数现在接受两个参数:
    • [string $databaseFilename]:若税务管理数据尚未获取,则为包含数据库连接信息的文件名;否则为 NULL;
    • [TaxAdminData $taxAdminData]:若税务管理数据已获取则传入该数据,否则为 NULL;

当 Web 会话开始时,[dao] 层将使用非 NULL 的 [$databaseFilename] 对象和 NULL 的 [taxAdminData] 对象进行构建。随后将从数据库中检索税务管理数据并存储在会话中。 对于同一会话内的后续请求,[dao] 层将使用一个 NULL 的 [databaseFilename] 对象以及从会话中检索到的 [taxAdminData] 对象(该对象不为 NULL)进行构建。因此,不会发生数据库查询。

19.1.3. 服务器脚本

服务器脚本 [impots-server.php] 由以下 JSON 文件 [config-server.json] 进行配置:


{
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09",
    "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",
        "/Dao/ServerDaoWithSession.php",
        "/../version-08/Métier/InterfaceServerMetier.php",
        "/../version-08/Métier/ServerMetier.php",
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php"
    ],
    "absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/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"
}

服务器脚本 [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");
 
// 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";
}
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
 
// session
$session = new Session();
$session->start();
 
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
 
// log file creation
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // internal server error
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // completed
  exit;
}
 
// 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")) {
  // log
  $logger->write("Autentification en cours…\n");
  // authentication

  }
  // has the user been found?
  if (!$trouvé) {
    // not found - code 401 HTTP_UNAUTHORIZED
    sendResponse(
      $response,
      ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"],
      Response::HTTP_UNAUTHORIZED,
      ["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")],
      $logger
    );
    // completed
    exit;
  } else {
    // we note in the session that we have authenticated the user
    $session->set("user", TRUE);
    // log
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} 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

 
// 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 have everything you need to work
// creation of the [dao] layer
if (!$session->has("taxAdminData")) {
  // the 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 ServerDaoWithSession($config["databaseFilename"], NULL);
    // put data in session
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // we note the error
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // completed
    exit;
  }
} else {
  // data are taken from the session
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // logs
  $logger->write("données fiscales prises en session\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);
// end
exit;
 
function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // 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);
}
 
// function to send HTTP response to client
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $response : answer HTTP
  // $result: results table
  // $statusCode: HTTP response status
  // $headers: HTTP headers to be included in the response
  // $logger: application logger
  //
  // 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();
  }
}

注释

  • 第 34–35 行:建立会话;
  • 第 38–40 行:准备 JSON 响应;
  • 第 42–50 行:尝试创建日志文件。如果发生异常,则调用 [doInternalServer] 方法(第 132–140 行);
  • 第 132 行:[doInternalServer] 方法接受四个参数:
    • [$message]:待记录的消息。必须采用 UTF-8 编码;
    • [$response]:封装服务器对客户端响应的 [Response] 对象;
    • [$logger]:用于记录日志的 [Logger] 对象;
    • [$infos]:用于向应用程序管理员发送电子邮件的信息;
  • 第 135–137 行:向应用程序管理员发送一封电子邮件;
  • 第 139 行:将响应发送给客户端:
    • $response:HTTP 响应;
    • $result:服务器发送来自数组 [‘response’=>["error" => $message]] 的 JSON 字符串;
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR],状态码 500;
    • $headers: [], 响应中无需添加 HTTP 头部;
    • $logger:应用程序日志记录器;
  • 第 58 行:得益于会话的设置,我们只需对客户端进行一次身份验证:
    • 客户端通过身份验证后,我们将在会话中设置 [user] 键(第 78 行);
    • 当同一客户端发起下一次请求时,第 58 行将防止不必要的身份验证;
  • 第 103 行:得益于已建立的会话,我们只需对数据库进行一次查询:
    • 在首次请求时,将执行数据库查询(第 108 行)。随后,检索到的数据将存储在与会话关联的 [taxAdminData] 键中(第 110 行);
    • 对于后续请求,将在会话中找到 [taxAdminData] 键(第 103 行),随后磁盘上的数据将直接传递给 [dao] 层(第 119 行);
  • 第 111–116 行:数据库中的税务数据检索可能失败。此时,将向客户端发送 [500 Internal Server Error] 状态码;
  • 第 113 行:MySQL 驱动程序异常返回的错误消息采用 ISO 8859-1 编码。为确保正确记录,将其转换为 UTF-8;
  • 其余代码与上一版本几乎完全相同;
  • 第 143–164 行:[sendResponse] 函数将所有响应发送给客户端;
  • 第 144–148 行:参数的含义;
  • 第 153 行:响应始终是数组的 JSON 字符串 [‘result’=>something]
  • 第 156 行:有时需要向响应中添加 HTTP 头部。第 71 行即为这种情况;
  • 第 158 行:发送响应;
  • 第 160–163 行:记录响应并关闭日志器;

19.1.4. [Codeception] 测试

Image

我们将仅测试 [dao] 层,因为这是唯一发生变化的层。

[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-09");
// 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 ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  public function testTaxAdminData() {

  }
 
}
  • 第 9–24 行:我们创建了一个与服务器脚本 [impots-server] 完全相同的运行时环境;
  • 第 38 行:为了构建 [dao] 层,我们实例化 [ServerDaoWithSession] 类;

测试结果如下:

Image

19.2. 客户端

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

Image

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

Image

在新版本中,唯一的变化是:

  • 配置文件 [config-client.json]
  • 客户端的 [dao] 层;

19.2.1. [dao] 层

[Dao] 层的演变如下:


<?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 {
    // session cookie ?
    if (!$this->sessionCookie) {
      // create a HTTP customer
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // make the request to the server without a session cookie
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // make a request to the server with the session cookie
      // create a HTTP customer
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // 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);
    }
    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];
          }
        }
      }
    }
    // we return the answer
    return $réponse;
  }
 
}

评论

[dao] 层的修改现在涉及会话管理:

  • 第 14 行:会话 Cookie;
  • 第 25–39 行:在首次请求时,该 Cookie 不存在;因此,我们通过发送身份验证信息(第 28 行)向服务器发送请求;
  • 第 40–53 行:对于后续请求,我们通常已持有会话 Cookie。因此,我们不再发送身份验证信息(第 42–44 行);
  • 第 69–82 行:服务器对首次请求的响应将包含一个会话 Cookie。我们将其检索出来。该代码已在链接的章节中使用并解释过;
  • 第 78 行:检索到的会话 Cookie 存储在类属性 [$sessionCookie] 中;

:鉴于成本微乎其微,我们本可以保留旧版的 [dao] 层,并在每次请求时执行身份验证。出于教学目的,我们希望演示 HTTP 客户端如何管理会话。

19.2.2. 配置文件

JSON 配置文件演变如下:


{
    "rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-09",
    "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-09/impots-server.php"
}

仅第24行的URL发生变化。

19.3. 一些测试

19.3.1. 测试 1

首先,我们在无错误的环境中运行客户端。结果与之前的版本相同。但现在,在服务器端,我们有一个日志文件 [logs.txt]


04/07/19 13:16:08:523 :
---nouvelle requête
04/07/19 13:16:08:529 : Autentification en cours
04/07/19 13:16:08:529 : Authentification réussie [admin, admin]
04/07/19 13:16:08:529 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:16:08:529 : tranches d'impôts prises en base de données
04/07/19 13:16:08:534 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:643 :
---nouvelle requête
04/07/19 13:16:08:648 : Authentification prise en session
04/07/19 13:16:08:648 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:16:08:648 : tranches d'impôts prises en session
04/07/19 13:16:08:648 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:16:08:769 :
---nouvelle requête
04/07/19 13:16:08:775 : Authentification prise en session
04/07/19 13:16:08:775 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:16:08:775 : tranches d'impôts prises en session
04/07/19 13:16:08:775 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:16:08:888 :
---nouvelle requête

  • 第3-7行:在首次请求期间,进行身份验证并从数据库中检索数据;
  • 第9-14行:在后续请求中,不再进行身份验证,数据从会话中获取。此后(第15行及之后)的请求均重复此过程;

19.3.2. 测试 2

现在让我们关闭 MySQL 数据库。在客户端,我们会看到以下控制台输出:


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é

在服务器端,我们有以下日志 [logs.txt]


04/07/19 13:19:52:396 :
---nouvelle requête
04/07/19 13:19:52:405 : Autentification en cours…
04/07/19 13:19:52:405 : Authentification réussie [admin, admin]
04/07/19 13:19:52:405 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:19:52:405 : tranches d'impôts prises en base de données
04/07/19 13:19:54:461 : {"réponse":{"erreur":"SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.\r\n"}}
04/07/19 13:19:55:602 : Message [SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.
] envoyé à guest@localhost
04/07/19 13:19:55:706 :
---nouvelle requête

为了检索应用程序管理员收到的电子邮件,我们使用来自以下配置文件 [config-imap-01.json] 所关联部分的 [imap-03.php] 脚本:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

得到以下结果:

Image

文件 [message_1.txt] 包含以下文本:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:20:22 +0200
message-id: <c82d26df5fb352e10a51577cd1b9ed87@localhost>
date: Thu, 04 Jul 2019 13:20:20 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable
 
SQLSTATE[HY000] [2002] Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée.

19.3.3. 测试 3

现在,让我们确保无法创建 [logs.txt] 文件。要做到这一点,只需创建一个名为 [logs.txt] 的文件夹:

Image

完成上述操作后,让我们运行客户端。

在客户端,我们会看到以下控制台输出:


L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé

服务器端没有日志,但管理员收到了以下电子邮件:


return-path: guest@localhost
received: from localhost (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Thu, 4 Jul 2019 15:31:49 +0200
message-id: <b2cee274f3437952231d62152ba1cdb3@localhost>
date: Thu, 04 Jul 2019 13:31:48 +0000
subject: plantage du serveur de calcul d'impôts
from: guest@localhost
to: guest@localhost
mime-version: 1.0
content-type: text/plain; charset=utf-8
content-transfer-encoding: quoted-printable
 
Echec lors de la création du fichier de logs [Data/logs.txt]

19.3.4. 测试 4

这次,让我们在客户端配置文件中为连接的客户端提供错误的凭据。

客户端显示以下控制台输出:


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

在服务器端,出现了以下日志:


---nouvelle requête
04/07/19 13:36:05:789 : Autentification en cours…
04/07/19 13:36:05:789 : {"réponse":{"erreur":"Echec de l'authentification [x, x]"}}

19.3.5. 测试 5

让我们将正确的用户名和密码 [admin, admin] 重新写入客户端配置文件。

现在,让我们在浏览器中直接访问服务器的 URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php],且不传递任何参数:

在服务器的日志文件 [logs.txt] 中,我们可以看到以下几行:


---nouvelle requête
04/07/19 13:37:33:711 : Autentification en cours…
04/07/19 13:37:33:711 : Authentification réussie [admin, admin]
04/07/19 13:37:33:711 : {"réponse":{"erreurs":["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]","paramètre marié manquant","paramètre enfants manquant","paramètre salaire manquant"]}}

19.4. 测试 [Codeception]

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

Image

19.4.0.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-09");
 
// 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";
}
//
// uses
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;
 
// test class
class ClientMetierTest extends Unit {
  
}

注释

  • 与 08 版本中的测试类相比,唯一的改动是第 10 行,该行指定了待测试客户端的根目录;

测试结果如下:

Image

建议查看服务器日志 [logs.txt]


04/07/19 13:48:48:525 :
---nouvelle requête
04/07/19 13:48:48:536 : Autentification en cours…
04/07/19 13:48:48:536 : Authentification réussie [admin, admin]
04/07/19 13:48:48:536 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>55555] valides
04/07/19 13:48:48:536 : données fiscales prises en base de données
04/07/19 13:48:48:548 : {"réponse":{"impôt":2814,"surcôte":0,"décôte":0,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:635 :
---nouvelle requête
04/07/19 13:48:48:645 : Autentification en cours…
04/07/19 13:48:48:645 : Authentification réussie [admin, admin]
04/07/19 13:48:48:645 : paramètres ['marié'=>oui, 'enfants'=>2, 'salaire'=>50000] valides
04/07/19 13:48:48:645 : données fiscales prises en base de données
04/07/19 13:48:48:655 : {"réponse":{"impôt":1384,"surcôte":0,"décôte":384,"réduction":347,"taux":0.14}}
04/07/19 13:48:48:751 :
---nouvelle requête
04/07/19 13:48:48:762 : Autentification en cours…
04/07/19 13:48:48:762 : Authentification réussie [admin, admin]
04/07/19 13:48:48:762 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>50000] valides
04/07/19 13:48:48:762 : données fiscales prises en base de données
04/07/19 13:48:48:773 : {"réponse":{"impôt":0,"surcôte":0,"décôte":720,"réduction":0,"taux":0.14}}
04/07/19 13:48:48:865 :
---nouvelle requête

---nouvelle requête
04/07/19 13:48:49:546 : Autentification en cours…
04/07/19 13:48:49:546 : Authentification réussie [admin, admin]
04/07/19 13:48:49:546 : paramètres ['marié'=>oui, 'enfants'=>3, 'salaire'=>200000] valides
04/07/19 13:48:49:546 : données fiscales prises en base de données
04/07/19 13:48:49:551 : {"réponse":{"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}}

我们可以看到,税务机关的数据总是从数据库中检索,从未从会话中获取。让我们回到已执行测试的代码:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 

 
// 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
  public function test1() {

  }
 
  public function test2() {

  }
 
  public function test3() {

  }
 

 
}

[Codeception] 测试类中,构造函数会在每次测试时被调用

  • 第 21 行:因此,每个测试都会创建一个新的 [ClientDao],且其会话 Cookie 为 NULL。这解释了为何该客户端无法利用任何会话;

此示例表明,会话并非存储税务管理数据的合适场所。事实上,这些数据在应用程序的所有用户之间共享。然而,在此处,这些数据却在每个用户的会话中被重复存储。

在 Web 编程中,共享数据有三种可见性类型:

  • 由 Web 应用程序所有用户共享的数据。这通常是只读数据。PHP 本身并不支持此类存储;
  • 在同一客户端的不同请求之间共享的数据。此类数据存储在会话中。我们将其称为客户端会话,以表示客户端的存储空间。来自同一客户端的所有请求均可访问该会话,并在其中存储和读取信息。在之前的脚本中,该会话由 Symfony 对象 [HttpFoundation\Session\Session] 实现;
  • 请求内存,或称请求上下文。用户的请求可能由多个连续的操作进行处理。请求上下文允许操作 1 将信息传递给操作 2。在之前的脚本中,请求由 Symfony 对象 [HttpFoundation\Request] 实现,其内存则由属性 [HttpFoundation\Request::attributes] 管理;

Image

现有第三方库可为 PHP 提供应用程序状态管理功能。本应用练习的新版本演示了其中一种库的用法。