20. Exercício prático – versão 10
A versão anterior mostrou que os dados fiscais, compartilhados por todos os usuários do aplicativo, deveriam ser armazenados em uma memória de escopo [Application]. Vamos usar um servidor Redis [https://redis.io] para implementar isso.
20.1. Redis
A memória de escopo [Application] será implementada por um servidor Redis. Os scripts PHP que necessitam dessa memória de aplicação serão clientes desse servidor:

20.2. Instalação do Redis
O Laragon vem com um servidor Redis desativado por padrão. Portanto, é preciso começar ativando-o:

- em [3], ative o servidor [Redis];
- em [4], mantenha a porta [6379] que os clientes Redis utilizam por padrão;
Os serviços do Laragon são reiniciados automaticamente após a ativação do Redis:

20.3. O cliente Redis no modo de comando
O servidor Redis pode ser consultado no modo de comando. Abra um terminal do Laragon (consulte o parágrafo com o link):

- em [1], o comando [redis-cli] inicia o cliente no modo de comando do servidor Redis;
Em julho de 2019, o cliente Redis podia utilizar 172 comandos para se comunicar com o servidor [https://redis.io/commands#list]. Um deles, [command count] [2], exibe este número [3].
Apresentaremos apenas aqueles de que precisaremos em nossa aplicação PHP. Usaremos o Redis para uma única finalidade: armazenar um array [‘attribut’=>’valeur’] na memória do Redis. Isso é feito com o comando do Redis [set attribut valeur] [4]. O valor pode então ser recuperado com o comando [get attribut] [5]. É tudo o que precisaremos.
Pode ser necessário esvaziar a memória do Redis. Isso é feito com o comando [flushdb] [6]. Em seguida, se solicitarmos o valor do atributo [titre] [7], obteremos uma referência [nil] [8] indicando que o atributo não foi encontrado. Também é possível usar o comando [exists] [9-10] para verificar a existência de um atributo.
Para sair do cliente Redis, digite o comando [quit] [11].
20.4. Instalação de um cliente Redis para PHP
Agora precisamos instalar um cliente Redis para PHP:

Existem várias bibliotecas que implementam um cliente Redis. Utilizaremos a biblioteca [Predis] [https://github.com/nrk/predis] (julho de 2019). Assim como as anteriores, ela é instalada com o [composer] em um terminal Laragon:

20.5. Código do servidor

O arquivo de configuração [config-server.json] sofre as seguintes alterações:
{
"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"
}
Comentários
- linhas 5-15: a versão 10 não traz nada de novo, exceto o script [impots-server.php]. Ela utiliza elementos das versões 08 e 09;
- linha 19: uma dependência necessária para a biblioteca [predis] que acabamos de instalar;
O código do servidor [impots-server.php] sofre as seguintes alterações:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// gestão de erros por PHP
ini_set("display_errors", "0");
//
// caminho do arquivo de configuração
define("CONFIG_FILENAME", "Data/config-server.json");
// alias de classe
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
// sessão
$session = new Session();
$session->start();
…
…
// primeiro registro
$logger->write("\n---nouvelle requête\n");
// recuperamos a solicitação atual
$request = Request::createFromGlobals();
// autenticação apenas na primeira vez
if (!$session->has("user")) {
…
} else {
// registro
$logger->write("Authentification prise en session…\n");
}
// temos um usuário válido — verificamos os parâmetros recebidos
$erreurs = [];
// devem existir três parâmetros GET
$method = strtolower($request->getMethod());
…
// erros?
if ($erreurs) {
// enviando um código de erro 400 HTTP_BAD_REQUEST ao cliente
sendResponse($response, ["erreurs" => $erreurs], Response::HTTP_BAD_REQUEST, [], $logger);
// concluído
exit;
} else {
// registros
$logger->write("paramètres ['marié'=>$marié, 'enfants'=>$enfants, 'salaire'=>$salaire] valides\n");
}
// Temos tudo o que é necessário para trabalhar
// Redis
\Predis\Autoloader::register();
try {
// cliente [predis]
$redis = new \Predis\Client();
// conectamos ao servidor para verificar se ele está disponível
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// erro interno do servidor
doInternalServerError("[redis], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger);
// concluído
exit;
}
// criação da camada [dao]
if (!$redis->get("taxAdminData")) {
// os dados fiscais são obtidos do banco de dados
$logger->write("données fiscales prises en base de données\n");
try {
// construção da camada [dao]
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// os dados fiscais são colocados na memória de escopo [application]
// o método [TaxAdminData]->__toString será chamado implicitamente
$redis->set("taxAdminData", $dao->getTaxAdminData());
} catch (\RuntimeException $ex) {
// observa-se o erro
doInternalServerError("[dao], " . utf8_encode($ex->getMessage()), $response, $config['adminMail'], $logger, $redis);
// concluído
exit;
}
} else {
// os dados fiscais são obtidos da memória de escopo [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
// instanciação da camada [dao]
$dao = new ServerDaoWithRedis(NULL, $taxAdminData);
// registros
$logger->write("données fiscales prises dans redis\n");
}
// criação da camada [métier]
$métier = new ServerMetier($dao);
// cálculo do imposto
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// a resposta é fornecida
sendResponse($response, $result, Response::HTTP_OK, [], $logger, $redis);
// fim
exit;
function doInternalServerError(string $message, Response $response, array $infos,
Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $message: mensagem de erro
// $response: resposta HTTP
// $infos: tabela de informações para o envio do e-mail
// $result: tabela de resultados
// $logger: o logger do aplicativo
// $predisClient: um cliente [predis]
//
//: envia-se um e-mail ao administrador
// SendAdminMail intercepta todas as exceções e as registra por conta própria
$infos['message'] = $message;
$sendAdminMail = new SendAdminMail($infos, $logger);
$sendAdminMail->send();
// envia-se um código de erro 500 ao cliente
sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger, $predisClient);
}
// função para enviar a resposta HTTP ao cliente
function sendResponse(Response $response, array $result, int $statusCode,
array $headers, Logger $logger = NULL, \Predis\Client $predisClient = NULL) {
// $response: resposta HTTP
// $result: tabela de resultados
// $statusCode: status HTTP da resposta
// $headers: cabeçalhos HTTP a serem incluídos na resposta
// $logger: o logger do aplicativo
// $predisClient: um cliente [predis]
//
// status HTTTP
$response->setStatusCode($statusCode);
// corpo
$body = \json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE);
$response->setContent($body);
// cabeçalhos
$response->headers->add($headers);
// envio
$response->send();
// registro
if ($logger != NULL) {
$logger->write("$body\n");
$logger->close();
}
// encerramento da conexão [redis]
if ($predisClient != NULL) {
$predisClient->disconnect();
}
}
Comentários
- linha 15: atribui-se o alias [ServerDaoWithRedis] à classe [\Application\ServerDaoWithSession] para refletir a mudança na implementação do script do servidor;
- linhas 18-19: a sessão é mantida. Temos aqui duas informações a serem registradas:
- o fato de o usuário ter se autenticado corretamente. Essa informação tem escopo [session]: está vinculada a um usuário específico e não é válida para outros usuários;
- os dados da administração fiscal. Essa informação tem escopo [application]: não está vinculada a um usuário específico, mas é válida para todos os usuários;
- linhas 54-64: criação do cliente [redis], que se comunicará com o servidor [redis]. Esse cliente se comunicará com a porta padrão do servidor. Se o servidor não se comunicasse em sua porta padrão ou se não estivesse na máquina [localhost], seria necessário passar essas informações para o construtor da classe [\Predis\Client];
- linha 59: conectamos imediatamente o cliente ao servidor para verificar se ele responde;
- linhas 60-65: se a conexão com o servidor Redis falhar, envia-se uma resposta de erro ao cliente e um e-mail será enviado ao administrador da aplicação;
- linha 67: solicita-se ao servidor [redis] a chave [taxAdminData]. Se ela não for encontrada, os dados fiscais são obtidos do banco de dados (linha 72);
- linha 75: a chave [taxAdminData] é inserida na memória [redis] associada à cadeia jSON da variável [$taxAdminData], que é um objeto do tipo [TaxAdminData]. O método [$redis→set] espera uma sequência de caracteres como valor da chave. Portanto, ele tentará converter o objeto do tipo [TaxAdminData] para o tipo [string]. Nesse caso, implicitamente, o método [TaxAdminData->__toString] será chamado. Esse método gera a sequência jSON a partir do objeto [TaxAdminData];
- linha 84: a chave [taxAdminData] está na memória [redis]; então, recupera-se seu valor. Sabe-se que trata-se da cadeia jSON de um objeto [TaxAdminData]. Em seguida, decodifica-se essa sequência para obter uma matriz de atributos;
- linha 85: a partir dessa matriz, um novo objeto [TaxAdminData] é instanciado;
- linha 87: a camada [dao] é instanciada;
20.6. Código do cliente

A versão 10 do cliente é idêntica à versão 9. A única alteração é no arquivo de configuração [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"
}
A única alteração, na linha 24, é o URL do servidor.
Os resultados são os mesmos da versão 09. Vamos testar um novo caso de erro:

O resultado no console é o seguinte:
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. Testes com o [Codeception] do cliente

A classe de teste [ClientMetierTest] da versão 10 é idêntica à da versão 09, com uma única exceção:
<?php
// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
// espaço de nomes
namespace Application;
// definição de constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-10");
…
}
- linha 10: o ambiente de teste é o do cliente da versão 10;
Antes de iniciar os testes, vamos excluir, usando o cliente [redis-cli], a chave [taxAdminData] da memória do servidor [redis]:

Agora, vamos executar o teste:

Agora, vamos examinar os logs [logs.txt] do servidor:
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
…
Já mencionamos que, a cada teste, o construtor da classe de teste é reexecutado, o que faz com que a classe [ClientDao] testada seja instanciada a cada teste com um cookie de sessão inexistente. Portanto, tudo ocorre como se os 11 testes representassem 11 usuários diferentes, com 11 sessões diferentes.
- linha 6: os dados fiscais são obtidos do banco de dados;
- linhas 13, 20: os dados fiscais são obtidos da memória [redis]. Trata-se, portanto, de uma memória de escopo [application] compartilhada por todos os usuários do aplicativo;
20.8. Interface web do servidor [Redis]
Vimos que o servidor [Redis] pode ser gerenciado no modo de comando. Ele também pode ser gerenciado por meio de uma interface web:

- no [4], o URL de administração;
- em [5], as chaves armazenadas pelo servidor;
- em [6], o estado atual do servidor;
Ao clicar em [5], obtêm-se informações sobre a chave [taxAdminData]:

- em [7], o URL que dá acesso às informações da chave [taxAdminData] [8];
- em [9], o status da chave;
- em [10], seu valor: reconhece-se a sequência jSON de um objeto do tipo [TaxAdminData];
- em [11], é possível excluir a chave;
- em [12], é possível adicionar outra;