19. Exercício prático – Versão 9
Nesta versão, iremos melhorar o servidor da seguinte forma:
- Atualmente, a cada pedido, os dados da administração fiscal são recuperados da base de dados. Iremos utilizar uma sessão:
- Durante a primeira solicitação de um utilizador, os dados da autoridade fiscal são recuperados da base de dados e armazenados na sessão;
- Para pedidos subsequentes do mesmo utilizador, os dados da autoridade fiscal são recuperados da sessão. Podemos esperar uma ligeira melhoria no tempo de execução, uma vez que as consultas à base de dados consomem muitos recursos;
- o servidor irá registar eventos importantes num ficheiro de texto:
- autenticação bem-sucedida ou falhada;
- se os parâmetros enviados pelo cliente são válidos;
- o resultado do cálculo do imposto;
- vários casos de erro;
- em caso de erro fatal, será enviado um e-mail ao administrador da aplicação;
O cliente também deve ser modificado para lidar com o cookie de sessão que lhe será enviado.
19.1. O servidor
Estamos a concentrar-nos no lado do servidor da aplicação.

Esta arquitetura será implementada pelos seguintes scripts:

19.1.1. Utilitários

19.1.1.1. A classe [Logger]
A classe [Logger] será utilizada para gravar registos num ficheiro de texto:
<?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);
}
}
Comentários
- linha 7: o recurso do ficheiro de registo;
- linha 10: o construtor da classe recebe o nome do ficheiro de registo como parâmetro;
- linha 12: o ficheiro de texto é aberto no modo de adição (a+): o ficheiro será aberto e o seu conteúdo preservado. Os novos dados serão gravados após o conteúdo atual;
- linhas 13–15: se o ficheiro não puder ser aberto, é lançada uma exceção;
- linhas 19–21: o método [write] grava a mensagem [$message] no ficheiro de registo, precedida da data e hora;
- linhas 24–16: o método [close] fecha o ficheiro de registo;
Nota: A aplicação do servidor pode servir vários clientes simultaneamente. No entanto, existe apenas um ficheiro de registo para todos eles. Existe, portanto, um risco de acesso simultâneo ao escrever no ficheiro. As gravações devem, portanto, ser sincronizadas para evitar que se misturem. O PHP fornece semáforos [https://www.php.net/manual/fr/book.sem.php] para este fim. Ignoraremos a sincronização de gravação aqui, mas devemos estar cientes da questão.
19.1.1.2. A classe [SendAdminMail]
A classe [SendAdminMail] permite enviar um e-mail ao administrador da aplicação em caso de falha:
<?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");
}
}
}
}
Comentários
- Linha 11: O construtor recebe dois parâmetros:
- [$config]: um array associativo contendo todas as informações necessárias para enviar o e-mail;
- [$logger]: um logger utilizado para registar momentos-chave durante o processo de envio do e-mail;
A matriz associativa terá o seguinte formato:
- Linhas 16–76: O método [send] é utilizado para enviar o e-mail. Este código foi apresentado e descrito na secção indicada;
19.1.2. A camada [dao]

O script [ServeurDaoWithSession.php] é o seguinte:
<?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);
}
}
}
Comentários
- linha 7: a classe [ServerDaoWithSession] na versão 09 estende a classe [ServerDao] da versão 08. De facto, a classe [ServerDao] sabe como utilizar a base de dados. Resta apenas tratar o caso em que os dados da administração fiscal já foram recuperados:
- linha 10: o construtor recebe agora dois parâmetros:
- [string $databaseFilename]: nome do ficheiro que contém as informações necessárias para se ligar à base de dados, caso os dados da administração fiscal ainda não tenham sido recuperados; caso contrário, NULL;
- [TaxAdminData $taxAdminData]: os dados da administração fiscal, caso já tenham sido recuperados; NULL caso contrário;
Quando uma sessão web é iniciada, a camada [dao] será construída com um objeto [$databaseFilename] diferente de NULL e um objeto [taxAdminData] com valor NULL. Os dados da administração fiscal serão então recuperados da base de dados e armazenados na sessão. Para pedidos subsequentes dentro da mesma sessão, a camada [dao] será construída com um objeto [databaseFilename] NULL e um objeto [taxAdminData] recuperado da sessão (que não é NULL). Portanto, não ocorrerá qualquer consulta à base de dados.
19.1.3. O script do servidor
O script do servidor [impots-server.php] é configurado pelo seguinte ficheiro 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"
}
O script do servidor [impots-server.php] é alterado da seguinte forma:
<?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();
}
}
Comentários
- linhas 34-35: iniciar uma sessão;
- linhas 38–40: preparar uma resposta JSON;
- linhas 42–50: tentar criar o ficheiro de registo. Se ocorrer uma exceção, o método [doInternalServer] (linhas 132–140) é chamado;
- linha 132: o método [doInternalServer] aceita quatro parâmetros:
- [$message]: a mensagem a registar. Deve estar codificada em UTF-8;
- [$response]: o objeto [Response] que encapsula a resposta do servidor ao seu cliente;
- [$logger]: o objeto [Logger] utilizado para o registo;
- [$infos]: as informações utilizadas para enviar um e-mail ao administrador da aplicação;
- linhas 135–137: é enviado um e-mail ao administrador da aplicação;
- linha 139: a resposta é enviada ao cliente:
- $response: resposta HTTP;
- $result: o servidor envia a cadeia JSON da matriz [‘response’=>["error" => $message]];
- $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], código 500;
- $headers: [], sem cabeçalhos HTTP para adicionar à resposta;
- $logger: o registador da aplicação;
- linha 58: graças à configuração da sessão, só iremos autenticar o cliente uma vez:
- assim que o cliente for autenticado, definiremos uma chave [user] na sessão (linha 78);
- durante o próximo pedido do mesmo cliente, a linha 58 evita uma autenticação desnecessária;
- linha 103: graças à sessão estabelecida, só iremos pesquisar a base de dados uma vez:
- durante a primeira solicitação, a pesquisa na base de dados será realizada (linha 108). Os dados recuperados são então armazenados na sessão (linha 110) associados à chave [taxAdminData];
- para pedidos subsequentes, a chave [taxAdminData] será encontrada na sessão (linha 103), e os dados no disco serão então passados diretamente para a camada [dao] (linha 119);
- linhas 111–116: a pesquisa de dados fiscais na base de dados pode falhar. Neste caso, é enviado um código [500 Internal Server Error] ao cliente;
- linha 113: a mensagem de erro da exceção do controlador MySQL está codificada em ISO 8859-1. É convertida para UTF-8 para ser registada corretamente;
- o resto do código é quase idêntico ao da versão anterior;
- linhas 143–164: a função [sendResponse] envia todas as respostas ao cliente;
- linhas 144–148: significado dos parâmetros;
- linha 153: a resposta é sempre a cadeia JSON de um array [‘result’=>something];
- linha 156: por vezes, há cabeçalhos HTTP a adicionar à resposta. É o caso na linha 71;
- linha 158: a resposta é enviada;
- linhas 160–163: a resposta é registada e o logger é fechado;
19.1.4. [Codeception] Testes

Iremos testar apenas a camada [dao], que é a única que sofreu alterações.
O código de teste [ServerDaoTest] é o seguinte:
<?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() {
…
}
}
- linhas 9–24: criamos um ambiente de execução idêntico ao do script do servidor [impots-server];
- linha 38: para construir a camada [dao], instanciamos a classe [ServerDaoWithSession];
Os resultados do teste são os seguintes:

19.2. O cliente
Estamos a concentrar-nos no lado do cliente da aplicação.

Esta arquitetura será implementada pelos seguintes scripts:

Na nova versão, as únicas alterações são:
- o ficheiro de configuração [config-client.json];
- a camada [dao] do cliente;
19.2.1. A camada [dao]
A camada [Dao] evolui da seguinte forma:
<?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;
}
}
Comentários
A modificação na camada [dao] envolve agora a gestão de uma sessão:
- linha 14: o cookie de sessão;
- linhas 25–39: durante o primeiro pedido, este cookie não existe; por isso, enviamos o pedido ao servidor, enviando as informações de autenticação (linha 28);
- linhas 40–53: para pedidos subsequentes, normalmente temos o cookie de sessão. Por isso, não enviamos as informações de autenticação (linhas 42–44);
- linhas 69-82: a resposta do servidor à primeira solicitação incluirá um cookie de sessão. Nós o recuperamos. Este código já foi utilizado e explicado na secção indicada;
- linha 78: o cookie de sessão recuperado é armazenado no atributo da classe [$sessionCookie];
Nota: Poderíamos ter mantido a versão antiga da camada [dao] e realizado a autenticação em cada pedido, uma vez que o custo é insignificante. Para fins educativos, quisemos demonstrar como um cliente HTTP pode gerir uma sessão.
19.2.2. O ficheiro de configuração
O ficheiro de configuração JSON evolui da seguinte forma:
{
"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"
}
Apenas a URL na linha 24 muda.
19.3. Alguns testes
19.3.1. Teste 1
Primeiro, executamos o cliente num ambiente sem erros. Os resultados são os mesmos das versões anteriores. Mas agora, no lado do servidor, temos um ficheiro de registo [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
…
- linhas 3-7: durante a primeira solicitação, ocorre a autenticação e os dados são recuperados da base de dados;
- linhas 9-14: durante a solicitação seguinte, não há mais autenticação e os dados são recuperados da sessão. Isto repete-se para as solicitações subsequentes (linhas 15 e seguintes);
19.3.2. Teste 2
Agora vamos encerrar a base de dados MySQL. No lado do cliente, obtemos a seguinte saída da consola:
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é
No lado do servidor, temos os seguintes registos [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
…
Para recuperar o e-mail recebido pelo administrador da aplicação, utilizamos o script [imap-03.php] da secção associada ao seguinte ficheiro de configuração [config-imap-01.json]:
O resultado obtido é o seguinte:

O ficheiro [message_1.txt] contém o seguinte texto:
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. Teste 3
Agora vamos garantir que o ficheiro [logs.txt] não possa ser criado. Para tal, basta criar uma pasta [logs.txt]:

Depois de fazer isso, vamos executar o cliente.
No lado do cliente, obtemos a seguinte saída na consola:
L'erreur suivante s'est produite : {"statut HTTP":500,"erreur":"Echec lors de la création du fichier de logs [Data\/logs.txt]"}
Terminé
No lado do servidor, não existem registos, mas o administrador recebe o seguinte e-mail:
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. Teste 4
Desta vez, vamos fornecer credenciais incorretas ao cliente de ligação no ficheiro de configuração do cliente.
O cliente apresenta a seguinte saída na consola:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
No lado do servidor, aparecem os seguintes registos:
---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. Teste 5
Vamos colocar o nome de utilizador e a palavra-passe corretos [admin, admin] de volta no ficheiro de configuração do cliente.
Agora, vamos aceder ao URL do servidor [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] diretamente num navegador, sem passar quaisquer parâmetros:
No ficheiro de registo do servidor [logs.txt], vemos as seguintes linhas:
---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. Testes [Codeception]
Tal como fizemos nas versões anteriores, iremos escrever testes [Codeception] para a versão 09.

19.4.0.1. Teste da camada [Business]
O teste [ClientMetierTest.php] é o seguinte:
<?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 {
…
}
Comentários
- Em comparação com a classe de teste da versão 08, a única alteração é a linha 10, que especifica o diretório raiz do cliente a ser testado;
Os resultados do teste são os seguintes:

Vale a pena verificar os registos do servidor [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}}
Podemos ver que os dados da autoridade fiscal são sempre recuperados da base de dados e nunca da sessão. Voltemos ao código do teste executado:
<?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() {
…
}
…
}
Numa classe de teste [Codeception], o construtor é executado para cada teste.
- Linha 21: É, portanto, criado um novo [ClientDao] para cada teste com um cookie de sessão NULL. Isto explica por que razão este cliente não beneficia de qualquer sessão;
Este exemplo mostra-nos que a sessão não é o local adequado para armazenar dados de administração fiscal. Com efeito, estes dados são partilhados entre todos os utilizadores da aplicação. No entanto, aqui são duplicados em cada uma das suas sessões.
Na programação web, existem três tipos de visibilidade para dados partilhados:
- dados partilhados por todos os utilizadores da aplicação web. Trata-se geralmente de dados de leitura única. O PHP não suporta nativamente este tipo de armazenamento;
- dados partilhados entre pedidos do mesmo cliente. Estes dados são armazenados na sessão. Referimo-nos a isto como a sessão do cliente para indicar o armazenamento do cliente. Todos os pedidos de um cliente têm acesso a esta sessão. Podem armazenar e ler informações nessa sessão. Nos scripts anteriores, esta sessão é implementada pelo objeto Symfony [HttpFoundation\Session\Session];
- a memória da solicitação, ou contexto da solicitação. A solicitação de um utilizador pode ser processada por várias ações sucessivas. O contexto da solicitação permite que a Ação 1 passe informações para a Ação 2. Nos scripts anteriores, a solicitação é implementada pelo objeto Symfony [HttpFoundation\Request] e a sua memória pelo atributo [HttpFoundation\Request::attributes];

Existem bibliotecas de terceiros para fornecer o estado da aplicação ao PHP. A nova versão do exercício da aplicação demonstra a utilização de uma delas.